OpenTelemetry Collector Tail Sampling으로 에러·지연 트레이스는 100% 살리고 옵저버빌리티 비용을 74% 줄이는 2단계 파이프라인 설계 (OTel Collector)
분산 시스템을 운영하다 보면 트레이싱 데이터가 예상보다 빠르게 쌓이는 상황을 마주하게 됩니다. 트래픽이 늘수록 Jaeger(오픈소스 분산 트레이싱 UI)나 Tempo(Grafana의 트레이스 저장소)로 유입되는 스팬(Span) 수는 선형이 아닌 폭발적으로 늘어나고, 어느 순간 인프라 팀으로부터 "트레이싱 스토리지 비용이 월 $25,000을 넘겼다"는 메시지를 받게 됩니다. 그렇다고 무작정 샘플링 비율을 낮추면 정작 장애가 발생했을 때 에러 트레이스가 사라져 있는 최악의 상황이 연출됩니다. OTel 에코시스템에 처음 접하신다면 OpenTelemetry 공식 Sampling 개념 문서를 먼저 읽어보시면 좋습니다.
이 문제를 해결하는 방법이 바로 OpenTelemetry Collector(이하 OTel Collector)의 tail_sampling processor와 filter processor를 조합한 2단계 파이프라인입니다. 전체 트레이스 중 정상 트레이스가 대부분을 차지할 때 이를 5% 수준으로 줄이면, 비용 절감 상한은 약 74%에 달합니다($25,000 → $6,500, 즉 $18,500 ÷ $25,000 ≈ 74%). 실제 절감 폭은 에러·지연 트레이스의 비중에 따라 달라지며, 이 설계의 핵심 트레이드오프는 메모리 비용이므로 먼저 현재 TPS(초당 트레이스 수)를 측정하는 것이 좋은 출발점이 됩니다.
이 글을 끝까지 읽으시면 20분 내에 동작하는 YAML 구성을 손에 넣게 됩니다. SDK(OpenTelemetry 계측 라이브러리)에서 OTLP(OTel 표준 전송 프로토콜) 형식으로 전송된 트레이스가 Collector를 거쳐 백엔드에 도달하는 전체 흐름을, 기본 구성부터 대용량 스케일링까지 단계별로 살펴보겠습니다.
핵심 개념
Head-Based vs Tail-Based Sampling
샘플링 방식은 크게 두 가지로 나뉩니다.
| 구분 | 결정 시점 | 판단 근거 | 특징 |
|---|---|---|---|
| Head-Based Sampling | 스팬 시작 시 | 확률 또는 컨텍스트 플래그만 가능 | 구현 단순, 에러 트레이스 누락 위험 |
| Tail-Based Sampling | 모든 스팬 수집 후 | 에러 코드·지연시간·속성 등 전체 컨텍스트 | 의미 있는 트레이스만 보존 가능 |
Tail-Based Sampling — 트레이스를 구성하는 모든 스팬이 수집된 이후에 "이 트레이스를 보존할 것인가"를 결정하는 방식. 에러 코드나 지연 시간처럼 트레이스 완료 후에야 알 수 있는 정보를 판단 근거로 활용할 수 있어, 중요한 트레이스만 선별 보존하는 데 적합합니다.
2단계 파이프라인의 구조
전체 아키텍처는 아래와 같이 구성됩니다.
[SDK] → [Layer 1: 게이트웨이 Collector]
└─ loadbalancingexporter (routing_key: traceID)
↓
[Layer 2: 샘플링 Collector ×N]
├─ tail_sampling processor ← 1단계: 트레이스 단위 보존 결정
├─ filter processor ← 2단계: 스팬 단위 추가 필터링
└─ 백엔드 (Jaeger / Tempo / OTLP)1단계 tail_sampling processor는 트레이스 전체를 메모리에 버퍼링한 뒤, 설정된 Policy 집합을 평가해 SAMPLE / DROP 여부를 결정합니다. status_code, latency, probabilistic, composite 등 다양한 정책 타입을 지원하며, 전체 목록은 공식 README에서 버전별로 확인할 수 있습니다.
2단계 filter processor는 OTTL(OpenTelemetry Transformation Language) 조건식으로 스팬 단위 필터링을 수행합니다. tail sampling을 통과한 트레이스 중 /health, /metrics 같은 노이즈성 경로를 추가로 제거하는 역할을 합니다.
OTTL(OpenTelemetry Transformation Language) — OTel Collector의
filter,transformprocessor에서 사용하는 선언적 조건식 언어. SQL의 WHERE 절과 유사한 문법으로 속성 조건, TraceState 접근, 중첩 논리 연산을 표현할 수 있습니다.
핵심 우선순위 원칙
세 가지 정책의 우선순위가 이 설계의 핵심입니다.
에러 트레이스 (status.code = ERROR) → status_code 정책 → 100% 보존
지연 트레이스 (latency > 임계치) → latency 정책 → 100% 보존
정상 트레이스 → probabilistic → 5% 샘플링도입 전 트레이드오프 한눈에 보기
본격적인 예시에 앞서 이 설계의 핵심 트레이드오프를 파악해두시면, 어떤 예시가 자신의 상황에 맞는지 더 쉽게 판단할 수 있습니다.
| 항목 | 내용 |
|---|---|
| 완전한 가시성 보장 | 에러·지연 트레이스 100% 캡처 — 사후 디버깅 데이터 손실 없음 |
| 비용 절감 | 정상 트레이스를 5~10% 수준으로 줄여 스토리지·인제스트 비용 대폭 절감 |
| 높은 메모리 요구량 | decision_wait × TPS만큼 스팬 버퍼링 필요. 20k spans/s 환경에서 2~2.5 GiB 권장 |
| 스테이트풀 구조 | 동일 트레이스의 모든 스팬이 같은 Collector에 도달해야 함 → 로드밸런싱 레이어 필수 |
실전 적용
이제 이 개념을 실제 YAML로 구현해보겠습니다. 예시는 기본 구성에서 시작해 복합 정책, Span Metrics 파생, 대용량 스케일링 순으로 난이도가 높아집니다.
예시 1: 기본 구성 — 에러·지연 100% 보존 + 정상 5% 샘플링
언제 필요한가: 단일 Collector 인스턴스로 충분한 트래픽 규모에서 에러와 느린 트레이스를 확실히 살리고 싶을 때 가장 먼저 적용할 수 있는 구성입니다.
tail_sampling으로 중요 트레이스를 선별하고, filter로 헬스체크 경로를 제거합니다.
processors:
tail_sampling:
decision_wait: 10s # 스팬 수집 대기 시간 (트레이스 완성 기다림)
num_traces: 100000 # 메모리에 유지할 최대 트레이스 수
expected_new_traces_per_sec: 10000
policies:
# 정책 1: 에러 트레이스 100% 보존
- name: keep-errors
type: status_code
status_code:
status_codes: [ERROR]
# 정책 2: 1초 이상 지연 트레이스 100% 보존
- name: keep-slow-traces
type: latency
latency:
threshold_ms: 1000
# 정책 3: 나머지 정상 트레이스 5% 확률 샘플링
- name: sample-normal
type: probabilistic
probabilistic:
sampling_percentage: 5
# 2단계: 헬스체크·메트릭 경로 제거
filter:
error_mode: ignore
traces:
span:
# 이 조건에 해당하는 스팬을 드롭
- |
attributes["http.target"] == "/health" or
attributes["http.target"] == "/metrics"
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, k8sattributes, tail_sampling, filter, batch]
exporters: [otlp/tempo]참고 — Semantic Conventions 버전:
attributes["http.target"]는 OTel Semantic Conventions 1.x 기준입니다. Semconv 2.x부터는http.target이 deprecated되고url.path로 전환됩니다. 최신 SDK를 사용하는 환경이라면attributes["url.path"]로 변경하는 것을 권장합니다.
| 설정 항목 | 역할 | 권장값 |
|---|---|---|
decision_wait |
스팬 완성 대기 시간 | 10~30s (서비스 P99 응답시간 기준) |
num_traces |
메모리 내 최대 트레이스 수 | TPS × decision_wait 이상 |
sampling_percentage |
정상 트레이스 샘플링 비율 | 5~10% |
error_mode: ignore |
필터 평가 오류 시 드롭 방지 | 항상 설정 권장 |
memory_limiterprocessor — OOM(Out of Memory) 방지용 안전망으로, 메모리 사용량이 임계치를 초과하면 신규 스팬 수신을 거부합니다.tail_sampling앞에 배치하는 것이 필수이며,otelcol_processor_refused_spans메트릭으로 드롭 여부를 모니터링할 수 있습니다.GOMEMLIMIT은 컨테이너 메모리 한도의 90%로 설정하는 것을 권장합니다.
주의:
k8sattributes처럼 요청 컨텍스트에 의존하는 프로세서는 반드시tail_sampling앞에 배치해야 합니다.tail_sampling이 스팬을 새 배치로 재조립하면서 원래 컨텍스트가 소실되기 때문입니다.
예시 2: Composite 정책으로 처리량 비율 동적 제어
언제 필요한가: 트래픽 급증 상황에서 초당 스팬 수 상한을 설정하고, 에러·지연·VIP 고객·일반 트레이스 간 처리량 비율을 선언적으로 조정하고 싶을 때 활용합니다.
processors:
tail_sampling:
decision_wait: 10s
num_traces: 100000
policies:
- name: composite-policy
type: composite
composite:
max_total_spans_per_second: 2000 # 초당 최대 스팬 수 상한
policy_order: [errors, slow, vip, baseline]
composite_sub_policy:
- name: errors
type: status_code
status_code: { status_codes: [ERROR] }
- name: slow
type: latency
latency: { threshold_ms: 500 }
- name: vip
type: string_attribute
string_attribute:
key: customer.tier
values: ["enterprise", "premium"]
- name: baseline
type: probabilistic
probabilistic: { sampling_percentage: 5 }
rate_allocation:
# 합계가 반드시 100일 필요는 없으나, 100으로 맞추면 비율 해석이 직관적
- policy: errors
percent: 40
- policy: slow
percent: 30
- policy: vip
percent: 20
- policy: baseline
percent: 10rate_allocation은 max_total_spans_per_second 예산 내에서 각 서브 정책이 사용할 수 있는 스팬 수의 상한 비율을 선언합니다. 상한에 도달했을 때 policy_order에 따라 우선순위가 높은 정책이 먼저 처리됩니다. 세부 동작 방식은 버전별로 다를 수 있으므로, 프로덕션 적용 전 공식 README를 한 번 더 확인하는 것을 권장합니다.
예시 3: Span Metrics를 샘플링 전에 파생하는 파이프라인
언제 필요한가: RED 메트릭(Rate / Error / Duration) 대시보드가 샘플링 이후 수치 왜곡 없이 100% 트래픽 기반으로 집계되기를 원할 때 필요합니다.
spanmetrics connector로 RED 메트릭을 생성할 때는 반드시 샘플링 이전 100% 트래픽에서 집계해야 합니다. 샘플링 이후에 집계하면 에러율·지연 분포 수치가 샘플링 비율만큼 왜곡됩니다.
forward connector는 하나의 파이프라인에서 다른 파이프라인으로 데이터를 그대로 전달하는 브릿지 역할을 합니다. 이를 이용해 동일한 트레이스 스트림을 메트릭 파생과 샘플링 두 갈래로 나눌 수 있습니다.
connectors:
forward: {} # traces/input → traces/sampling으로 데이터를 그대로 전달하는 브릿지
spanmetrics: {} # 스팬에서 RED 메트릭을 파생해 metrics 파이프라인으로 전달
service:
pipelines:
# 수신 파이프라인: 100% 트래픽으로 메트릭 파생
traces/input:
receivers: [otlp]
processors: [memory_limiter, k8sattributes]
exporters: [spanmetrics, forward]
# 샘플링 파이프라인: 메트릭 파생 이후 필터링
traces/sampling:
receivers: [forward]
processors: [tail_sampling, filter, batch]
exporters: [otlp/tempo]
# 메트릭 파이프라인
metrics:
receivers: [spanmetrics]
exporters: [prometheus]예시 4: 대용량 환경을 위한 2계층 스케일링 아키텍처
언제 필요한가: 단일 Collector의 메모리 한계를 넘는 트래픽(수만 TPS 이상)이 발생하거나, 여러 Collector 인스턴스를 수평 확장해야 할 때 필요합니다.
트래픽이 높으면 동일 트레이스의 스팬이 여러 Collector에 분산될 수 있습니다. 이 경우 tail_sampling이 트레이스의 일부 스팬만 보고 결정을 내리게 되어 결과가 불완전해집니다. loadbalancingexporter는 traceID를 라우팅 키로 사용해 동일 트레이스의 모든 스팬을 항상 같은 Layer 2 Collector로 보내주는 역할을 합니다.
# Layer 1: 게이트웨이 Collector — traceID 기반 일관 라우팅
exporters:
loadbalancing:
routing_key: traceID
protocol:
otlp:
tls: { insecure: true }
resolver:
dns:
hostname: otel-sampling-collector-headless.monitoring.svc.cluster.local
port: 4317
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [loadbalancing]# Layer 2: 샘플링 Collector — 예시 1 또는 예시 2의 파이프라인 구성을 그대로 적용
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, k8sattributes, tail_sampling, filter, batch]
exporters: [otlp/tempo]동일한 traceID를 가진 모든 스팬이 항상 같은 Layer 2 Collector로 전달되므로 tail sampling 결정의 일관성이 보장됩니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 완전한 가시성 보장 | 에러·지연 트레이스를 100% 캡처하여 사후 디버깅에 필요한 데이터 손실 없음 |
| 비용 절감 | 정상 트레이스를 5~10% 수준으로 줄여 스토리지·인제스트 비용 대폭 절감 (예: $25,000 → $6,500/월) |
| 풍부한 정책 표현력 | 다양한 정책 타입 + OTTL 조건식으로 복잡한 비즈니스 로직 구현 가능 |
| 선언적 구성 | YAML 기반으로 코드 재배포 없이 샘플링 비율 변경 가능 |
| 장애 격리 | 샘플링 레이어 장애가 게이트웨이 레이어에 전파되지 않아 안정성 향상 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 높은 메모리 요구량 | decision_wait × TPS만큼 스팬 버퍼링 필요 |
20k spans/s 환경에서 2~2.5 GiB 권장, GOMEMLIMIT을 컨테이너 한도의 90%로 설정 |
| 스테이트풀 구조 | 동일 트레이스의 모든 스팬이 같은 Collector에 도달해야 함 | loadbalancingexporter로 traceID 기반 라우팅 필수 (예시 4 참조) |
decision_wait 딜레마 |
짧으면 스팬 누락 위험, 길면 메모리 급증 | 통상 10~30s 권장, 서비스 P99 응답시간 기준으로 설정 |
| 콜드 스타트 불완전성 | Collector 재시작 직후 도착한 트레이스는 불완전한 상태로 평가됨 | Rolling restart 방식으로 인스턴스 순차 재시작 |
| 컨텍스트 손실 | tail_sampling 이후 컨텍스트 의존 프로세서 사용 불가 |
k8sattributes 등은 반드시 tail_sampling 앞에 배치 |
실무에서 가장 흔한 실수
k8sattributes를tail_sampling뒤에 배치 — 재조립된 배치에는 원래 요청 컨텍스트가 없어 Kubernetes 메타데이터 enrichment가 동작하지 않습니다. 프로세서 순서를 반드시memory_limiter → k8sattributes → tail_sampling순으로 유지하는 것을 권장합니다.num_traces를 기본값으로 방치 —expected_new_traces_per_sec × decision_wait이상으로 반드시 설정해야 합니다. 부족하면 오래된 트레이스가 조기 드롭되어 에러 트레이스가 함께 사라질 수 있습니다.- Span Metrics를 샘플링 이후 파이프라인에서 집계 — RED 메트릭이 샘플링 비율만큼 축소되어 잘못된 에러율·지연 분포가 대시보드에 표시됩니다. 예시 3처럼 반드시 샘플링 전 파이프라인에서 집계해야 합니다.
마치며
이 설계의 핵심 트레이드오프는 메모리 비용이므로, 먼저 현재 TPS를 측정하고 num_traces를 적절히 설정하는 것이 성공적인 도입의 출발점이 됩니다.
지금 바로 시작해볼 수 있는 3단계:
- 현재 트레이스 볼륨 파악 —
otelcol_receiver_accepted_spans메트릭으로 초당 스팬 수(TPS)를 확인하고,num_traces = TPS × 30을 초기값으로 설정해보시면 좋습니다. - 기본 파이프라인 배포 — 예시 1의 YAML을 기반으로
sampling_percentage: 100으로 시작해 트레이스 손실 없이 파이프라인이 정상 동작하는지 검증한 뒤, 비율을 점진적으로 낮추는 방식을 권장합니다. 샘플링 비율을 낮춘 후otelcol_processor_refused_spans가 증가한다면num_traces를 늘려야 한다는 신호입니다. - Composite 정책으로 고도화 — 비용 절감 목표가 확인되면 예시 2의
composite정책을 도입해 에러·VIP·일반 트레이스의 처리량 비율을 서비스 특성에 맞게 조정해보시면 됩니다.
다음 글: OTel Collector의
spanmetricsconnector와 Prometheus를 연결해 샘플링 전 100% 트래픽 기반 RED 대시보드를 구성하는 방법
참고 자료
- OpenTelemetry 공식 – Sampling 개념
- Tail Sampling Processor README (opentelemetry-collector-contrib)
- Filter Processor README (opentelemetry-collector-contrib)
- OpenTelemetry 공식 블로그 – Tail Sampling 소개
- OpenTelemetry 공식 블로그 – 2025 Sampling Milestones
- Scaling the Collector (공식 문서)
- Loadbalancing Exporter README
- TraceState Probability Sampling 명세
- Adaptive Tail-Based Sampling with Dynamic Trace Enrichment (Medium)
- Tail-Based Sampling: Sizing, Memory & Cost Model – Michal Drozd
- Grafana Alloy – tail_sampling 컴포넌트 레퍼런스
- Grafana Tempo – Tail Sampling 정책·전략
- Mastering the OpenTelemetry Filter Processor – Dash0
- Composing OTel Reference Architectures – Elastic
- Scale Alloy Tail Sampling – Grafana 문서