OpenTelemetry Tail Sampling 심화: 복합 정책 설계와 decision_wait로 메모리 최적화하기
이 글은 OTel Collector를 이미 운영 중인 DevOps 엔지니어를 대상으로 합니다. 헤드 기반 샘플링을 쓰고 있다면 한 가지 불편한 진실을 먼저 직면해야 합니다. 프로덕션에서 Collector가 조용히 트레이스를 드롭하고 있어도 알아채기 어렵고, 알아챘을 때는 이미 그날의 에러 트레이스가 사라진 뒤입니다. Tail Sampling은 이 문제를 근본적으로 해결하지만, 잘못 설정하면 다른 종류의 문제가 찾아옵니다. 1,000 TPS 환경에서 decision_wait: 30s를 그대로 두면 메모리가 600MB에 달하고, 이것이 OOM Kill의 전조가 됩니다.
이 글을 마치면 복합 정책 설계, decision_wait 튜닝, 2-tier 아키텍처 구성을 포함한 프로덕션 설정을 30분 안에 검토할 수 있습니다. OpenTelemetry Collector contrib 패키지의 tailsamplingprocessor를 기준으로, 정책 종류별 특성부터 메모리 추정 공식, 모니터링 메트릭까지 단계별로 살펴봅니다.
핵심 개념
Head vs. Tail Sampling — 무엇이 다른가
헤드 기반 샘플링은 첫 번째 스팬이 생성되는 시점에 샘플링 여부를 결정합니다. 빠르고 상태가 없어(stateless) 수평 확장이 쉽지만, 트레이스가 에러를 포함하는지, 응답이 느린지 알 수 없는 상태에서 결정을 내려야 합니다.
Tail Sampling은 모든 스팬이 Collector에 도착한 뒤 결정을 내립니다. 이를 위해 트레이스 전체를 일정 시간 동안 메모리에 보관해야 하며, 이것이 메모리 관리가 핵심 과제가 되는 이유입니다.
| 항목 | Head Sampling | Tail Sampling |
|---|---|---|
| 결정 시점 | 트레이스 시작 시 | 모든 스팬 수집 후 |
| 에러 기반 결정 | 불가 | 가능 |
| 레이턴시 기반 결정 | 불가 | 가능 |
| 메모리 요구 | 낮음 | 높음 |
| 수평 확장 | 쉬움 | stateful — 별도 설계 필요 |
stateful이란? 동일 트레이스의 모든 스팬이 반드시 같은 Collector 인스턴스에 모여야 올바른 샘플링 결정이 가능하다는 의미입니다. 단순히 레플리카 수를 늘리는 것만으로는 수평 확장이 되지 않습니다.
핵심 파라미터 이해와 메모리 추정
Tail Sampling Processor를 제어하는 세 가지 핵심 파라미터가 있습니다.
processors:
tail_sampling:
decision_wait: 10s # 첫 스팬 도착 후 결정까지 대기 시간
num_traces: 100000 # 메모리에 유지할 최대 트레이스 수
expected_new_traces_per_sec: 1000 # 내부 맵 사전 할당용
decision_wait란? 첫 번째 스팬이 도착한 시점부터 샘플링 결정을 내리기까지 대기하는 시간입니다. 이 시간 안에 해당 트레이스의 모든 스팬이 도착하기를 기대합니다. 길수록 정확도가 높아지고, 짧을수록 메모리가 절약됩니다.
expected_new_traces_per_sec주의: 이 값이 실제 유입량보다 너무 낮으면 내부 맵이 런타임에 리사이징되면서 순간적인 레이턴시 스파이크가 발생할 수 있습니다. 실제 TPS의 120~150% 수준으로 여유 있게 설정하는 것을 권장합니다.
운영 전에 반드시 메모리를 추정해보는 것이 좋습니다.
필요 메모리 ≈ traces_per_second × decision_wait(초) × avg_spans_per_trace × avg_span_size(bytes)초당 1,000 트레이스, 30s 대기, 트레이스당 평균 10 스팬, 스팬당 2KB라면:
1,000 TPS × 30s × 10 spans × 2KB = 600MB같은 조건에서 decision_wait를 10s로 줄이면 200MB로 3분의 1이 됩니다. 이것이 decision_wait 단축이 가장 효과적인 메모리 절감 전략인 이유입니다. 위 계산에서 num_traces: 100000은 1,000 TPS × 100s를 수용하는 크기인데, decision_wait: 10s 기준이라면 30,000으로도 충분합니다. 남는 num_traces는 메모리를 불필요하게 점유하므로 계산 후 조정하는 것을 권장합니다.
decision_wait 시작점 공식: 서비스 p99 응답시간 × 3
| 상황 | 권장 decision_wait |
|---|---|
| 일반 웹 API (p99 < 1s) | 5s ~ 10s |
| 배치 처리 포함 혼합 워크로드 | 15s ~ 20s |
| 레거시 시스템 (p99 > 5s) | 30s (기본값 유지) |
GOMEMLIMIT과 memory_limiter 병행 구성
메모리 보호를 위해 두 가지 메커니즘을 함께 사용하는 것이 현재 표준 패턴입니다.
memory_limiter프로세서: 메모리 상한 초과 시 스팬을 드롭해 OOM을 방지합니다. 반드시 파이프라인에서tail_sampling앞에 배치해야 백프레셔가 제대로 동작합니다.GOMEMLIMIT환경변수: Go 런타임의 소프트 메모리 한도입니다. 컨테이너 메모리 한도의 80~90% 수준으로 설정하면 GC가 OOM Kill 전에 적극적으로 메모리를 회수합니다.
# 환경변수 설정 예 (Kubernetes)
env:
- name: GOMEMLIMIT
value: "1800MiB" # 컨테이너 limit이 2GiB인 경우정책(Policy) 종류 한눈에 보기
| 정책 타입 | 설명 | 대표 사용 사례 |
|---|---|---|
status_code |
ERROR / UNSET / OK 기반 | 에러 트레이스 전량 보존 |
latency |
threshold_ms 초과 여부 |
느린 요청 포착 |
string_attribute |
문자열 속성값 (정규식 지원) | 특정 서비스·경로 필터 |
numeric_attribute |
숫자 속성 범위 | 특정 user_id 범위 등 |
probabilistic |
확률적 비율 | 정상 트래픽 일부만 보존 |
and |
여러 정책을 AND 조합 | 조건 교차 필터 |
composite |
정책별 비율 할당 | 트래픽 예산 분배 |
always_sample |
모든 트레이스 통과 | composite 내 폴백용 |
실전 적용
예시 1: 에러 + 레이턴시 + 확률적 샘플링 복합 정책
가장 보편적인 구성입니다. 중요한 트레이스(에러, 느린 요청)는 전량 보존하고, 나머지는 5%만 샘플링합니다. 아래는 service.pipelines까지 포함한 완성형 예시입니다.
processors:
memory_limiter:
check_interval: 1s
limit_mib: 1800
spike_limit_mib: 400
tail_sampling:
decision_wait: 10s
num_traces: 30000 # 1,000 TPS × 10s = 10,000 + 여유분
expected_new_traces_per_sec: 1200
policies:
# 에러 트레이스 전량 보존
- name: errors-policy
type: status_code
status_code:
status_codes: [ERROR]
# 500ms 초과 느린 트레이스 보존
- name: slow-traces-policy
type: latency
latency:
threshold_ms: 500
# 나머지는 5%만 샘플링
- name: probabilistic-policy
type: probabilistic
probabilistic:
sampling_percentage: 5
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, tail_sampling]
exporters: [otlp/backend]정책 평가 방식: 모든 정책이 순서대로 평가되며, 하나라도
SAMPLE을 반환하면 해당 트레이스는 보존됩니다. 정책 순서가 결과 자체를 바꾸지는 않지만, 에러 정책을 앞에 두면 early exit으로 평가 비용을 줄일 수 있고 의도를 명확히 전달할 수 있습니다.
| 정책 | 역할 | 주의점 |
|---|---|---|
errors-policy |
에러 트레이스 보장 | 앞에 배치해 의도를 명확히 표현 |
slow-traces-policy |
성능 이슈 포착 | threshold_ms는 p99 기준으로 설정 권장 |
probabilistic-policy |
비용 절감 | 에러·느린 요청과 무관하게 독립적으로 평가됨 |
예시 2: AND 정책 — 결제 서비스의 느린 트레이스만 포착
특정 서비스에서 발생하는 느린 요청만 골라내고 싶을 때 and 정책을 활용할 수 있습니다.
processors:
tail_sampling:
decision_wait: 10s
num_traces: 30000
expected_new_traces_per_sec: 1200
policies:
- name: slow-payment-traces
type: and
and:
and_sub_policy:
- name: latency-check
type: latency
latency:
threshold_ms: 300
- name: service-check
type: string_attribute
string_attribute:
key: service.name
values: [payment-service]
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, tail_sampling]
exporters: [otlp/backend]and 정책은 모든 서브 정책이 SAMPLE을 반환해야 최종적으로 샘플링이 확정됩니다. 위 예시는 payment-service이면서 동시에 300ms를 초과하는 트레이스만 보존합니다.
예시 3: Composite 정책 — 초당 스팬 수를 예산처럼 배분
예시 1의 기본 구성으로 시작했지만 백엔드 비용을 더 정밀하게 제어해야 한다면 composite 정책으로 확장할 수 있습니다. max_total_spans_per_second로 총 처리량을 제한하고 각 정책에 비율을 할당합니다.
processors:
tail_sampling:
decision_wait: 10s
num_traces: 30000
expected_new_traces_per_sec: 1200
policies:
- name: composite-policy
type: composite
composite:
max_total_spans_per_second: 1000
policy_order: [errors-policy, slow-traces-policy, probabilistic-policy]
composite_sub_policy:
- name: errors-policy
type: status_code
status_code:
status_codes: [ERROR]
- name: slow-traces-policy
type: latency
latency:
threshold_ms: 500
- name: probabilistic-policy
type: always_sample
rate_allocation:
- policy: errors-policy
percent: 50 # 초당 최대 500 스팬을 에러 트레이스에
- policy: slow-traces-policy
percent: 30 # 초당 최대 300 스팬을 느린 트레이스에
# 나머지 20%(200 스팬)는 probabilistic-policy에 할당
# 주의: rate_allocation에 명시되지 않은 정책의 남은 예산은
# 해당 정책에 분배되는 것이 아니라 버려집니다.
# 모든 정책을 명시적으로 선언하는 것을 권장합니다.
rate_allocation주의: 퍼센트 합이 100%에 미달하더라도 나머지 예산이 자동으로 다른 정책에 배분되지 않습니다. 명시되지 않은 정책의 미할당 예산은 버려집니다. 모든 서브 정책을rate_allocation에 명시적으로 선언하는 것을 권장합니다.
예시 4: 헬스체크 경로 제외 (invert_match)
/health, /readyz 같은 고빈도 노이즈 요청을 샘플링에서 제외할 수 있습니다.
processors:
tail_sampling:
decision_wait: 10s
num_traces: 30000
expected_new_traces_per_sec: 1200
policies:
# 헬스체크 경로는 NOT_SAMPLED 처리
- name: exclude-health-check
type: string_attribute
string_attribute:
key: http.target
values: ["/health", "/readyz", "/metrics"]
invert_match: true
# 나머지 트레이스는 5% 샘플링
- name: probabilistic-policy
type: probabilistic
probabilistic:
sampling_percentage: 5
invert_match: true동작 방식: 이 정책은 지정된 값과 일치하는 트레이스를NOT_SAMPLED로 처리합니다. 일치하지 않는 트레이스는 이 정책에서 결정이 내려지지 않고 다음 정책으로 넘어갑니다.invert_match정책 단독으로는 "나머지를 모두 샘플링"하는 효과가 없으므로, 반드시 다른 정책과 함께 복합 구성으로 사용해야 합니다.
예시 5: 2-tier 아키텍처 — 수평 확장을 위한 필수 구성
Tail Sampling은 stateful하기 때문에 단순 수평 확장이 불가합니다. 동일 트레이스의 스팬이 서로 다른 Collector 인스턴스에 분산되면 올바른 샘플링 결정을 내릴 수 없습니다. 이 문제를 해결하는 공식 권장 아키텍처가 2-tier 구성입니다.
# 1단계: 로드밸런서 Collector — trace_id 해시 기반 라우팅
exporters:
loadbalancing:
routing_key: traceID
protocol:
otlp:
tls:
insecure: true # 개발/테스트용. 프로덕션은 TLS 인증서 설정 필요
resolver:
k8s:
service: otel-collector-tailsampling-headless
service:
pipelines:
traces:
receivers: [otlp]
exporters: [loadbalancing]# 2단계: Tail Sampling Collector — 위 예시의 tail_sampling 프로세서 적용
processors:
memory_limiter:
check_interval: 1s
limit_mib: 1800
spike_limit_mib: 400
tail_sampling:
decision_wait: 10s
num_traces: 30000
expected_new_traces_per_sec: 1200
policies:
- name: errors-policy
type: status_code
status_code:
status_codes: [ERROR]
- name: slow-traces-policy
type: latency
latency:
threshold_ms: 500
- name: probabilistic-policy
type: probabilistic
probabilistic:
sampling_percentage: 5
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, tail_sampling]
exporters: [otlp/backend]
loadbalancingexporter란? trace_id를 해시 키로 사용해 동일 트레이스의 모든 스팬을 항상 같은 Collector 인스턴스로 라우팅하는 exporter입니다. 2-tier Tail Sampling 아키텍처의 핵심 컴포넌트로, 이 레이어 없이 Tail Sampling Collector를 수평 확장하면 잘못된 샘플링 결정이 발생합니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 완전한 정보 기반 결정 | 모든 스팬 수집 후 판단 — 에러와 레이턴시를 정확히 포착 가능 |
| 비용 절감 효과 | 백엔드로 전송되는 트레이스 80~95% 감소 가능 |
| 유연한 정책 조합 | AND / Composite로 복잡한 비즈니스 규칙 표현 가능 |
| 노이즈 제거 | 헬스체크, 정상 고빈도 요청 손쉽게 제외 |
단점 및 주의사항
| 항목 | 설명 | 대응 방안 |
|---|---|---|
| 메모리 압박 | decision_wait × TPS 만큼 트레이스가 메모리에 상주 |
decision_wait 단축 + memory_limiter + GOMEMLIMIT 병행 |
| num_traces 초과 드롭 | 한도 초과 시 오래된 트레이스가 드롭되고 불완전한 트레이스 전송 가능 | sampling_trace_dropped_too_early_count 메트릭 모니터링 |
| stateful 제약 | 단순 수평 확장 불가 | loadbalancingexporter 선행 레이어 구성 필수 |
| 스팬 누락 위험 | decision_wait 내 도착하지 못한 스팬은 결정에서 제외 |
p99 기반으로 여유 있게 설정 |
모니터링 필수 메트릭
# 이 메트릭이 증가하면 num_traces를 늘리거나 decision_wait를 단축해야 합니다
otelcol_processor_tail_sampling_sampling_trace_dropped_too_early_count
# 정책 평가 오류 — 정책 설정 문제 진단이 필요한 경우
otelcol_processor_tail_sampling_sampling_policy_evaluation_error_count
# 실제 샘플링된 트레이스 수 — 비율 검증
otelcol_processor_tail_sampling_count_traces_sampled실무에서 가장 흔한 실수
memory_limiter를tail_sampling뒤에 배치하는 것 —memory_limiter는 반드시 파이프라인에서tail_sampling앞에 위치해야 백프레셔가 제대로 동작합니다.- 2-tier 아키텍처 없이 Tail Sampling Collector를 수평 확장하는 것 —
loadbalancingexporter없이 단순히 레플리카 수를 늘리면 동일 트레이스의 스팬이 분산되어 잘못된 샘플링 결정이 내려집니다. invert_match정책을 단독으로 사용하는 것 —invert_match: true는 일치하는 트레이스를 제외할 뿐, 나머지를 샘플링하겠다는 지시가 아닙니다. 반드시 다른 샘플링 정책과 함께 구성해야 합니다.
마치며
헤드 샘플링 비율을 조정하기 전에, 먼저 Tail Sampling의 decision_wait와 메모리 비용을 계산해보는 것이 순서입니다. 샘플링 비율을 낮추는 것보다 중요한 트레이스를 정확히 고르는 것이 더 가치 있으며, decision_wait 단축이 그 출발점입니다.
지금 바로 시작할 수 있는 3단계:
- 서비스의 p99 응답시간을 측정해
decision_wait = p99 × 3공식으로 초기값을 계산하고,TPS × decision_wait × avg_spans × avg_span_size로 필요 메모리를 추정합니다. errors-policy → slow-traces-policy → probabilistic-policy순서의 기본 3단계 복합 정책을 적용하고,memory_limiter를 파이프라인 앞단에 배치합니다.- Prometheus에서
otelcol_processor_tail_sampling_sampling_trace_dropped_too_early_count메트릭을 대시보드에 추가합니다. 이 값이 증가하면num_traces확대 또는decision_wait단축을 검토합니다.
다음 글: Tail Sampling을 Kubernetes 환경에서 KEDA와 연동해 트래픽 급증에 자동으로 대응하는 오토스케일링 아키텍처 설계
참고 자료
- Tail Sampling Processor 공식 README | GitHub — 정책 파라미터 전체 레퍼런스가 필요한 경우
- OpenTelemetry 공식 Sampling 개념 문서 | OpenTelemetry — Head vs. Tail Sampling 개념을 처음 접하는 경우
- Tail Sampling with OpenTelemetry: Why it's useful | OpenTelemetry Blog — 도입 결정의 배경이 필요한 경우
- Tail-Based Sampling: Sizing, Memory Crashes and Cost Model | Michal Drozd — 메모리 OOM 사례와 비용 모델을 깊이 파고싶은 경우
- How to Fix the Collector Memory Leak Caused by Tail Sampling Processor | OneUptime — 실제 메모리 누수 트러블슈팅이 필요한 경우
- How to Right-Size CPU and Memory for the OpenTelemetry Collector | OneUptime — Collector 리소스 사이징 전반을 다루는 경우
- Scale Alloy tail sampling | Grafana OpenTelemetry Docs — Grafana Alloy 환경에서 동일 설정을 구현하는 경우
- otelcol.processor.tail_sampling | Grafana Alloy 공식 문서 — Alloy 컴포넌트 파라미터 레퍼런스
- Scaling the Collector | OpenTelemetry 공식 문서 — 2-tier 아키텍처 설계 원칙을 공식 문서로 확인하는 경우
- Traces at Scale: Head or Tail? Sampling Strategies | DEV Community — 샘플링 전략 선택의 맥락을 더 넓게 살펴보는 경우