분산 트레이싱이 끊기는 자리 · 스팬이 안 보인다 · 실습
없는 스팬의 원인을 하나씩 배제한다
목표
신고된 사건의 증거 두 장을 대조해 없는 요청을 숫자로 만들고, 공통점으로 범위를 좁힌 뒤, 네 가지 후보 원인을 직접 재현해 각각이 덤프에 남기는 흔적이 어떻게 다른지 확인합니다. 그 차이를 분류표로 정리하고 진단 스크립트로 굳혀, 원인이 다른 두 번째 사건에 돌려 다른 답이 나오는 것까지 봅니다.
왜 중요한가
스팬이 없는 이유는 여러 가지인데 화면에 보이는 증상은 하나다 — 없다. 그래서 짐작으로 고치기 시작하면 표본 비율을 올렸다가 익스포터를 만졌다가 하면서 며칠이 간다. 실제로 그렇게 이틀을 쓰고 나서야 로그와 덤프를 요청 식별자로 맞춰 보았고, 없어진 것이 전부 한 경로의 요청이라는 사실이 그 자리에서 드러났다. 진단이 먼저인 이유는 두 가지다. 몇 건이 없는지를 숫자로 만들어 두지 않으면 고친 뒤에도 나아졌는지 알 수 없고, 없는 것들의 공통점을 보지 않으면 전역 설정을 만져야 할 문제인지 한 경로의 코드를 봐야 할 문제인지 고를 수 없다. 네 가지 후보는 덤프에 서로 다른 흔적을 남기므로, 그 흔적을 한 번 직접 만들어 보면 다음부터는 덤프 한 장으로 갈린다. 결함이 든 배선을 찾아 고치는 일은 SDK 수명주기 모듈의 몫이고, 여기서 만드는 것은 분류표와 진단 스크립트다.
단계
1. 사건 하나의 증거가 /opt/app/tracelab/tp_missing/case1/ 에 있습니다 — 요청 기록 app.log 와 스팬 덤프 spans.jsonl 입니다. 로그 한 줄의 request_id= 값과 덤프의 스팬 속성 request.id 를 맞춰, 로그에는 있는데 덤프에는 없는 요청을 찾으세요. 결과를 두 파일로 남깁니다. /root/tp-missing/01-missing.txt 에는 세 줄 — logged= 뒤에 로그의 요청 수, traced= 뒤에 덤프에서 찾은 요청 수, missing= 뒤에 없는 요청 수. /root/tp-missing/01-missing-ids.txt 에는 없는 요청의 식별자를 오름차순으로 한 줄에 하나씩 적습니다.
2. 같은 사건에서 없는 요청이 어디에 몰려 있는지 봅니다. /root/tp-missing/02-shape.tsv 에 탭으로 나눈 네 칸을 적으세요. 먼저 경로마다 한 줄씩 route<탭><경로><탭><로그 건수><탭><없는 건수> 를 경로 이름 오름차순으로, 그다음 분(分)마다 한 줄씩 minute<탭><HH:MM><탭><로그 건수><탭><없는 건수> 를 시각 오름차순으로 적습니다. 마지막 줄은 verdict<탭><route 또는 minute><탭><가장 많이 빠진 값><탭><그 값에서 빠진 건수> 입니다 — 어느 축에 몰려 있는지를 고르는 줄입니다.
3. 덤프에 아무것도 남기지 않는 원인 둘을 직접 만들어 봅니다. /root/tp-missing/sampling.py 는 재료 tracelab.tp_missing.samplers.drop_requests(["e-02", "e-05"]) 를 표본 추출기로 써서 webapp.REQUESTS 여섯 건을 처리합니다(기본 덤프 경로 /root/tp-missing/03-sampling.jsonl). /root/tp-missing/early_exit.py 는 추출기 없이 같은 여섯 건을 처리하되 e-05 차례가 되면 os._exit(0) 으로 프로세스를 끝냅니다(기본 덤프 경로 /root/tp-missing/03-exit.jsonl). 둘 다 루트 스팬 이름은 GET <경로> 이고 속성 request.id 를 스팬을 시작할 때 넘기며, 안에서 webapp.work(tracer, req) 를 부릅니다. 그다음 /root/tp-missing/03-nothing.tsv 에 탭으로 나눈 세 칸 두 줄을 적으세요 — 첫 줄은 sampling, 둘째 줄은 early-exit 이고, 둘째 칸은 그 덤프에 없는 요청의 식별자를 쉼표로 이은 것, 셋째 칸은 그 없는 것들이 여섯 건의 끝에서부터 잇달아 있으면 tail, 아니면 scattered 입니다.
4. /root/tp-missing/unfinished.py 를 만드세요(기본 덤프 경로 /root/tp-missing/04-unfinished.jsonl). 같은 여섯 건을 처리하되 e-02 와 e-05 두 건은 루트 스팬을 tracer.start_span(...) 으로 만들고 끝내지 않습니다(end() 를 부르지 않습니다). 그 두 건도 자식 스팬은 정상으로 만들어야 하므로 webapp.work(tracer, req, context=trace.set_span_in_context(span)) 처럼 문맥을 넘겨 부르세요. 나머지 네 건은 3단계와 같은 방식입니다. 돌리고 나면 덤프에 스팬이 10줄 들어 있고, 그중 두 줄은 parent_id 가 덤프의 어떤 span_id 도 아닐 것입니다.
5. /root/tp-missing/broken_parent.py 를 만드세요(기본 덤프 경로 /root/tp-missing/05-split.jsonl). 여섯 건을 모두 정상으로 처리하되 e-03 과 e-06 두 건만 자식을 webapp.work(tracer, req, context=Context()) 로 불러 빈 문맥에 붙입니다(from opentelemetry.context import Context). 돌리고 나면 스팬은 12줄이고 없는 요청은 한 건도 없는데, request.id 가 붙은 스팬이 하나도 없는 트레이스가 두 개 생깁니다.
6. 앞의 세 단계에서 만든 네 개의 덤프를 보고 /root/tp-missing/06-fingerprints.tsv 에 탭으로 나눈 네 칸 네 줄을 적으세요. 첫 칸은 원인 이름으로 순서대로 sampled-out·unfinished·early-exit·broken-parent 입니다. 둘째 칸은 없어진 요청의 루트 스팬이 덤프에 있는가로 none 또는 present. 셋째 칸은 자식 스팬이 어떤 모양인가로 none(없다)·orphan(있는데 가리키는 부모가 덤프에 없다)·detached(있는데 다른 트레이스의 루트가 되었다) 가운데 하나. 넷째 칸은 없는 요청의 분포로 scattered·tail·none(없는 요청이 아예 없다) 가운데 하나입니다.
7. /root/tp-missing/classify.py 를 만드세요. python3 classify.py <app.log> <spans.jsonl> 로 돌리면 두 줄을 찍습니다 — verdict=<원인 이름> 과 missing=<없는 요청 수>. 규칙은 이 순서로 봅니다. (1) parent_id 가 덤프의 어떤 span_id 도 아닌 스팬이 있으면 unfinished. (2) request.id 속성을 가진 스팬이 하나도 없는 trace_id 가 있으면 broken-parent. (3) 없는 요청이 하나도 없으면 ok. (4) 없는 요청이 로그의 마지막 줄부터 잇달아 있으면 early-exit. (5) 그 밖은 sampled-out. 만든 스크립트를 /opt/app/tracelab/tp_missing/case1/ 에 돌린 출력을 그대로 /root/tp-missing/07-verdict.txt 에 저장하세요.
8. 두 번째 사건 /opt/app/tracelab/tp_missing/case2/ 에 같은 스크립트를 돌려 출력을 /root/tp-missing/08-verdict.txt 에 저장하세요 — 첫 사건과 다른 답이 나와야 합니다. 그리고 /root/tp-missing/08-report.md 에 다음 사람이 읽을 조사 기록을 남깁니다. 제목 네 개를 이 순서로 두고 각 제목 아래에 60자 이상을 적으세요 — ## 무엇이 없었나(두 사건에서 몇 건이 없었고 어디에 몰려 있었는지), ## 어떻게 갈랐나(어떤 흔적으로 후보를 배제했는지), ## 원인(두 사건의 판정 이름을 그대로 적을 것), ## 다음 사람에게(같은 신고가 또 오면 무엇부터 할 것인지). 본문 어딘가에 case1 과 case2 가 모두 나와야 합니다.
참고
- 작업 디렉터리는
/root/tp-missing입니다. 없으면 먼저 만드세요. - 계측 프로그램은 반드시
/opt/otel-lab/bin/python <파일>로 돌립니다. 시스템python3에는 OpenTelemetry SDK 가 없습니다. 반대로 로그와 덤프만 읽는 프로그램은 시스템python3로 돌리세요. - 재료는 사건 폴더
/opt/app/tracelab/tp_missing/case1~/opt/app/tracelab/tp_missing/case5(각각app.log와spans.jsonl), 실험용 요청과 자식 스팬 도우미/opt/app/tracelab/tp_missing/webapp.py, 실험용 표본 추출기/opt/app/tracelab/tp_missing/samplers.py입니다. 사건을 만든 생성기는/opt/app/tracelab/tp_missing/make_cases.py이고, 공용 배선은/opt/app/tracelab/dump.py, 덤프 읽기 도우미는/opt/lab/checks/_tplib.py입니다. - 흔한 실수: 덤프 파일을 지우지 않고 프로그램을 두 번 돌리는 것. 덤프는 이어 붙이므로 스팬이 두 배가 됩니다.
- 흔한 실수: 표본 추출 판정에 쓰이는 속성을
set_attribute로 나중에 붙이는 것. 표본 결정은 스팬이 시작될 때 나므로 그때 넘긴 속성만 추출기가 봅니다. - [표본 추출 개념](https://opentelemetry.io/docs/concepts/sampling/) · [Trace SDK 명세(ForceFlush·Shutdown·ShouldSample)](https://opentelemetry.io/docs/specs/otel/trace/sdk/) · [W3C Trace Context](https://www.w3.org/TR/trace-context/) · [Traces 개념](https://opentelemetry.io/docs/concepts/signals/traces/) · [Python 계측 문서](https://opentelemetry.io/docs/languages/python/instrumentation/)
단계 8개
- 로그와 덤프를 대조해 없는 것의 목록을 만든다
- 없는 것들의 공통점으로 조사 범위를 좁힌다
- 아무 흔적도 남기지 않는 두 원인은 분포로만 갈린다
- 끝내지 않은 스팬은 부모 없는 자식을 남긴다
- 부모 문맥이 끊기면 없는 요청 없이 트레이스가 갈라진다
- 원인마다 다른 흔적을 분류표로 굳힌다
- 같은 판정을 스크립트로 굳혀 사건에 돌린다
- 원인이 다른 두 번째 사건에 돌리고 조사 기록을 남긴다