분산 트레이싱이 끊기는 자리 · 스팬 경계 — 어디서 끊을 것인가 · 실습
스팬이 하나면 아무것도 모르고, 서른 개면 아무도 안 본다
목표
계측이 한 줄도 없는 주문 처리 코드에 스팬을 직접 넣으면서, 경계를 어디에 그을지 숫자로 정하는 법을 익힙니다. 계측 공백을 재고, 반복을 속성으로 접고, 그 판단을 규칙 파일로 적어 두 번째 핸들러에 그대로 적용합니다.
왜 중요한가
스팬을 하나만 두면 트레이스는 '211밀리초 걸렸다' 말고 아무 말도 하지 않는다. 반복문마다 스팬을 만들면 이번에는 같은 모양 막대가 서른 개 이어져 아무도 끝까지 보지 않는다. 두 실패는 같은 질문을 건너뛴 결과다 — 나중에 이 트레이스로 무엇을 물을 것인가. 그 질문에 답이 되는 자리를 찾는 도구가 계측 공백이다. 부모 구간 중 어느 자식도 덮지 않은 시간이 곧 '아직 모르는 시간' 이고, 그 자리가 다음 스팬을 그을 곳이다. 반대로 반복은 스팬이 아니라 횟수·총시간·최댓값으로 접어야 트레이스가 읽을 수 있는 크기로 남는다. 마지막으로 이 판단을 사람의 취향으로 두면 리뷰마다 다시 싸우게 되므로, 요청당 스팬 상한과 허용 공백을 파일로 적어 기계가 읽게 한다.
단계
1. /root/tp-boundary/01_root.py 를 만드세요. /opt/app/tracelab/dump.py 의 provider 로 서비스 이름 shop-api 짜리 프로바이더를 만들고, 덤프 경로는 환경변수 TRACELAB_OUT 에서 먼저 읽되 없으면 /root/tp-boundary/01-root.jsonl 을 씁니다. POST /checkout 이라는 이름의 SERVER 스팬 하나 안에서 tracelab.tp_boundary.shop 의 validate → load_cart → price_of(전 상품) → charge → write_receipt 를 차례로 부르고, 끝에 flush() 를 부릅니다. 그런 다음 /opt/otel-lab/bin/python 로 돌려 덤프를 만들고, /root/tp-boundary/01-root.txt 에 spans= 와 root_ms= 두 줄을 적으세요(덤프에서 읽은 값 그대로).
2. /root/tp-boundary/02_split.py 를 만드세요. 1단계와 같되, 데이터베이스 왕복 load_cart 를 cart.load 스팬으로, 밖으로 나가는 charge 를 payment.charge 스팬으로 감쌉니다(자동 계측이 대신 만들어 주는 자리가 이 둘입니다). 덤프 기본 경로는 /root/tp-boundary/02-split.jsonl 입니다. 돌린 뒤 /root/tp-boundary/02-gap.txt 에 네 줄 root_ms= · covered_ms= · gap_ms= · gap_ratio= 을 적으세요. covered_ms 는 루트의 자식 구간을 합집합으로 더한 길이이고, gap_ratio 는 (루트 − 덮인 시간) ÷ 루트 를 소수 넷째 자리까지 적습니다.
3. /root/tp-boundary/03_close.py 를 만드세요. 2단계에서 남은 공백을 없애도록 validate 는 order.validate, 상품 값 조회 반복 전체는 price.lookup, write_receipt 는 receipt.write 스팬으로 감쌉니다. 스팬 이름은 이 다섯 개(order.validate·cart.load·price.lookup·payment.charge·receipt.write)와 루트 POST /checkout 로 정확히 맞추세요. 덤프 기본 경로는 /root/tp-boundary/03-close.jsonl 입니다. 돌린 뒤 /root/tp-boundary/03-gap.txt 에 2단계와 같은 네 줄을 적고, gap_ratio 가 0.05 미만이 되게 하세요.
4. /root/tp-boundary/04_peritem.py 를 만드세요. 3단계와 같되 price.lookup 안에서 상품 하나를 조회할 때마다 price.item 스팬을 하나씩 만듭니다. 덤프 기본 경로는 /root/tp-boundary/04-peritem.jsonl 입니다. 돌린 뒤 /root/tp-boundary/04-count.txt 에 세 줄 spans_per_request= · item_spans= · spans_per_1000_requests= 를 적으세요. 마지막 줄은 요청당 스팬 수에 1000 을 곱한 정수입니다.
5. /root/tp-boundary/05_fold.py 를 만드세요. price.item 스팬을 없애고, 대신 price.lookup 스팬에 세 속성 price.lookup.count(조회 횟수) · price.lookup.total_ms(합계 밀리초) · price.lookup.max_ms(가장 오래 걸린 한 건의 밀리초)를 붙입니다. 그리고 가장 오래 걸린 상품을 price.lookup.slowest 라는 이벤트로 남기되 이벤트 속성에 sku 를 넣으세요. 덤프 기본 경로는 /root/tp-boundary/05-fold.jsonl 이고, 전체 스팬 수는 8개를 넘지 않아야 합니다.
6. /root/tp-boundary/06_kind.py 를 만드세요. 5단계와 같되 모든 스팬에 SpanKind 를 명시합니다 — 들어온 요청은 SERVER, 프로세스 밖으로 나가는 호출(cart.load·payment.charge·receipt.write)은 CLIENT, 프로세스 안에서 나눈 구간(order.validate·price.lookup)은 INTERNAL 입니다. 덤프 기본 경로는 /root/tp-boundary/06-kind.jsonl 이고, 돌린 뒤 /root/tp-boundary/06-kinds.tsv 에 스팬마다 한 줄씩 <스팬이름><탭><kind> 를 시작 시각 순서로 적으세요(머리글 없이 여섯 줄).
7. /root/tp-boundary/budget.txt 에 세 줄을 적으세요 — max_spans_per_request=8, max_gap_ratio=0.10, root_kind=SERVER. 그리고 /root/tp-boundary/check_budget.py 를 만드세요. python3 check_budget.py <규칙파일> <덤프> 로 부르면 규칙을 어긴 것마다 VIOLATION <규칙키> <지금 값> 을 한 줄씩 찍고 종료 코드 1, 다 지켰으면 OK 로 시작하는 한 줄과 종료 코드 0 을 냅니다. 마지막으로 /root/tp-boundary/07-verdict.tsv 에 세 줄을 적으세요 — 각 줄은 <덤프파일이름><탭><pass|fail><탭><깨진 규칙키 또는 -> 이고, 02-split.jsonl · 04-peritem.jsonl · 06-kind.jsonl 을 이 순서로 검사한 결과입니다.
8. /root/tp-boundary/08_search.py 를 만들어 GET /search 핸들러를 처음부터 계측하세요. shop.parse_query → shop.search_index → 결과마다 shop.hydrate → shop.render 순서로 부르고, 스팬은 루트 GET /search(SERVER) 아래 query.parse(INTERNAL) · index.search(CLIENT) · result.hydrate(INTERNAL) · response.render(INTERNAL) 네 개입니다. 반복은 접어서 result.hydrate 에 result.hydrate.count · result.hydrate.total_ms · result.hydrate.max_ms 를 속성으로 답니다. 덤프 기본 경로는 /root/tp-boundary/08-search.jsonl 이고, 7단계의 검사기를 이 덤프에 돌렸을 때 OK 가 나와야 합니다.
참고
- 작업 디렉터리는
/root/tp-boundary입니다. 없으면 먼저 만드세요. - 계측 프로그램은 반드시
/opt/otel-lab/bin/python로 돌립니다. 시스템python3에는 OpenTelemetry 가 없습니다. 덤프를 읽고 세는 작은 스크립트는 시스템python3로 충분합니다. - 공용 배선은
/opt/app/tracelab/dump.py(provider·flush), 재료는/opt/app/tracelab/tp_boundary/shop.py(계측이 없는 주문 처리 코드)와/opt/app/tracelab/tp_boundary/gapstat.py(덤프 읽기·합집합 계산·tree출력)입니다. - 흔한 실수: 덤프 파일을 지우지 않고 프로그램을 다시 돌리는 것. 덤프는 이어 쓰기라 스팬이 쌓이고, 트레이스가 두 개가 됩니다.
- 흔한 실수: 공백을 '느린 구간' 으로 읽는 것. 공백은 아직 스팬이 없는 구간이고, 자기 시간·임계 경로와는 다른 질문입니다.
- 이 파드에는 컬렉터도 트레이스 화면도 없습니다. 판정은 덤프의 구조로만 하고, 밀리초 절대값이 아니라 비율로 봅니다.
- [OpenTelemetry — Traces](https://opentelemetry.io/docs/concepts/signals/traces/) · [Tracing API 명세](https://opentelemetry.io/docs/specs/otel/trace/api/) · [OpenTelemetry Python 계측](https://opentelemetry.io/docs/languages/python/instrumentation/) · [라이브러리 계측 지침](https://opentelemetry.io/docs/concepts/instrumentation/libraries/) · [W3C Trace Context](https://www.w3.org/TR/trace-context/)
단계 8개
- 스팬 하나짜리 트레이스로 시작한다
- 자동 계측이 주는 두 스팬만 넣고 공백을 잰다
- 공백이 큰 구간을 갈라 5% 아래로 내린다
- 반복마다 스팬을 만들면 몇 개가 되는지 세어 본다
- 반복을 스팬 대신 속성과 이벤트로 접는다
- SpanKind 로 경계를 표시한다
- 경계 규칙을 파일로 적고 검사기를 만든다
- 같은 규칙으로 두 번째 핸들러를 계측한다