OTCA — 오픈텔레메트리 인증 어소시에이트 · 시맨틱 컨벤션과 컨텍스트 전파 · 이론
전파가 끊기는 네 지점과 각각의 처방
한 줄 요약
트레이스라는 트리를 만드는 유일한 메커니즘은 컨텍스트 전파입니다. 호출하는 쪽이 W3C traceparent 헤더에 현재 트레이스 ID 와 스팬 ID 를 넣고, 받는 쪽이 그것을 읽어 자기 스팬의 부모로 삼습니다. 이 사슬이 끊기는 곳은 거의 정해져 있습니다 — 스레드풀, 메시지 큐, 백그라운드 작업, 헤더를 지우는 중간 계층.
왜 이게 필요했나
프로덕션 트레이스를 하나 열었더니 요청이 통과한 서비스는 여섯 개인데 SERVER 스팬이 두 개뿐입니다. 이 상태에서 수동 스팬을 추가하는 것은 아무 의미가 없습니다. 이번 주에 할 일은 나머지 네 곳의 전파를 살리는 것입니다.
어떻게 동작하나
traceparent 해부
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 -- -------------------------------- ---------------- -- | | | | | | | +-- flags: 01 이면 샘플링됨, 00 이면 저장 안 됨 | | +-- parent-id: 16자리 16진수(8바이트). 호출 단계마다 바뀐다 | +-- trace-id: 32자리 16진수(16바이트). 요청 전체에서 동일하다 +-- version: 현재 00tracestate 는 벤더별 부가 정보를 실어 나르는 동반 헤더이고, baggage 는 사용자 정의 키-값을 서비스 경계 너머로 나르는 별도 규약입니다. baggage 는 스팬 속성이 아니므로 자동으로 저장되지 않습니다.
끊김 1 — 스레드풀과 executor. 컨텍스트는 스레드 로컬(또는 asyncio contextvar)에 있으므로 작업을 다른 스레드로 넘기면 따라가지 않습니다. 처방은 현재 컨텍스트를 캡처해 워커 안에서 다시 활성화하는 것입니다. Java 는 Context.current().wrap(runnable), Go 는 context.Context 를 goroutine 인자로 넘기는 것, Node.js 는 AsyncLocalStorage 가 같은 역할을 합니다. 이름은 달라도 원리는 하나입니다 — 컨텍스트는 실행 단위를 따라가지 않으므로 명시적으로 옮겨야 합니다.
끊김 2 — 메시지 큐. 큐는 프로세스 경계이자 시간 경계입니다. HTTP 처럼 헤더가 자동으로 흐르지 않으므로 프로듀서가 메시지 헤더에 컨텍스트를 주입하고 컨슈머가 추출해야 합니다. 여기서 중요한 설계 판단이 하나 있습니다. 배치 컨슈머는 부모-자식이 아니라 Link 를 써야 합니다. 메시지 100개를 한 번에 처리하면 부모가 100개인데 스팬은 부모를 하나만 가질 수 있기 때문입니다. 큐 대기 시간이 긴 파이프라인은 메시지가 하나여도 Link 를 고려합니다. 부모-자식으로 이으면 트레이스 하나의 지속 시간이 대기 시간만큼 늘어나 백엔드에서 다루기 어려워집니다.
끊김 3 — 백그라운드 작업과 스케줄러. 크론이나 백그라운드 태스크는 부모가 없습니다. 흔한 실수는 요청 컨텍스트를 억지로 이어 붙이는 것입니다. 요청은 이미 응답을 보내고 끝났는데 그 트레이스에 30초짜리 자식이 붙으면 요청 지연 통계가 오염됩니다. 처방은 새 루트 트레이스로 시작하고 원인 요청은 Link 로 남기는 것입니다.
끊김 4 — 헤더를 지우는 중간 계층. 프록시, WAF, API 게이트웨이, CDN 이 화이트리스트 방식으로 헤더를 필터링하면 traceparent 가 조용히 사라집니다. 로그에는 아무것도 남지 않고 증상은 "게이트웨이 뒤부터 트레이스가 새로 시작된다"입니다. 허용 목록에 traceparent, tracestate, baggage 가 있는지 확인하고, B3 헤더를 쓰는 레거시가 섞여 있으면 전파기를 복수로 설정합니다.
리소스 속성과 스팬 속성의 경계도 시험에 나옵니다.
| 구분 | 대상 | 예 |
| --- | --- | --- |
| 리소스 속성 | 텔레메트리를 만든 주체 | service.name, k8s.pod.name, host.name |
| 스팬 속성 | 그 작업 한 번 | http.route, db.system, cart.item_count |
스팬 속성의 고카디널리티는 메트릭에서와 달리 폭발 문제가 아닙니다. 주문 ID, 사용자 ID, 쿼리 파라미터는 트레이스에 넣으라고 있는 값이고, 없으면 트레이스는 필터링할 수 없는 그림이 됩니다. 문제가 되는 곳은 두 군데입니다. 스팬을 메트릭으로 변환할 때(spanmetrics 의 dimensions 에 넣은 속성이 그대로 메트릭 레이블이 됩니다)와 스팬 자체의 크기입니다.
service.instance.id 는 카디널리티가 높지만 리소스 속성이라 트레이스와 로그에서는 문제가 없습니다. 다만 이것을 메트릭 레이블로 승격시키면 시계열이 파드 수만큼 곱해지므로, 컬렉터에서 메트릭 파이프라인에 한해 제거하는 것이 일반적입니다.
현장에서 만나는 모습
전파가 살아 있는지 확인하는 가장 값싼 방법은 게이트웨이 흉내를 내는 것입니다. 알려진 trace ID 를 traceparent 헤더에 직접 넣어 요청을 보내고, 백엔드에서 그 ID 로 조회했을 때 스팬이 그 아래에 붙는지 봅니다. 안 붙으면 위의 네 가지 중 하나입니다.
계측이 끝났다고 말할 수 있는 기준도 체크리스트로 둡니다. 프로덕션 요청 하나를 골라 트레이스를 열었을 때 관여한 서비스 수와 SERVER 스팬 수가 일치하는가, 루트 스팬 지속 시간이 게이트웨이 접근 로그의 응답 시간과 맞는가, 가장 큰 self time 이 전체의 20% 미만인가, trace ID 로 로그가 검색되는가, 스팬 이름 상위 20개가 라우트 템플릿이지 ID 가 아닌가, 큐를 넘는 작업이 하나의 트레이스나 Link 로 이어져 있는가. 마지막 항목 하나 — 컬렉터를 한 번 죽여 보고 앱의 에러율과 지연이 흔들리지 않는지 확인하는 것 — 은 실제로 해 봐야만 압니다.
다음 확인에서 볼 것
이 모듈은 개념 모듈이라 실습이 없습니다. 퀴즈로 traceparent 필드와 Link 의 판단 기준을 확인한 뒤, 마지막 모듈에서 샘플링 정책과 버퍼 메모리 계산으로 넘어갑니다.