LabHub
배우기 러닝패스 코스

Where Distributed Tracing Breaks

Deciding What to Throw Away

LabHub 에서 이어서 보기

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

한 줄 요약

샘플링은 비용을 줄이는 장치가 아니라 무엇을 볼지 정하는 설계다. 잘못 정하면 정작 필요한 트레이스가 없다.

Flow map: 비용을 줄이는 장치가 아니라 무엇을 볼지 정하는 설계 · 그 요청의 트레이스가 없을 확률이 99% · 헤드에서 1020% · 오류는 100%

왜 이게 필요했나

트레이스는 요청마다 스팬 수십 개를 만든다. 초당 1,000 요청이면 하루에 수십억 스팬이다. 전부 저장하면 저장 비용이 애플리케이션 비용을 넘는다.

그런데 무작정 1% 만 남기면, 장애가 났을 때 그 요청의 트레이스가 없을 확률이 99% 다. 그래서 샘플링은 "얼마나 줄일까" 가 아니라 "무엇을 반드시 남길까" 로 접근한다.

세 가지 방식

방식 언제 정하나 장점 대가
헤드 요청 시작 시 싸고 단순, 전파가 쉽다 느린 요청만 고를 수 없다
테일 트레이스 완료 후 오류·느린 것만 정확히 고른다 스팬을 모아 둬야 해 비싸다
레이트 리미팅 초당 N개 상한 폭주해도 비용이 안 튄다 폭주 구간의 표본이 적어진다

실무 조합은 대개 이렇다.

  1. 헤드에서 10~20% 로 기본선을 줄인다
  2. 오류는 100% 강제한다(traceparent 의 샘플 플래그를 켜서 전파)
  3. 중요한 엔드포인트만 테일 샘플링으로 느린 것을 건진다

샘플링 결정은 전파되어야 한다

여기서 가장 많이 틀리는 지점입니다. 샘플링은 트레이스 단위 로 정해져야 하는데, 각 서비스가 따로 정하면 트레이스가 조각납니다.

프런트(20%) → API(20%) → 결제(20%)

각자 정하면 세 서비스가 모두 남길 확률 = 0.2³ = 0.8%
→ 완전한 트레이스는 거의 안 남고, 조각난 스팬만 쌓인다

W3C traceparent 헤더의 마지막 바이트가 샘플 플래그입니다. 맨 앞에서 한 번 정하고, 뒤로는 그 결정을 따르기만 해야 합니다.

traceparent: 00-4bf92f...36-00f067aa0ba902b7-01
                                              ↑ 01 = 샘플됨, 00 = 아님

OpenTelemetry SDK 의 ParentBased(root=TraceIdRatioBased(0.2)) 가 이것을 구현합니다. 부모가 있으면 부모의 결정을 따르고, 없을 때(루트일 때)만 20% 주사위를 굽니다. TraceIdRatioBased 만 쓰면 서비스마다 따로 굴려 위의 0.8% 문제가 생깁니다.

TraceIdRatioBased 가 무작위가 아니라 trace ID 의 해시 로 정하는 이유도 여기 있습니다. 같은 trace ID 는 어느 서비스에서 계산해도 같은 답이 나오므로, 설정이 같다면 결정이 저절로 일치합니다.

오류를 100% 남기는 방법

"오류는 다 남긴다" 는 말은 쉽지만 헤드 샘플링에서는 불가능합니다. 요청이 시작될 때는 그것이 실패할지 모르기 때문입니다. 두 가지 우회로가 있습니다.

애플리케이션이 플래그를 켠다. 오류를 감지한 시점에 현재 스팬을 강제로 기록 대상으로 표시합니다. 이미 지나간 부모 스팬은 못 살리지만, 그 아래는 남습니다.

테일 샘플링을 쓴다. 컬렉터가 트레이스 전체를 본 뒤 정하므로 정확합니다. 정책은 이런 모양입니다.

tail_sampling:
  decision_wait: 10s
  policies:
    - name: errors            # 오류는 전부
      type: status_code
      status_code: {status_codes: [ERROR]}
    - name: slow              # 2초 넘는 것은 전부
      type: latency
      latency: {threshold_ms: 2000}
    - name: baseline          # 나머지는 5%
      type: probabilistic
      probabilistic: {sampling_percentage: 5}

정책은 OR 로 평가됩니다 — 하나라도 맞으면 남깁니다.

테일 샘플링의 숨은 비용

컬렉터가 트레이스 전체를 메모리에 모아 두었다가 판단한다. 그래서 두 가지가 따라온다.

이 두 가지를 모르고 테일 샘플링을 켜면 컬렉터가 OOM 으로 죽고, 죽는 동안의 트레이스는 통째로 사라진다.

흔한 착각

"샘플링 비율을 올리면 더 정확해진다" — 지표는 샘플링과 무관해야 한다. 요청 수·오류율·지연은 메트릭으로 재고, 트레이스는 "그 요청이 어디서 시간을 썼나" 를 보는 도구다. 트레이스로 비율을 계산하면 샘플링 편향이 그대로 들어간다.

"오류만 남기면 된다" — 오류가 아닌 느린 요청이 더 많은 정보를 준다. 오류는 로그에도 남지만, 정상 응답인데 3초 걸린 요청은 트레이스가 아니면 설명할 수 없다.

실무에서 진짜 중요한 것

샘플링 결정은 맨 앞에서 한 번 내리고 끝까지 전파돼야 한다. 중간 서비스가 자기 판단으로 다시 정하면 트레이스가 반쪽만 남는다.

traceparent 의 마지막 플래그(01/00)가 그 결정을 실어 나른다. 자동 계측은 이걸 지켜 주지만, 직접 HTTP 클라이언트를 만드는 코드가 헤더를 새로 쓰면 그 지점에서 결정이 초기화된다.