트레이싱(Tracing)과 스팬(Span) 심화
트레이스가 답하는 질문
메트릭은 "에러율이 5%다"라고 알려준다. 로그는 "14:32:01에 타임아웃 예외가 발생했다"고 기록한다. 하지만 "그 요청이 어떤 서비스를 거쳐, 어디서 지연이 생겼고, 어느 DB 쿼리가 느렸는가"는 트레이스가 답한다.
트레이스(Trace)는 단일 요청이 분산 시스템을 통과하는 전체 여정이다. 요청이 어떤 서비스에 닿았는지, 각 구간이 얼마나 걸렸는지, 어디서 에러가 났는지를 한눈에 보여준다. 그 여정을 구성하는 단위 작업 하나하나가 스팬(Span)이다.
스팬의 구조
스팬은 단일 작업을 나타내는 타임드 구간이다. 시작 시각과 종료 시각이 있고, 다음 필드로 구성된다.
| 필드 | 설명 |
|---|---|
trace_id | 요청 전체를 묶는 16바이트 고유 식별자 |
span_id | 이 스팬을 식별하는 8바이트 식별자 |
parent_span_id | 부모 스팬의 ID. 없으면 루트 스팬 |
name | 작업 이름. 예: GET /api/orders |
kind | 스팬 역할 (아래 참고) |
start_time / end_time | 나노초 정밀도 타임스탬프 |
attributes | 키-값 메타데이터. 최대 128자 string, bool, number, array |
events | 스팬 내 타임스탬프 있는 점 이벤트 (SQL 쿼리 실행 시점 등) |
links | 다른 스팬·트레이스와의 인과 관계 참조 |
status | Unset / Ok / Error |
resource | 서비스 이름, 버전, 호스트 같은 인프라 메타데이터 |
스팬 종류(Span Kind)
kind는 스팬이 시스템 내에서 어떤 역할을 하는지 표시한다. 분산 추적 UI에서 서비스 간 경계를 파악하는 데 쓰인다.
| Kind | 의미 |
|---|---|
Internal | 서비스 내부 작업. 기본값 |
Server | 원격 호출을 수신하는 쪽 (HTTP 서버, gRPC 서버) |
Client | 원격 서비스를 호출하는 쪽 (HTTP 클라이언트, DB 드라이버) |
Producer | 메시지 브로커에 메시지를 발행하는 쪽 |
Consumer | 메시지 브로커에서 메시지를 소비하는 쪽 |
트레이스 구조: 부모-자식 트리
여러 스팬이 parent_span_id로 연결되어 트리를 이룬다. 아래 다이어그램은 사용자 주문 요청이 세 서비스를 거치는 예다.
컨텍스트 전파(Context Propagation)
스팬 트리가 여러 서비스에 걸쳐 있으려면 trace_id와 span_id를 서비스 경계를 넘어 전달해야 한다. 이것이 컨텍스트 전파다.
OTel은 W3C Trace Context 표준을 기본 형식으로 사용한다. HTTP 요청에 두 헤더가 붙는다.
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
tracestate: vendor1=opaqueValue01traceparent 필드 구조:
00— 버전 (현재 항상 00)4bf92f3577b34da6a3ce929d0e0e4736— trace_id (32 hex chars, 16 bytes)00f067aa0ba902b7— parent_span_id (16 hex chars, 8 bytes)01— trace flags (01= 샘플링됨,00= 샘플링 안 됨)
tracestate는 벤더 특화 메타데이터를 옮긴다. OTel SDK는 인바운드 요청에서 이 헤더를 읽어 현재 스팬의 부모로 설정하고, 아웃바운드 요청에 자동으로 다시 삽입한다.
gRPC에서는 grpc-trace-bin 바이너리 헤더나 traceparent를 메타데이터로 전달한다. 메시지 큐에서는 메시지 헤더나 속성에 컨텍스트를 실어 보낸다.
Baggage
traceparent와 별도로 Baggage는 임의의 키-값 쌍을 서비스 경계를 넘어 전파한다. 예를 들어 사용자 ID나 A/B 테스트 플래그를 Baggage에 넣으면 모든 하위 서비스가 읽을 수 있다. 단, Baggage는 자동으로 스팬 속성이 되지 않으므로 성능과 개인정보 영향을 고려해서 최소한으로 써야 한다.
스팬 이벤트와 상태
이벤트(Events)
이벤트는 스팬 수명 중 특정 시점에 일어난 일을 타임스탬프와 함께 기록한다. 기간이 없는 점 표시다.
with tracer.start_as_current_span("order.process") as span:
span.add_event("cache.miss", {"cache.key": "order:42"})
result = db.fetch_order(42)
span.add_event("db.result", {"row.count": len(result)})상태(Status)
스팬의 최종 상태는 세 값 중 하나다.
| Status | 의미 |
|---|---|
Unset | 기본값. 명시적 성공·실패 없음 |
Ok | 명시적으로 성공 표시 (호출자의 판단) |
Error | 실패. description 문자열을 함께 기록 가능 |
예외를 잡으면 스팬에 기록하고 상태를 Error로 표시하는 것이 표준 패턴이다.
except Exception as e:
span.record_exception(e)
span.set_status(StatusCode.ERROR, str(e))
raise샘플링: 무엇을 저장하고 버릴 것인가
트래픽이 많으면 모든 요청의 트레이스를 저장하기 어렵다. 샘플링은 어떤 트레이스를 보관할지 결정하는 정책이다.
헤드 샘플링(Head Sampling)
요청이 들어오는 시작 시점에 저장 여부를 결정한다. 결정이 이루어지면 traceparent의 플래그로 하위 서비스에 전파된다. 모든 서비스가 같은 결정을 따른다는 점이 장점이다.
가장 간단한 구현은 확률 샘플러다.
from opentelemetry.sdk.trace.sampling import TraceIdRatioBased
# 10% 샘플링
sampler = TraceIdRatioBased(0.1)단점은 느리거나 에러가 난 요청이 운이 없으면 버려질 수 있다는 것이다.
테일 샘플링(Tail Sampling)
스팬을 수집한 뒤 전체 트레이스를 보고 저장 여부를 결정한다. "에러가 있는 트레이스", "200ms 이상 걸린 트레이스" 같은 조건으로 선택적 보존이 가능하다. OTel Collector의 tail_sampling 프로세서가 이 역할을 한다.
processors:
tail_sampling:
decision_wait: 10s
policies:
- name: keep-errors
type: status_code
status_code: { status_codes: [ERROR] }
- name: keep-slow
type: latency
latency: { threshold_ms: 200 }
- name: probabilistic
type: probabilistic
probabilistic: { sampling_percentage: 5 }테일 샘플링은 더 정교하지만, Collector가 같은 트레이스의 모든 스팬을 모아야 하기 때문에 상태를 유지해야 하고 메모리 비용이 크다.
두 전략 조합
실무에서는 헤드 샘플링으로 1차 필터링하고(예: 50%), Collector에서 테일 샘플링으로 에러·지연이 큰 트레이스를 전량 보존하는 식으로 조합한다.
실제 트레이스가 보여주는 것
트레이싱 UI(Jaeger, Tempo 등)에서 위 다이어그램 같은 요청을 열면 이런 정보를 얻는다.
- 병목 구간: DB 쿼리가 전체 60ms 중 50ms를 차지한다면 즉시 보인다.
- 에러 전파: Payment 서비스가 External API를 호출해서 실패했고, 그 에러가 주문 서비스로 올라갔는지 추적된다.
- 서비스 맵: 어떤 서비스가 어떤 서비스를 호출하는지 자동으로 그려진다.
- 크리티컬 패스: 가장 긴 순차 경로를 찾아 최적화 대상을 좁힌다.
한 줄 정리
스팬은 trace_id와 parent_span_id로 부모-자식 트리를 이루고, W3C traceparent 헤더로 서비스 경계를 넘어 전파된다. 헤드 샘플링은 빠르고 일관적이지만 맹목적이고, 테일 샘플링은 의미 있는 트레이스를 선택적으로 보존하지만 상태를 유지해야 한다.
References
- https://opentelemetry.io/docs/concepts/signals/traces/
- https://last9.io/blog/opentelemetry-spans-events/
- https://opentelemetry.io/docs/specs/otel/trace/api/
- https://www.w3.org/TR/trace-context/
- https://opentelemetry.io/docs/concepts/sampling/
- https://signoz.io/blog/opentelemetry-spans/
- https://oneuptime.com/blog/post/2025-08-27-traces-and-spans-in-opentelemetry/view