何を捨てるかを決める仕事
한국어 원문으로 표시합니다.
한 줄 요약
샘플링은 비용을 줄이는 장치가 아니라 무엇을 볼지 정하는 설계다. 잘못 정하면 정작 필요한 트레이스가 없다.
왜 이게 필요했나
트레이스는 요청마다 스팬 수십 개를 만든다. 초당 1,000 요청이면 하루에 수십억 스팬이다. 전부 저장하면 저장 비용이 애플리케이션 비용을 넘는다.
그런데 무작정 1% 만 남기면, 장애가 났을 때 그 요청의 트레이스가 없을 확률이 99% 다. 그래서 샘플링은 "얼마나 줄일까" 가 아니라 "무엇을 반드시 남길까" 로 접근한다.
세 가지 방식
| 방식 | 언제 정하나 | 장점 | 대가 |
|---|---|---|---|
| 헤드 | 요청 시작 시 | 싸고 단순, 전파가 쉽다 | 느린 요청만 고를 수 없다 |
| 테일 | 트레이스 완료 후 | 오류·느린 것만 정확히 고른다 | 스팬을 모아 둬야 해 비싸다 |
| 레이트 리미팅 | 초당 N개 상한 | 폭주해도 비용이 안 튄다 | 폭주 구간의 표본이 적어진다 |
실무 조합은 대개 이렇다.
- 헤드에서 10~20% 로 기본선을 줄인다
- 오류는 100% 강제한다(
traceparent의 샘플 플래그를 켜서 전파) - 중요한 엔드포인트만 테일 샘플링으로 느린 것을 건진다
샘플링 결정은 전파되어야 한다
여기서 가장 많이 틀리는 지점입니다. 샘플링은 트레이스 단위 로 정해져야 하는데, 각 서비스가 따로 정하면 트레이스가 조각납니다.
프런트(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 로 평가됩니다 — 하나라도 맞으면 남깁니다.
테일 샘플링의 숨은 비용
컬렉터가 트레이스 전체를 메모리에 모아 두었다가 판단한다. 그래서 두 가지가 따라온다.
- 메모리: 대기 시간(보통 5~30초) × 초당 트레이스 수만큼 쌓인다
- 라우팅: 같은 트레이스의 스팬이 같은 컬렉터로 가야 한다. 그래서 컬렉터 앞에
trace_id기준 로드밸런서를 둔다. 안 하면 트레이스가 조각나 판단이 틀린다
이 두 가지를 모르고 테일 샘플링을 켜면 컬렉터가 OOM 으로 죽고, 죽는 동안의 트레이스는 통째로 사라진다.
흔한 착각
"샘플링 비율을 올리면 더 정확해진다" — 지표는 샘플링과 무관해야 한다. 요청 수·오류율·지연은 메트릭으로 재고, 트레이스는 "그 요청이 어디서 시간을 썼나" 를 보는 도구다. 트레이스로 비율을 계산하면 샘플링 편향이 그대로 들어간다.
"오류만 남기면 된다" — 오류가 아닌 느린 요청이 더 많은 정보를 준다. 오류는 로그에도 남지만, 정상 응답인데 3초 걸린 요청은 트레이스가 아니면 설명할 수 없다.
실무에서 진짜 중요한 것
샘플링 결정은 맨 앞에서 한 번 내리고 끝까지 전파돼야 한다. 중간 서비스가 자기 판단으로 다시 정하면 트레이스가 반쪽만 남는다.
traceparent 의 마지막 플래그(01/00)가 그 결정을 실어 나른다. 자동 계측은 이걸 지켜 주지만, 직접 HTTP 클라이언트를 만드는 코드가 헤더를 새로 쓰면 그 지점에서 결정이 초기화된다.