LabHub
배우기 러닝패스 코스

Where Distributed Tracing Breaks

The span is there, but nothing reproduces

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

실패한 요청의 스팬 한 줄에서 출발해, 그 스팬만 들고 같은 실패를 다시 낼 수 있을 만큼 속성과 이벤트를 채웁니다. 넣으면 안 되는 값을 바꿔 남기는 법, 고유값 예산을 세는 법, 팀 규약을 파일로 적고 검사기로 강제하는 법까지 한 바퀴 돕니다.

왜 중요한가

자동 계측은 HTTP 겉면을 채워 준다. 경로와 상태 코드와 길이는 나오지만, 새벽에 그 스팬을 열어 놓고 '그래서 무엇을 넣으면 이게 다시 나나' 를 물으면 답이 없다. 재현 입력과 그때의 상태와 우리가 한 일을 속성으로 남겨야 조사가 시작된다. 반대로 요청 본문을 통째로 넣으면 연락처와 토큰이 관측 백엔드에 그대로 쌓인다 — 그래서 원문 대신 해시·범주·길이로 바꿔 남긴다. 시점이 있는 사실은 속성이 아니라 이벤트이고, 부모-자식이 아닌 관계는 링크다. 마지막으로 열쇠마다 고유값이 몇 가지인지를 세어 두지 않으면, 예외 메시지 하나가 열쇠를 수천 가지 값으로 부풀려 검색 화면을 죽인다.

단계

  1. /opt/app/tracelab/tp_attrs/failed.jsonl 한 줄을 읽어 보세요. 실패한 요청의 스팬인데 재현에 필요한 값이 없습니다. /root/tp-attrs/01-missing.txt 에 다섯 줄을 적으세요 — 각 줄은 <열쇠이름>=<왜 필요한가> 이고, 열쇠는 차례로 shop.cart.item_count · shop.request.body_bytes · shop.cache.hit · shop.queue.depth · shop.payment.retry_count 입니다. 이유는 열쇠마다 25자 이상으로, '이 값이 없으면 무엇을 판단할 수 없는가' 를 자기 말로 적습니다.
  2. /root/tp-attrs/02_attrs.py 를 만드세요. tracelab.tp_attrs.orders 로 주문 ord-1010 를 처리하면서 POST /checkout 이라는 SERVER 스팬 하나를 만들고, 1단계의 다섯 열쇠에 주문 번호 shop.order.id 를 더해 여섯 속성을 답니다. 결제는 charge(주문번호, 시도번호) 를 1·2·3 으로 세 번까지 시도하고, shop.payment.retry_count 는 (시도 횟수 − 1) 입니다. 덤프 기본 경로는 /root/tp-attrs/02-attrs.jsonl 이고 환경변수 TRACELAB_OUT 이 있으면 그쪽을 씁니다.
  3. /root/tp-attrs/03_redact.py 를 만드세요. 2단계에 세 속성을 더합니다 — shop.customer.email_hash 는 이메일의 SHA-256 16진 문자열 앞 16자, shop.payment.card_brand 는 카드의 brand 값, shop.auth.token_len 은 인증 토큰의 길이(정수)입니다. 이메일·카드 번호(pan)·토큰·우편번호의 원문은 어느 속성에도 남기지 않습니다. 덤프 기본 경로는 /root/tp-attrs/03-redact.jsonl 입니다.
  4. /root/tp-attrs/04_events.py 를 만드세요. 3단계에 더해 (1) 캐시가 빗나갔으면 cache.miss 이벤트를 하나, (2) 결제 시도가 실패할 때마다 payment.attempt.failed 이벤트를 하나씩 남깁니다 — 이벤트 속성은 attempt(정수 시도 번호)와 reason(PaymentErrorkind)입니다. (3) 세 번 다 실패하면 예외 메시지를 shop.error.message 속성에 그대로 넣고 스팬 상태를 ERROR 로 바꿉니다. 덤프 기본 경로는 /root/tp-attrs/04-events.jsonl 입니다. 이 단계의 shop.error.message 는 5단계에서 다시 봅니다.
  5. /root/tp-attrs/05_bulk.py 를 만들어 orders.ORDER_IDS 의 200건을 같은 계측으로 한 번씩 처리하세요(요청마다 스팬 하나). 덤프 기본 경로는 /root/tp-attrs/05-bulk.jsonl 입니다. 그다음 /root/tp-attrs/05-cardinality.tsv 에 덤프에 나온 속성 열쇠마다 한 줄씩 <열쇠><탭><고유값수><탭><판정>열쇠 이름 오름차순으로 적으세요. 판정은 shop.order.idshop.customer.email_hashfree, 그 밖에 고유값이 20 이하면 low, 20 을 넘으면 leak 입니다.
  6. /root/tp-attrs/convention.tsv 를 만드세요. 한 줄이 열쇠 하나이고 탭으로 나눈 네 칸 <열쇠><탭><타입><탭><허용값><탭><고유값성격> 입니다. 타입은 string·int·bool·deny 중 하나, 허용값은 *(제한 없음)이나 | 로 이은 목록, 고유값 성격은 free 또는 low 입니다. 5단계에 나온 열쇠 열 개 중 shop.error.messagedeny 로 막고 대신 shop.error.kinddeclined|timeout 으로 새로 넣으세요. 8단계에서 쓸 shop.refund.amount(int·free)도 미리 넣고, 원문 금지 열쇠 shop.customer.email · shop.payment.card_pan · shop.auth.tokendeny 로 적습니다. deny 줄의 허용값과 성격 칸은 - 로 둡니다.
  7. /root/tp-attrs/lint_spans.py 를 만드세요. python3 lint_spans.py <규약파일> <덤프> 로 부르면 규약을 어긴 열쇠마다 VIOLATION <열쇠> <이유>열쇠 이름 오름차순으로 한 번씩만 찍고 종료 코드 1, 어긴 것이 없으면 OK 로 시작하는 한 줄과 종료 코드 0 을 냅니다. 봐야 할 것은 네 가지입니다 — 규약에 없는 열쇠, deny 로 막은 열쇠, 타입이 다른 값, 허용값 목록에 없는 값. 여기에 더해 low 로 선언한 열쇠의 고유값이 덤프 안에서 20 을 넘으면 그것도 위반입니다. 만든 뒤 /root/tp-attrs/05-bulk.jsonl/opt/app/tracelab/tp_attrs/noisy.jsonl 에 각각 돌리고, /root/tp-attrs/07-violations.tsv 에 두 줄 <덤프파일이름><탭><깨진 열쇠들을 쉼표로 이은 것> 을 그 순서로 적으세요.
  8. /root/tp-attrs/08_refund.py 를 만들어 큐가 시킨 환불 ord-1027 를 처리하는 POST /refund SERVER 스팬 하나를 만드세요. 속성은 shop.order.id · shop.refund.amount · shop.queue.depth · shop.customer.email_hash · shop.payment.card_brand · shop.auth.token_len · shop.payment.retry_count 이고, 실패로 끝나면 shop.error.kind 를 더하고 상태를 ERROR 로 둡니다(원문 메시지는 넣지 않습니다). 실패한 시도마다 payment.attempt.failed 이벤트를 남기고, orders.job_context(주문번호) 가 주는 트레이스 좌표를 링크로 답니다(부모로 붙이지 않습니다). 덤프 기본 경로는 /root/tp-attrs/08-refund.jsonl 이고, 7단계의 검사기를 이 덤프에 돌리면 OK 가 나와야 합니다.

참고

실패한 스팬을 놓고 없는 값을 적는다

/opt/app/tracelab/tp_attrs/failed.jsonl 한 줄을 읽어 보세요. 실패한 요청의 스팬인데 재현에 필요한 값이 없습니다. /root/tp-attrs/01-missing.txt 에 다섯 줄을 적으세요 — 각 줄은 <열쇠이름>=<왜 필요한가> 이고, 열쇠는 차례로 shop.cart.item_count · shop.request.body_bytes · shop.cache.hit · shop.queue.depth · shop.payment.retry_count 입니다. 이유는 열쇠마다 25자 이상으로, '이 값이 없으면 무엇을 판단할 수 없는가' 를 자기 말로 적습니다.

덤프를 보기 좋게 펴려면 python3 -m json.tool /opt/app/tracelab/tp_attrs/failed.jsonl 이 편합니다. 스팬에 지금 있는 것은 HTTP 겉면뿐입니다 — 경로, 상태 코드, 길이. 그것만으로 같은 실패를 다시 낼 수 있는지 스스로 물어보세요. 채점기는 다섯 열쇠가 정말 그 스팬에 없는지도 확인합니다.

재현에 필요한 값을 속성으로 더한다

/root/tp-attrs/02_attrs.py 를 만드세요. tracelab.tp_attrs.orders 로 주문 ord-1010 를 처리하면서 POST /checkout 이라는 SERVER 스팬 하나를 만들고, 1단계의 다섯 열쇠에 주문 번호 shop.order.id 를 더해 여섯 속성을 답니다. 결제는 charge(주문번호, 시도번호) 를 1·2·3 으로 세 번까지 시도하고, shop.payment.retry_count 는 (시도 횟수 − 1) 입니다. 덤프 기본 경로는 /root/tp-attrs/02-attrs.jsonl 이고 환경변수 TRACELAB_OUT 이 있으면 그쪽을 씁니다.

orders.payload 가 요청 본문을, orders.body_bytes 가 그 크기를, orders.cache_lookuporders.queue_depth 가 그때의 상태를 돌려줍니다. 결제 실패는 orders.PaymentError 입니다. 계측 프로그램은 /opt/otel-lab/bin/python 로 돌리세요 — 시스템 python3 에는 OpenTelemetry 가 없습니다.

넣으면 안 되는 값은 해시·범주·길이로 바꾼다

/root/tp-attrs/03_redact.py 를 만드세요. 2단계에 세 속성을 더합니다 — shop.customer.email_hash 는 이메일의 SHA-256 16진 문자열 앞 16자, shop.payment.card_brand 는 카드의 brand 값, shop.auth.token_len 은 인증 토큰의 길이(정수)입니다. 이메일·카드 번호(pan)·토큰·우편번호의 원문은 어느 속성에도 남기지 않습니다. 덤프 기본 경로는 /root/tp-attrs/03-redact.jsonl 입니다.

원문을 통째로 버리지 않고 바꿔 남기는 이유는, 그래도 답할 수 있는 질문이 있기 때문입니다 — 해시는 '같은 사용자에게 반복되는가', 범주는 '특정 카드사에서만 나는가', 길이는 '토큰이 잘려 들어왔는가'. 해시는 hashlib.sha256(문자열.encode("utf-8")).hexdigest() 로 만듭니다.

시점이 있는 사실은 이벤트로 옮긴다

/root/tp-attrs/04_events.py 를 만드세요. 3단계에 더해 (1) 캐시가 빗나갔으면 cache.miss 이벤트를 하나, (2) 결제 시도가 실패할 때마다 payment.attempt.failed 이벤트를 하나씩 남깁니다 — 이벤트 속성은 attempt(정수 시도 번호)와 reason(PaymentErrorkind)입니다. (3) 세 번 다 실패하면 예외 메시지를 shop.error.message 속성에 그대로 넣고 스팬 상태를 ERROR 로 바꿉니다. 덤프 기본 경로는 /root/tp-attrs/04-events.jsonl 입니다. 이 단계의 shop.error.message 는 5단계에서 다시 봅니다.

재시도를 두 번 했다는 것은 하나의 숫자라 속성이고, 첫 재시도가 언제 왜 일어났는가는 시각이 있는 기록이라 이벤트입니다. 둘은 경쟁하지 않습니다 — 세는 것은 속성, 일어난 순간은 이벤트. 상태는 span.set_status(Status(StatusCode.ERROR, "...")) 로 바꿉니다.

열쇠마다 고유값을 세어 예산표를 만든다

/root/tp-attrs/05_bulk.py 를 만들어 orders.ORDER_IDS 의 200건을 같은 계측으로 한 번씩 처리하세요(요청마다 스팬 하나). 덤프 기본 경로는 /root/tp-attrs/05-bulk.jsonl 입니다. 그다음 /root/tp-attrs/05-cardinality.tsv 에 덤프에 나온 속성 열쇠마다 한 줄씩 <열쇠><탭><고유값수><탭><판정>열쇠 이름 오름차순으로 적으세요. 판정은 shop.order.idshop.customer.email_hashfree, 그 밖에 고유값이 20 이하면 low, 20 을 넘으면 leak 입니다.

식별자는 요청마다 달라야 정상이고, 범주형 열쇠는 값이 몇 가지뿐이어야 정상입니다. 범주여야 할 자리에 원문이 새어 들어가면 열쇠 하나가 수천 가지 값을 갖게 됩니다 — 이 덤프에서 leak 이 하나 나오는데, 그게 4단계에서 일부러 넣은 그 속성입니다.

팀 규약을 파일로 쓴다

/root/tp-attrs/convention.tsv 를 만드세요. 한 줄이 열쇠 하나이고 탭으로 나눈 네 칸 <열쇠><탭><타입><탭><허용값><탭><고유값성격> 입니다. 타입은 string·int·bool·deny 중 하나, 허용값은 *(제한 없음)이나 | 로 이은 목록, 고유값 성격은 free 또는 low 입니다. 5단계에 나온 열쇠 열 개 중 shop.error.messagedeny 로 막고 대신 shop.error.kinddeclined|timeout 으로 새로 넣으세요. 8단계에서 쓸 shop.refund.amount(int·free)도 미리 넣고, 원문 금지 열쇠 shop.customer.email · shop.payment.card_pan · shop.auth.tokendeny 로 적습니다. deny 줄의 허용값과 성격 칸은 - 로 둡니다.

규약을 문장으로만 남기면 다음 사람이 읽지 않습니다. 기계가 읽을 수 있는 표로 만들어야 검사기를 붙일 수 있습니다. 금지 열쇠를 함께 적는 이유는 '넣지 말자' 는 합의가 코드 리뷰에서만 살아 있으면 결국 새기 때문입니다. 줄 수는 열다섯입니다.

규약을 어긴 스팬을 잡는 검사기를 만든다

/root/tp-attrs/lint_spans.py 를 만드세요. python3 lint_spans.py <규약파일> <덤프> 로 부르면 규약을 어긴 열쇠마다 VIOLATION <열쇠> <이유>열쇠 이름 오름차순으로 한 번씩만 찍고 종료 코드 1, 어긴 것이 없으면 OK 로 시작하는 한 줄과 종료 코드 0 을 냅니다. 봐야 할 것은 네 가지입니다 — 규약에 없는 열쇠, deny 로 막은 열쇠, 타입이 다른 값, 허용값 목록에 없는 값. 여기에 더해 low 로 선언한 열쇠의 고유값이 덤프 안에서 20 을 넘으면 그것도 위반입니다. 만든 뒤 /root/tp-attrs/05-bulk.jsonl/opt/app/tracelab/tp_attrs/noisy.jsonl 에 각각 돌리고, /root/tp-attrs/07-violations.tsv 에 두 줄 <덤프파일이름><탭><깨진 열쇠들을 쉼표로 이은 것> 을 그 순서로 적으세요.

위반을 스팬마다 찍으면 200줄이 나옵니다 — 열쇠 단위로 한 번만 모아서 찍으세요. noisy.jsonl 은 남의 팀 덤프라 고칠 수 없습니다. 이 단계에서 할 일은 '무엇이 어긋났는지 기계가 말해 주게 만드는 것' 이고, 고치는 일은 8단계에서 내 코드로 합니다.

규약대로 두 번째 핸들러를 계측하고 링크로 잇는다

/root/tp-attrs/08_refund.py 를 만들어 큐가 시킨 환불 ord-1027 를 처리하는 POST /refund SERVER 스팬 하나를 만드세요. 속성은 shop.order.id · shop.refund.amount · shop.queue.depth · shop.customer.email_hash · shop.payment.card_brand · shop.auth.token_len · shop.payment.retry_count 이고, 실패로 끝나면 shop.error.kind 를 더하고 상태를 ERROR 로 둡니다(원문 메시지는 넣지 않습니다). 실패한 시도마다 payment.attempt.failed 이벤트를 남기고, orders.job_context(주문번호) 가 주는 트레이스 좌표를 링크로 답니다(부모로 붙이지 않습니다). 덤프 기본 경로는 /root/tp-attrs/08-refund.jsonl 이고, 7단계의 검사기를 이 덤프에 돌리면 OK 가 나와야 합니다.

링크는 Link(SpanContext(trace_id=int(16진문자열, 16), span_id=int(16진문자열, 16), is_remote=True, trace_flags=TraceFlags(0x01))) 를 만들어 start_as_current_span(..., links=[link]) 로 넘깁니다. 큐 작업을 부모로 붙이면 몇 시간 전에 시작해 지금 끝나는 이상한 부모가 생깁니다 — 관계는 있지만 부모-자식은 아닌 자리가 바로 링크입니다. 검사기가 VIOLATION 을 내면 규약이 아니라 계측을 고치세요.