LLM WikiAccess-protected knowledge portal
← 스터디 홈
2편 · 약 14분

트레이싱(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다른 스팬·트레이스와의 인과 관계 참조
statusUnset / Ok / Error
resource서비스 이름, 버전, 호스트 같은 인프라 메타데이터

스팬 종류(Span Kind)

kind는 스팬이 시스템 내에서 어떤 역할을 하는지 표시한다. 분산 추적 UI에서 서비스 간 경계를 파악하는 데 쓰인다.

Kind의미
Internal서비스 내부 작업. 기본값
Server원격 호출을 수신하는 쪽 (HTTP 서버, gRPC 서버)
Client원격 서비스를 호출하는 쪽 (HTTP 클라이언트, DB 드라이버)
Producer메시지 브로커에 메시지를 발행하는 쪽
Consumer메시지 브로커에서 메시지를 소비하는 쪽

트레이스 구조: 부모-자식 트리

여러 스팬이 parent_span_id로 연결되어 트리를 이룬다. 아래 다이어그램은 사용자 주문 요청이 세 서비스를 거치는 예다.

← 시간 흐름 (전체 120ms) 0ms 60ms 120ms API Gateway GET /api/orders [Server] 120ms Order Service order.process [Internal] 95ms Order Service db.query SELECT orders [Client] 60ms Payment Service payment.charge [Server] 40ms Payment Gateway http.post /charge [Client] 30ms 모든 스팬이 공유하는 trace_id: 4bf92f3577b34da6a3ce929d0e0e4736 루트 스팬의 span_id: 00f067aa0ba902b7 자식 스팬의 parent_span_id = 부모 스팬의 span_id
분산 트레이스 스팬 트리 예시

컨텍스트 전파(Context Propagation)

스팬 트리가 여러 서비스에 걸쳐 있으려면 trace_idspan_id를 서비스 경계를 넘어 전달해야 한다. 이것이 컨텍스트 전파다.

OTel은 W3C Trace Context 표준을 기본 형식으로 사용한다. HTTP 요청에 두 헤더가 붙는다.

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
tracestate:  vendor1=opaqueValue01

traceparent 필드 구조:

  • 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