LabHub
배우기 러닝패스 코스

分散トレーシングが切れる場所

欠けたスパンの原因を一つずつ消していく

LabHub 에서 이어서 보기

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

목표

신고된 사건의 증거 두 장을 대조해 없는 요청을 숫자로 만들고, 공통점으로 범위를 좁힌 뒤, 네 가지 후보 원인을 직접 재현해 각각이 덤프에 남기는 흔적이 어떻게 다른지 확인합니다. 그 차이를 분류표로 정리하고 진단 스크립트로 굳혀, 원인이 다른 두 번째 사건에 돌려 다른 답이 나오는 것까지 봅니다.

왜 중요한가

스팬이 없는 이유는 여러 가지인데 화면에 보이는 증상은 하나다 — 없다. 그래서 짐작으로 고치기 시작하면 표본 비율을 올렸다가 익스포터를 만졌다가 하면서 며칠이 간다. 실제로 그렇게 이틀을 쓰고 나서야 로그와 덤프를 요청 식별자로 맞춰 보았고, 없어진 것이 전부 한 경로의 요청이라는 사실이 그 자리에서 드러났다. 진단이 먼저인 이유는 두 가지다. 몇 건이 없는지를 숫자로 만들어 두지 않으면 고친 뒤에도 나아졌는지 알 수 없고, 없는 것들의 공통점을 보지 않으면 전역 설정을 만져야 할 문제인지 한 경로의 코드를 봐야 할 문제인지 고를 수 없다. 네 가지 후보는 덤프에 서로 다른 흔적을 남기므로, 그 흔적을 한 번 직접 만들어 보면 다음부터는 덤프 한 장으로 갈린다. 결함이 든 배선을 찾아 고치는 일은 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-02e-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-03e-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자 이상을 적으세요 — ## 무엇이 없었나(두 사건에서 몇 건이 없었고 어디에 몰려 있었는지), ## 어떻게 갈랐나(어떤 흔적으로 후보를 배제했는지), ## 원인(두 사건의 판정 이름을 그대로 적을 것), ## 다음 사람에게(같은 신고가 또 오면 무엇부터 할 것인지). 본문 어딘가에 case1case2 가 모두 나와야 합니다.

참고

로그와 덤프를 대조해 없는 것의 목록을 만든다

사건 하나의 증거가 /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 에는 없는 요청의 식별자를 오름차순으로 한 줄에 하나씩 적습니다.

요청 식별자는 서버 스팬에만 붙어 있습니다. 자식 스팬(db.query)에는 없으니 덤프 전체에서 attributesrequest.id 를 모아 집합으로 만드세요. 로그 한 줄은 공백으로 나뉜 열쇠=값 들이라 split() 한 뒤 request_id= 로 시작하는 토막만 보면 됩니다. 덤프를 읽는 데는 otel 이 필요 없으므로 시스템 python3 로 돌리세요.

없는 것들의 공통점으로 조사 범위를 좁힌다

같은 사건에서 없는 요청이 어디에 몰려 있는지 봅니다. /root/tp-missing/02-shape.tsv 에 탭으로 나눈 네 칸을 적으세요. 먼저 경로마다 한 줄씩 route<탭><경로><탭><로그 건수><탭><없는 건수> 를 경로 이름 오름차순으로, 그다음 분(分)마다 한 줄씩 minute<탭><HH:MM><탭><로그 건수><탭><없는 건수> 를 시각 오름차순으로 적습니다. 마지막 줄은 verdict<탭><route 또는 minute><탭><가장 많이 빠진 값><탭><그 값에서 빠진 건수> 입니다 — 어느 축에 몰려 있는지를 고르는 줄입니다.

로그 한 줄에 route= 가 들어 있고 시각은 줄 맨 앞 2026-09-16T09:00:00Z 의 11번째 글자부터 다섯 글자가 HH:MM 입니다. 한 축은 값 하나에 전부 몰려 있고 다른 축은 고르게 퍼져 있을 것입니다 — 몰려 있는 쪽이 verdict 입니다. 없는 요청의 목록은 1단계에서 이미 만들었으니 그대로 쓰세요.

아무 흔적도 남기지 않는 두 원인은 분포로만 갈린다

덤프에 아무것도 남기지 않는 원인 둘을 직접 만들어 봅니다. /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 입니다.

표본 추출기는 스팬이 시작될 때 결정하므로 set_attribute 로 나중에 붙인 request.id 는 보지 못합니다 — start_as_current_span(이름, attributes={...}) 로 넘기세요. osos._exit 를 쓰려면 미리 import os 해 두어야 합니다. 두 덤프 모두 없는 요청은 루트도 자식도 한 줄도 없을 것입니다. 덤프를 다시 만들기 전에 파일을 지우세요 — 덤프는 이어 붙입니다.

끝내지 않은 스팬은 부모 없는 자식을 남긴다

/root/tp-missing/unfinished.py 를 만드세요(기본 덤프 경로 /root/tp-missing/04-unfinished.jsonl). 같은 여섯 건을 처리하되 e-02e-05 두 건은 루트 스팬을 tracer.start_span(...) 으로 만들고 끝내지 않습니다(end() 를 부르지 않습니다). 그 두 건도 자식 스팬은 정상으로 만들어야 하므로 webapp.work(tracer, req, context=trace.set_span_in_context(span)) 처럼 문맥을 넘겨 부르세요. 나머지 네 건은 3단계와 같은 방식입니다. 돌리고 나면 덤프에 스팬이 10줄 들어 있고, 그중 두 줄은 parent_id 가 덤프의 어떤 span_id 도 아닐 것입니다.

with 문은 블록을 벗어날 때 end() 를 대신 불러 주므로, 끝내지 않은 스팬을 만들려면 with 를 쓰지 말아야 합니다. tracer.start_span 은 만들기만 하고 현재 스팬으로 세우지도 않습니다 — 그래서 자식이 부모를 찾게 하려면 문맥을 손으로 넘겨야 합니다. 끝나지 않은 스팬은 익스포터에 넘어가지 않으므로 덤프에 아예 나타나지 않습니다.

부모 문맥이 끊기면 없는 요청 없이 트레이스가 갈라진다

/root/tp-missing/broken_parent.py 를 만드세요(기본 덤프 경로 /root/tp-missing/05-split.jsonl). 여섯 건을 모두 정상으로 처리하되 e-03e-06 두 건만 자식을 webapp.work(tracer, req, context=Context()) 로 불러 빈 문맥에 붙입니다(from opentelemetry.context import Context). 돌리고 나면 스팬은 12줄이고 없는 요청은 한 건도 없는데, request.id 가 붙은 스팬이 하나도 없는 트레이스가 두 개 생깁니다.

Context() 에는 현재 스팬이 없으므로 그 안에서 시작한 스팬은 부모를 찾지 못하고 새 트레이스의 루트가 됩니다. 이 원인이 앞의 셋과 다른 점은 대조표에 아무것도 안 잡힌다는 것입니다 — 그래서 세어야 하는 것이 없는 요청이 아니라 요청 식별자가 없는 트레이스입니다. python3 /opt/lab/checks/_tplib.py summary <덤프> 로 트레이스가 몇 개인지 볼 수 있습니다.

원인마다 다른 흔적을 분류표로 굳힌다

앞의 세 단계에서 만든 네 개의 덤프를 보고 /root/tp-missing/06-fingerprints.tsv 에 탭으로 나눈 네 칸 네 줄을 적으세요. 첫 칸은 원인 이름으로 순서대로 sampled-out·unfinished·early-exit·broken-parent 입니다. 둘째 칸은 없어진 요청의 루트 스팬이 덤프에 있는가로 none 또는 present. 셋째 칸은 자식 스팬이 어떤 모양인가로 none(없다)·orphan(있는데 가리키는 부모가 덤프에 없다)·detached(있는데 다른 트레이스의 루트가 되었다) 가운데 하나. 넷째 칸은 없는 요청의 분포로 scattered·tail·none(없는 요청이 아예 없다) 가운데 하나입니다.

네 줄 가운데 셋은 3·4·5단계에서 여러분이 직접 만든 덤프가 그대로 답을 보여 줍니다. 헷갈리는 것은 첫 줄과 셋째 줄인데, 그 둘은 덤프만 보면 똑같고 넷째 칸에서만 갈립니다. 채점기는 여러분이 만든 덤프를 다시 읽어 표의 각 줄과 맞는지 봅니다 — 외워 적는 표가 아니라 여러분 덤프의 요약입니다.

같은 판정을 스크립트로 굳혀 사건에 돌린다

/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 에 저장하세요.

순서가 중요합니다 — 끝내지 않은 스팬은 '요청 식별자가 없는 트레이스' 도 함께 만들어 내므로 (1) 을 (2) 보다 먼저 보지 않으면 두 원인이 뒤바뀝니다. missing 은 어느 원인이든 같은 방법으로 셉니다(로그의 요청 가운데 덤프에 request.id 가 없는 것). 채점기는 여러분의 스크립트를 /opt/app/tracelab/tp_missing 아래의 다른 사건 폴더에도 돌려 보므로 파일 이름이나 특정 식별자로 판정하면 안 됩니다.

원인이 다른 두 번째 사건에 돌리고 조사 기록을 남긴다

두 번째 사건 /opt/app/tracelab/tp_missing/case2/ 에 같은 스크립트를 돌려 출력을 /root/tp-missing/08-verdict.txt 에 저장하세요 — 첫 사건과 다른 답이 나와야 합니다. 그리고 /root/tp-missing/08-report.md 에 다음 사람이 읽을 조사 기록을 남깁니다. 제목 네 개를 이 순서로 두고 각 제목 아래에 60자 이상을 적으세요 — ## 무엇이 없었나(두 사건에서 몇 건이 없었고 어디에 몰려 있었는지), ## 어떻게 갈랐나(어떤 흔적으로 후보를 배제했는지), ## 원인(두 사건의 판정 이름을 그대로 적을 것), ## 다음 사람에게(같은 신고가 또 오면 무엇부터 할 것인지). 본문 어딘가에 case1case2 가 모두 나와야 합니다.

두 사건의 덤프는 겉보기에 비슷합니다 — 둘 다 16건이 없습니다. 갈리는 것은 그 16건이 로그의 어디에 있느냐와, 덤프에 부모 없는 자식이 남았느냐입니다. 기록을 쓸 때는 결론만 적지 말고 무엇을 보고 무엇을 지웠는지 를 적으세요. 다음 사람이 필요한 것은 답이 아니라 순서입니다.