OpenTelemetry Collector Tail-based Sampling: 에러·느린 요청을 100% 보존하고 저장 비용 70%를 줄이는 방법
분산 시스템에서 트레이싱을 도입했는데도 정작 중요한 에러나 느린 요청의 트레이스가 샘플링에서 탈락해버린 경험, 한 번쯤 있으실 겁니다. 전통적인 Head-based Sampling은 첫 번째 스팬이 도착하는 순간 보존 여부를 결정하기 때문에, 그 요청이 나중에 에러를 일으키거나 500ms를 초과하는지 알 방법이 없습니다. 결과적으로 운영 중 가장 중요한 트레이스가 무작위로 버려지는 상황이 발생합니다.
OpenTelemetry Collector의 Tail-based Sampling 프로세서를 올바르게 설정하면, 에러와 느린 요청 트레이스를 100% 보존하면서도 정상 트레이스 저장 비용을 70% 이상 절감할 수 있습니다. (AWS ADOT 운영 사례 기준 — 자세한 내용은 본문 '예시 1'에서 다룹니다.) 이 글에서는 tailsamplingprocessor의 핵심 동작 원리부터, 프로덕션에서 검증된 정책 조합, 그리고 실무에서 자주 빠지는 함정까지 단계적으로 살펴봅니다.
이 글을 읽고 나면 에러 100% 보존 정책, 레이턴시 기반 필터링, 서비스별 차등 샘플링을 직접 설정할 수 있게 됩니다. 이 글은 OTel 기본 개념과 YAML 설정에 익숙한, 분산 시스템 트레이싱 경험이 있는 개발자를 대상으로 합니다. OTel Collector를 처음 접하신다면 공식 Getting Started 문서를 먼저 살펴보시는 것을 권장합니다.
핵심 개념
Head-based Sampling vs Tail-based Sampling
샘플링 전략의 핵심 차이는 언제 결정을 내리느냐입니다.
| 구분 | 결정 시점 | 에러 보존 | 레이턴시 기반 판단 |
|---|---|---|---|
| Head-based | 첫 스팬 도착 시 | 불가 | 불가 |
| Tail-based | 트레이스 완성 후 | 가능 (100%) | 가능 |
Tail-based Sampling: 트레이스를 구성하는 모든 스팬이 수집된 뒤, 트레이스 전체 맥락(에러 여부, 총 소요 시간 등)을 검토해 보존 여부를 결정하는 샘플링 전략입니다.
OTel Collector의 tailsamplingprocessor는 contrib 저장소에 포함된 프로세서로, decision_wait 동안 트레이스를 메모리에 버퍼링한 뒤 설정된 정책을 평가합니다. 13가지 이상의 정책 타입을 OR/AND/composite 방식으로 조합해 세밀한 규칙을 정의할 수 있습니다.
decision_wait과 레이턴시 임계값의 관계
tailsamplingprocessor를 설정할 때 가장 중요하게 이해해야 할 관계가 decision_wait과 threshold_ms의 상호작용입니다.
decision_wait: Collector가 트레이스의 모든 스팬 도착을 기다리는 시간. 이 시간이 지나면 수집된 스팬을 기준으로 정책을 평가해 보존 여부를 결정합니다.threshold_ms: 레이턴시 정책의 임계값. 전체 트레이스 소요 시간이 이 값을 초과하면 보존합니다.
핵심은 decision_wait이 threshold_ms보다 짧으면 레이턴시 기반 판정이 불완전해질 수 있다는 점입니다. 예를 들어 threshold_ms: 2000(2초 초과 트레이스 보존)이면 decision_wait은 최소 2초 이상, 가능하면 3~4초 이상으로 설정해야 합니다. 반대로 decision_wait: 30s로 설정하면 500ms에 완료된 빠른 트레이스도 30초 동안 메모리에 머무른다는 점도 함께 고려해야 합니다.
정책 평가 방식: OR와 AND 조합
정책은 기본적으로 OR 방식으로 동작합니다. 하나의 정책이라도 sample 판정을 내리면 해당 트레이스는 보존됩니다. 특정 조건을 동시에 만족해야 할 때는 and 타입 정책으로 묶으면 됩니다.
# AND 조건 예시: payment-service 또는 checkout-service의 에러만 선별적으로 보존
processors:
tail_sampling:
decision_wait: 30s
num_traces: 100000
policies:
- name: critical-service-errors
type: and
and:
and_sub_policy:
- name: service-filter
type: string_attribute
string_attribute:
key: service.name
values: [payment-service, checkout-service]
- name: error-filter
type: status_code
status_code: {status_codes: [ERROR]}왜 지금 Tail-based Sampling인가 — 2025년 생태계 변화
2025년 OTel 생태계에는 Tail Sampling의 활용을 더욱 강화하는 중요한 변화가 있었습니다.
- W3C TraceContext Level 2 표준 채택: TraceID 하위 56비트를 랜덤 값으로 보장하고,
tracestate헤더의ot키에 샘플링 임계값(th)을 인코딩하는 방식이 표준화되었습니다. - ConsistentProbabilityBased 샘플러 도입: 분산 시스템에서 Head/Tail 샘플러 간 결정 일관성을 수학적으로 보장합니다. Head Sampling에서 이미 드롭된 트레이스를 Tail 단에서 중복 처리하는 비효율을 줄여줍니다.
ottl_condition정책 성숙: OTTL(OpenTelemetry Transformation Language) 표현식으로 복잡한 조건을 코드 없이 설정할 수 있습니다.
# OTTL 조건 예시: 에러이면서 1초를 초과하는 스팬
- name: ottl-error-slow
type: ottl_condition
ottl_condition:
error_mode: ignore
span:
- |
span.status.code == STATUS_CODE_ERROR and
span.duration > Duration("1s")실전 적용
예시 1: 에러·느린 요청 보존 + 비용 최적화 (AWS ADOT 패턴)
AWS Distro for OpenTelemetry(ADOT) 환경에서 에러와 느린 요청 트레이스를 보존하고, 정상 트레이스는 5%만 유지해 스토리지 비용을 70% 이상 절감한 패턴입니다.
UNSET상태 코드 주의:status_codes: [ERROR, UNSET]처럼 UNSET을 함께 지정하면, 상태를 명시적으로 설정하지 않은 정상 스팬 대부분이 함께 보존됩니다. UNSET은 스팬의 기본값이므로, 에러 트레이스만 보존하려면ERROR만 사용하는 것을 권장합니다.
processors:
memory_limiter: # Tail Sampling은 메모리를 많이 소모하므로 필수
check_interval: 1s
limit_mib: 2048
spike_limit_mib: 512
tail_sampling:
decision_wait: 30s # 최대 트레이스 완성 시간을 고려해 충분히 설정
num_traces: 100000
expected_new_traces_per_sec: 500
policies:
- name: keep-errors
type: status_code
status_code: {status_codes: [ERROR]} # UNSET 제외, 명시적 에러만 보존
- name: keep-slow
type: latency
latency: {threshold_ms: 1000} # decision_wait(30s) > threshold(1s)이므로 안전
- name: sample-rest
type: probabilistic
probabilistic: {sampling_percentage: 5}
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, tail_sampling]
exporters: [otlp/backend]| 정책 | 타입 | 역할 |
|---|---|---|
keep-errors |
status_code |
ERROR 상태 코드 트레이스 전량 보존 |
keep-slow |
latency |
1초 초과 트레이스 전량 보존 |
sample-rest |
probabilistic |
나머지 정상 트레이스 5% 무작위 샘플링 |
예시 2: 서비스별 차등 샘플링 (composite 정책)
결제 서비스와 일반 API를 다른 비율로 샘플링하되 에러와 느린 요청은 항상 보존하는 패턴입니다.
rate_allocation.percent의미: 이 값은 샘플링 확률이 아닙니다.max_total_spans_per_second대역폭 중 각 정책에 할당하는 처리 비율입니다. 실제 샘플링 비율은 각 서브 정책 내부의probabilistic설정으로 제어합니다.
processors:
memory_limiter:
check_interval: 1s
limit_mib: 2048
spike_limit_mib: 512
tail_sampling:
decision_wait: 30s
num_traces: 100000
policies:
- name: composite-policy
type: composite
composite:
max_total_spans_per_second: 10000
policy_order:
- errors-policy
- slow-traces-policy
- payment-policy
- default-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: payment-policy # payment-service 트레이스를 50% 샘플링
type: and
and:
and_sub_policy:
- name: service-match
type: string_attribute
string_attribute:
key: service.name
values: [payment-service]
- name: payment-rate
type: probabilistic
probabilistic: {sampling_percentage: 50}
- name: default-policy # 나머지 모든 트레이스는 5% 샘플링
type: probabilistic
probabilistic: {sampling_percentage: 5}
rate_allocation:
- policy: errors-policy
percent: 30 # 전체 처리량의 30%를 에러 정책에 할당
- policy: slow-traces-policy
percent: 30
- policy: payment-policy
percent: 20 # 전체 처리량의 20%를 결제 서비스 정책에 할당
- policy: default-policy
percent: 20
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, tail_sampling]
exporters: [otlp/backend]예시 3: 수평 확장 아키텍처 (Grafana Labs 패턴)
단일 Collector 인스턴스의 처리 한계를 넘어설 때(초당 14,000스팬 이상)는 이중 레이어 아키텍처를 적용합니다. Grafana Labs가 자체 인프라에서 검증한 구조입니다.
[애플리케이션]
│ OTLP
▼
[1계층 Collector — loadbalancingexporter]
│ traceID 기반 일관 라우팅
▼
[2계층 Collector 클러스터 — tail_sampling processor]
│
▼
[Grafana Tempo / 백엔드]loadbalancingexporter: traceID를 키로 사용해 동일한 traceID의 모든 스팬을 항상 같은 2계층 Collector 인스턴스로 라우팅합니다. 이 컴포넌트 없이 단순히 Collector를 수평 확장하면, 동일 traceID의 스팬이 여러 인스턴스에 흩어져 불완전한 트레이스가 판정됩니다.
# 1계층 Collector: traceID 기반 라우팅만 담당 (tail_sampling 없음)
exporters:
loadbalancing:
protocol:
otlp:
tls:
insecure: true
resolver:
dns:
hostname: otelcol-tail-sampling.tracing.svc.cluster.local
port: 4317
service:
pipelines:
traces:
receivers: [otlp]
exporters: [loadbalancing]# 2계층 Collector: 실제 Tail Sampling 처리
processors:
memory_limiter:
check_interval: 1s
limit_mib: 4096 # 2계층은 트레이스 버퍼링 부담이 크므로 넉넉하게 확보
spike_limit_mib: 1024
tail_sampling:
decision_wait: 30s
num_traces: 100000
expected_new_traces_per_sec: 500
policies:
- name: keep-errors
type: status_code
status_code: {status_codes: [ERROR]}
- name: keep-slow
type: latency
latency: {threshold_ms: 1000}
- name: sample-rest
type: probabilistic
probabilistic: {sampling_percentage: 5}
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, tail_sampling]
exporters: [otlp/backend]장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 에러 100% 캡처 | 트레이스 완료 후 status_code = ERROR 여부를 확인하므로 Head Sampling으로는 불가능한 완전 보존 가능 |
| 느린 요청 정밀 포착 | 전체 트레이스 duration 측정 후 임계값 초과 여부를 판단해 느린 요청만 선별 |
| 비용 최적화 | 정상 트레이스를 낮은 비율(5~10%)로 샘플링해 저장 비용 대폭 절감 |
| 다양한 정책 조합 | 13+ 정책 타입, AND/OR/composite 조합으로 서비스별 세밀한 제어 |
| OTTL 조건 지원 | 2025년 성숙한 ottl_condition으로 코드 없이 복잡한 표현식 기반 필터링 가능 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 메모리 사용량 | num_traces × 트레이스 크기만큼 RAM 점유 (10만 트레이스 × 20KB ≈ 2GB) |
memory_limiter 프로세서 필수 적용, num_traces 조정 |
| Late Span 드롭 | decision_wait 이후 도착한 스팬은 무조건 드롭 |
decision_wait을 예상 최대 트레이스 duration보다 충분히 크게 설정 |
| 수평 확장 제약 | 동일 traceID의 스팬이 여러 인스턴스에 분산되면 불완전 판정 | loadbalancingexporter + 이중 레이어 아키텍처 적용 |
| 운영 복잡도 | 정책이 많아질수록 디버깅 어려움 | otelcol_processor_tail_sampling_* 메트릭 모니터링 |
Late Span: 네트워크 지연, 비동기 처리, 큐 대기 등으로 인해 트레이스의 다른 스팬보다 훨씬 늦게 도착하는 스팬입니다.
decision_wait설정이 너무 짧으면 이런 스팬들이 판정 완료 후 도착해 버려집니다.
실무에서 가장 흔한 실수
-
decision_wait을 레이턴시 임계값보다 짧게 설정:threshold_ms: 2000이지만decision_wait: 1s로 설정하면 2초짜리 느린 트레이스가 판정 전에 드롭됩니다.decision_wait은 예상 최대 트레이스 완성 시간의 1.5~2배로 설정하는 것을 권장합니다. -
loadbalancingexporter없이 수평 확장 시도: Tail Sampling Collector를 단순히 여러 인스턴스로 복제하면, 동일 traceID의 스팬이 여러 인스턴스에 흩어져 불완전한 트레이스로 판정됩니다. 반드시 이중 레이어 아키텍처를 적용해야 합니다. -
num_traces과소 설정: 트래픽 급증 시num_traces한도를 초과하면 오래된 트레이스부터 드롭됩니다. 피크 트래픽 기준으로decision_wait(초) × 초당_트레이스_수 × 1.5이상으로 설정하는 것이 좋습니다.
마치며
Tail-based Sampling을 올바르게 적용하면, 이제 운영 중 가장 중요한 에러와 느린 요청 트레이스를 하나도 놓치지 않으면서 저장 비용은 대폭 낮출 수 있습니다. 관측 가능성 파이프라인을 비용과 신뢰성 두 마리 토끼를 잡는 방향으로 제어할 수 있는 기반이 마련됩니다.
지금 바로 시작해볼 수 있는 3단계:
-
OTel Collector contrib 이미지로 교체: 기존
otel/opentelemetry-collector이미지를otel/opentelemetry-collector-contrib로 변경합니다.tailsamplingprocessor는 contrib 저장소에만 포함되어 있습니다. 프로덕션에서는latest태그 대신 버전을 고정해서 사용하는 것을 권장합니다(예:otel/opentelemetry-collector-contrib:0.100.0). -
최소 정책 3개로 시작: 예시 1(에러 보존 + 레이턴시 보존 + 5% 랜덤)처럼 단순한 구성부터 적용한 뒤,
otelcol validate --config=config.yaml로 설정을 검증해 보시면 좋습니다. -
샘플링 메트릭 대시보드 구성: 아래 메트릭을 Prometheus + Grafana로 시각화해 실제 보존율을 확인하는 것을 권장합니다.
| 메트릭 이름 | 의미 |
|---|---|
otelcol_processor_tail_sampling_count_traces_sampled |
정책별 보존된 트레이스 수 |
otelcol_processor_tail_sampling_sampling_decision_timer_latency |
정책 평가에 걸리는 시간 |
otelcol_processor_tail_sampling_late_release_decisions |
Late Span으로 인한 드롭 수 |
다음 글: OTel Collector 파이프라인에서
filterprocessor와transformprocessor를 조합해 수집 전 불필요한 스팬을 사전에 제거하고, 스팬 속성 값의 고유한 조합 수(카디널리티)가 지나치게 높아져 저장·쿼리 비용이 급증하는 문제를 제어하는 방법을 다룰 예정입니다.
참고 자료
- Tail Sampling Processor README | GitHub (open-telemetry/opentelemetry-collector-contrib)
- Sampling 개념 | OpenTelemetry 공식 문서
- Tail Sampling with OpenTelemetry: Why it's useful | OpenTelemetry Blog
- OpenTelemetry Sampling Milestones 2025 | OpenTelemetry Blog
- Tail sampling | Grafana OpenTelemetry 문서
- Scale Alloy tail sampling | Grafana 문서
- Add tail sampling policies and strategies | Grafana Tempo 문서
- How Grafana Labs enables horizontally scalable tail sampling | Grafana Blog
- Scaling the Collector | OpenTelemetry 공식 문서
- Getting Started with Advanced Sampling | AWS ADOT
- otelcol.processor.tail_sampling | Grafana Alloy 문서