분산 트레이싱이 끊기는 자리 · 큐를 건너는 추적 · 실습
생산자와 소비자를 부모-자식으로 이었더니 트레이스가 끝나지 않는다
목표
파일 한 개로 된 작은 큐를 놓고, 생산과 소비를 부모-자식으로 이었을 때 생기는 문제를 덤프로 본 뒤 링크로 바꿉니다. 배치 소비·큐 대기 시간·스팬 종류·링크 속성을 차례로 붙이고, 팬아웃까지 적용한 다음 링크를 따라가 한 주문의 여정을 트레이스 너머로 이어 붙입니다.
왜 중요한가
부모-자식은 '부모가 자식을 기다린다' 는 뜻이다. 그런데 생산자는 소비가 끝나기를 기다리지 않는다. 그 둘을 부모-자식으로 이으면 사용자에게 이미 응답이 나갔는데도 루트 스팬이 닫히지 못하고, 큐가 밀린 날에는 트레이스 하나가 몇 분씩 열려 있는다. 팬아웃이 섞이면 주문 하나가 수천 스팬으로 자라 백엔드가 그 트레이스만 특별 취급하기 시작한다. 링크는 바로 이 자리를 위해 있다 — 인과는 남기되 기다리지는 않는다는 관계다. 소비 쪽을 새 트레이스의 루트로 세우고 생산 스팬을 링크로 가리키면 트레이스 하나하나는 작게 유지되고, 전체 여정은 링크를 따라가며 다시 이어 붙일 수 있다. 헤더를 읽어 끊긴 체인을 잇는 일이나 프로세스 안에서 문맥을 넘기는 일과는 다른 판단이다.
단계
1. /root/tp-links/naive.py 를 만드세요. 덤프 경로는 TRACELAB_OUT 에서 먼저 읽고 없으면 /root/tp-links/01-naive.jsonl 을 씁니다. 큐 파일은 덤프와 같은 디렉터리의 01-q.jsonl 로 잡고 시작할 때 비웁니다. order.submit 스팬 하나 안에서 bus.publish 로 메시지 3건(m-1~m-3)을 넣고 time.sleep(0.25) 로 큐에서 기다린 뒤, 같은 스팬 안에서 bus.poll 로 꺼내 메시지마다 자식 스팬 order.handle 을 만들어 bus.handle 을 부릅니다. 그다음 /root/tp-links/01-problem.txt 에 네 줄을 적으세요 — traces= 뒤에 덤프의 트레이스 수, root_span= 뒤에 루트 스팬 이름, wait_ms= 뒤에 루트 스팬 길이에서 자식들이 덮은 구간을 뺀 값을 정수로, problem= 뒤에 이 모양의 문제가 무엇인지 40자 이상으로.
2. /root/tp-links/linked.py 를 만드세요(기본 덤프 경로 /root/tp-links/02-linked.jsonl, 큐 파일 02-q.jsonl). order.submit 안에서 메시지마다 스팬 order.publish 를 만들고, 그 스팬의 trace_id 와 span_id 를 16진 문자열로 메시지에 실어 보냅니다. 기다린 뒤 꺼내는 쪽은 order.submit 밖에서 돌려 스팬 order.process 가 트레이스의 루트가 되게 하고, 메시지에 실려 온 아이디로 Link 를 만들어 links= 로 겁니다. 두 스팬 모두 messaging.message.id 속성에 메시지 아이디를 적습니다.
3. /root/tp-links/batch.py 를 만드세요(기본 덤프 경로 /root/tp-links/03-batch.jsonl, 큐 파일 03-q.jsonl). 이번에는 메시지를 5건(m-1~m-5) 넣고, 기다린 뒤 bus.poll(Q, 5) 로 한 번에 꺼냅니다. 꺼낸 배치 전체를 스팬 하나 order.process.batch 로 처리하고, 그 스팬에 꺼낸 메시지 수만큼 링크를 걸고 정수 속성 messaging.batch.message_count 에 건수를 적습니다. 이 스팬도 트레이스의 루트여야 합니다.
4. /root/tp-links/wait.py 를 만드세요(기본 덤프 경로 /root/tp-links/04-wait.jsonl, 큐 파일 04-q.jsonl). 2단계의 모양으로 돌아가되 메시지에 produced_at_ns 칸을 더해 bus.now_ns() 값을 실어 보내고, 소비 스팬 order.process 에 그 시각과 지금의 차이를 밀리초로 계산해 속성 messaging.queue.wait_ms 로 적습니다. 그리고 /root/tp-links/04-wait.tsv 에 메시지마다 한 줄씩 <메시지 아이디><탭><기다린 밀리초 소수 첫째 자리> 를 적으세요(세 줄).
5. /root/tp-links/kinds.py 를 만드세요(기본 덤프 경로 /root/tp-links/05-kinds.jsonl, 큐 파일 05-q.jsonl). 4단계의 흐름에 스팬 종류를 붙입니다 — order.publish 는 SpanKind.PRODUCER, order.process 는 SpanKind.CONSUMER, 감싸는 order.submit 은 그대로 둡니다. 그리고 두 메시징 스팬에 messaging.system(labbus)·messaging.destination.name(orders)·messaging.operation.type(생산은 send, 소비는 process)·messaging.operation.name·messaging.message.id 를 답니다. 그다음 /root/tp-links/05-kinds.tsv 에 세 줄을 적으세요 — 각 줄은 <스팬 이름><탭><종류><탭><operation.type> 이고 순서는 order.submit, order.publish, order.process 이며 order.submit 의 셋째 칸은 - 입니다.
6. /root/tp-links/linkattrs.py 를 만드세요(기본 덤프 경로 /root/tp-links/06-linkattrs.jsonl, 큐 파일 06-q.jsonl). 5단계와 같은 흐름이되 Link 를 만들 때 두 번째 인자로 속성을 함께 넘깁니다 — link.relation 에 queue.message, messaging.message.id 에 그 메시지의 아이디, messaging.destination.name 에 orders 를 적습니다. 덤프의 links 칸마다 이 세 속성이 들어 있어야 합니다.
7. /root/tp-links/fanout.py 를 만드세요(기본 덤프 경로 /root/tp-links/07-fanout.jsonl, 큐 파일 07-q.jsonl). 6단계의 규칙을 그대로 쓰되 흐름을 늘립니다 — order.submit 이 메시지 3건을 만들고, 소비 쪽에서 주문 A-1002 를 처리하는 order.process 스팬 안에서 두 건을 더 만듭니다(스팬 이름 invoice.publish 와 email.publish, 메시지 아이디 inv-2 와 eml-2). 잠시 기다린 뒤 그 두 건을 꺼내 invoice.process 와 email.process 로 각각 처리하며, 이들도 새 트레이스의 루트이고 링크로 앞의 생산 스팬을 가리킵니다. 덤프에는 트레이스가 6개 들어 있어야 합니다.
8. /root/tp-links/journey.py 를 만드세요. python3 journey.py <덤프> <메시지아이디> 로 돌리면 그 메시지를 만든 order.publish 스팬에서 출발해, 링크로 이어진 스팬을 홉 단위로 따라가며 한 줄씩 <홉><탭><trace_id><탭><스팬이름> 을 찍습니다. 출발 스팬이 홉 0 이고, 다음 홉은 앞 홉의 스팬 또는 그 스팬의 자손을 링크로 가리키는 스팬들이며 같은 홉 안에서는 스팬 이름 오름차순으로 찍습니다. 더 따라갈 곳이 없으면 멈춥니다. 이 프로그램을 /root/tp-links/07-fanout.jsonl 과 m-2 로 돌린 출력을 /root/tp-links/08-journey.tsv 에 저장하세요.
참고
- 작업 디렉터리는
/root/tp-links입니다. 없으면 먼저 만드세요. - 계측 프로그램은 반드시
/opt/otel-lab/bin/python <파일>로 돌립니다. 시스템python3에는 OpenTelemetry SDK 가 없습니다. 덤프만 읽는 프로그램은 시스템python3로 돌리세요. - 재료는
/opt/app/tracelab/tp_links/bus.py(파일 한 개로 된 큐)이고 공용 배선은/opt/app/tracelab/dump.py, 덤프 읽기 도우미는/opt/lab/checks/_tplib.py입니다. 큐 파일은 단계마다 따로 두고 시작할 때 비웁니다. - 흔한 실수: 소비 반복문을
order.submit블록 안에 둔 채 링크만 거는 것. 그러면 링크도 있고 부모도 있어 트레이스가 여전히 하나로 붙습니다. 덤프의parent_id가 비어 있는지로 확인하세요. - [Traces (OpenTelemetry Concepts)](https://opentelemetry.io/docs/concepts/signals/traces/) · [Tracing API 명세](https://opentelemetry.io/docs/specs/otel/trace/api/) · [메시징 스팬 시맨틱 컨벤션](https://opentelemetry.io/docs/specs/semconv/messaging/messaging-spans/) · [메시징 속성 레지스트리](https://opentelemetry.io/docs/specs/semconv/registry/attributes/messaging/) · [Python 계측 문서](https://opentelemetry.io/docs/languages/python/instrumentation/)
단계 8개
- 생산과 소비를 부모-자식으로 이으면 무슨 일이 나는가
- 소비를 새 트레이스의 루트로 세우고 링크로 잇는다
- 배치 소비는 링크 여러 개인 스팬 하나로
- 큐에서 기다린 시간을 어떻게 잴 것인가
- PRODUCER 와 CONSUMER 는 언제 쓰는가
- 링크에 왜 이어져 있는지를 적는다
- 하나가 여럿을 만드는 팬아웃까지 적용한다
- 링크를 따라가 여정을 다시 이어 붙인다