에러 스팬 100% 수집, 비용 최대 95% 절감 — Grafana Alloy + OpenTelemetry Tail-Based Sampling 실전 가이드
분산 시스템을 운영하다 보면 언제나 같은 딜레마에 맞닥뜨립니다. 트레이스를 전부 저장하자니 비용이 감당이 안 되고, 무작위로 샘플링하자니 정작 필요한 에러 트레이스가 누락될까 걱정됩니다. 이 문제는 단순한 비용 이슈를 넘어 장애 대응 능력 자체에 영향을 주는 핵심 아키텍처 결정입니다.
Grafana Tempo의 tail-based sampling(테일 기반 샘플링) 은 이 딜레마를 근본적으로 해결합니다. 트레이스가 완전히 수집된 이후에 저장 여부를 판단하므로, "에러가 포함된 트레이스는 100% 보존, 정상 트레이스는 5%만 보존"과 같은 조건부 필터링이 가능합니다. 정책을 올바르게 설계하면 에러 트레이스를 한 건도 놓치지 않으면서 전체 스토리지 사용량을 최대 95%까지 줄일 수 있습니다.
이 글은 Grafana Alloy + Tempo 파이프라인이 이미 구성된 환경을 가정합니다. OTel SDK 계측이나 Alloy 초기 설치는 Grafana Alloy 공식 문서와 Grafana Tempo 공식 문서를 참고하세요. 이미 파이프라인이 있고 샘플링 전략을 고도화하려는 백엔드·인프라 엔지니어라면 이 글이 바로 도움이 됩니다. otelcol.processor.tail_sampling 컴포넌트 설정부터 고트래픽 메모리 튜닝, 프로덕션 적용 전 운영 체크리스트까지 실무에 바로 적용할 수 있는 내용을 담았습니다.
핵심 개념
Head-Based vs Tail-Based Sampling: 무엇이 다른가
트레이스 샘플링에는 크게 두 가지 방식이 있습니다.
| 방식 | 결정 시점 | 에러 스팬 보존 | 구현 복잡도 |
|---|---|---|---|
| Head-based sampling | 트레이스 시작 시점 | 보장 불가 (에러 발생 전에 결정) | 낮음 |
| Tail-based sampling | 트레이스 완료 후 | 100% 보장 가능 | 중간 |
Head-based sampling은 트레이스가 시작될 때 저장 여부를 확률적으로 결정합니다. 구현이 단순하지만, 나중에 에러가 발생하더라도 이미 드롭 결정이 내려진 트레이스는 잃게 됩니다. Tail-based sampling은 모든 스팬이 수집된 뒤에 판단하므로 트레이스 전체 내용을 보고 보존 여부를 결정할 수 있습니다.
용어 정리 — 스팬(Span): 분산 트레이스에서 하나의 작업 단위(예: HTTP 요청, DB 쿼리)를 나타내는 데이터. 여러 스팬이 모여 하나의 **트레이스(Trace)**를 구성합니다.
Grafana Alloy + Tempo 아키텍처 전체 흐름
애플리케이션 (OTel SDK)
└─▶ Grafana Alloy
├─ otelcol.processor.tail_sampling
│ ├─ Policy 1: status_code = ERROR → 100% 보존
│ ├─ Policy 2: latency > 2000ms → 100% 보존
│ └─ Policy 3: probabilistic 5% → 나머지 5% 보존
└─▶ Grafana Tempo (S3 / GCS / MinIO)
└─▶ Grafana 대시보드 (TraceQL)Grafana Alloy는 구 Grafana Agent Flow 모드의 공식 후속 제품으로, otelcol.processor.tail_sampling 컴포넌트가 tail sampling의 표준 구성 방식입니다. Tempo는 오브젝트 스토리지(S3, GCS, MinIO 등)를 백엔드로 사용해 Jaeger나 Zipkin 대비 인덱스 저장 비용을 대폭 낮춥니다.
세 가지 핵심 정책 유형
| 정책 유형 | 역할 | 주요 파라미터 |
|---|---|---|
status_code |
OTel 상태 코드 기준 필터 | status_codes: ["ERROR"] |
latency |
트레이스 전체 지연 시간 기준 필터 | threshold_ms: 2000 |
probabilistic |
확률 기반 무작위 샘플링 | sampling_percentage: 5 |
핵심 규칙 —
probabilistic정책은 반드시 마지막에 배치해야 합니다. 앞에 위치하면 에러 트레이스가 확률 판단에 의해 드롭될 수 있습니다. Grafana 공식 문서도 이를 명시적으로 요구합니다.
decision_wait과 불완전한 트레이스 처리
Tail sampling의 근본적인 제약은 트레이스의 마지막 스팬이 도착해야 판단이 시작된다는 점입니다. decision_wait 시간이 지나도 스팬이 완결되지 않은 트레이스는 수집된 스팬만으로 정책을 평가하며, 어떤 보존 정책에도 해당하지 않으면 드롭됩니다. 따라서 decision_wait은 서비스 내 가장 긴 트레이스의 예상 완료 시간보다 충분히 크게 설정하는 것이 중요합니다.
num_traces는 decision_wait 동안 메모리에 유지할 최대 트레이스 수로, 이 값이 너무 낮으면 아직 평가가 완료되지 않은 트레이스가 조기에 드롭됩니다.
계산 공식 —
num_traces≥초당 트레이스 수×decision_wait(초)예: 초당 500 트레이스,decision_wait = "10s"→num_traces최솟값 5,000
이제 이 세 가지 정책을 조합해 실제 설정을 구성해 보겠습니다.
실전 적용
예시 1: 에러 100% + 정상 트레이스 저율 샘플링 (기본 패턴)
가장 일반적인 구성입니다. 에러와 느린 트레이스는 전량 보존하고, 나머지 정상 트레이스는 5%만 남깁니다.
// Grafana Alloy — config.alloy
// output 블록에서 참조하는 OTLP 익스포터 선언이 별도로 필요합니다.
// otelcol.exporter.otlp "tempo" {
// client { endpoint = "tempo:4317" }
// }
otelcol.processor.tail_sampling "default" {
decision_wait = "10s"
num_traces = 10000
expected_new_traces_per_sec = 1000
// 정책 1: 에러 스팬 포함 트레이스 100% 보존
policy {
name = "keep-errors"
type = "status_code"
status_code {
status_codes = ["ERROR"]
}
}
// 정책 2: 2초 초과 느린 트레이스 전량 보존
policy {
name = "keep-slow-traces"
type = "latency"
latency {
threshold_ms = 2000
}
}
// 정책 3: 나머지 5% 무작위 샘플링 — 반드시 마지막
policy {
name = "probabilistic-sample"
type = "probabilistic"
probabilistic {
sampling_percentage = 5
}
}
output {
traces = [otelcol.exporter.otlp.tempo.input]
}
}| 파라미터 | 설명 | 설정 기준 |
|---|---|---|
decision_wait |
스팬 수집 완료까지 대기 시간 | 서비스 내 가장 긴 트레이스 예상 완료 시간 이상 |
num_traces |
메모리에 유지할 최대 트레이스 수 | 예상 동시 활성 트레이스 수 이상 (핵심 개념 섹션의 계산 공식 참고) |
expected_new_traces_per_sec |
초당 신규 트레이스 예상 수 | 실제 트래픽 기준으로 측정 후 설정 |
예시 2: 헬스체크 완전 제외 + 에러 100% 수집
/health, /readyz 같은 헬스체크 엔드포인트 트레이스는 대부분 노이즈입니다. 이를 먼저 제거하면 저장 비용을 추가로 절감할 수 있습니다.
// Grafana Alloy — config.alloy (헬스체크 제외 패턴)
otelcol.processor.tail_sampling "default" {
decision_wait = "10s"
num_traces = 10000
expected_new_traces_per_sec = 1000
// 정책 1: 헬스체크 경로와 일치하지 않는 트레이스만 보존 대상으로 평가
policy {
name = "drop-healthcheck"
type = "string_attribute"
string_attribute {
key = "http.target"
values = ["/health", "/readyz", "/livez"]
invert_match = true
}
}
// 정책 2: 에러 트레이스 100% 보존
policy {
name = "keep-errors"
type = "status_code"
status_code {
status_codes = ["ERROR"]
}
}
// 정책 3: 나머지 10% 샘플링 — 반드시 마지막
policy {
name = "baseline"
type = "probabilistic"
probabilistic {
sampling_percentage = 10
}
}
output {
traces = [otelcol.exporter.otlp.tempo.input]
}
}
invert_match = true— 지정한 값과 일치하지 않는 트레이스를 보존 대상으로 평가합니다./health등과 일치하는 트레이스는 어떤 보존 정책에도 해당하지 않아 결과적으로 드롭됩니다. 직접 드롭을 명령하는 것이 아니라, 보존 조건에서 제외되는 방식으로 동작한다는 점에 유의하세요.
예시 3: Composite Policy로 고트래픽 속도 제한 적용
초당 트레이스 수가 수만 건에 달하는 환경에서는 composite 정책으로 초당 최대 스팬 수를 제한하면서 에러를 우선 보존할 수 있습니다. 트래픽이 초당 수천 건 이하라면 예시 1로 충분하므로 이 설정이 필요하지 않을 수 있습니다.
아래 예시는 OTel Collector contrib의 YAML 설정 형식입니다. 중첩 서브 정책과 rate_allocation이 함께 필요한 composite 구성은 OTel Collector YAML로 작성한 뒤 Alloy 파이프라인과 연결하는 방식이 실용적입니다.
# OpenTelemetry Collector — config.yaml (composite 정책)
processors:
tail_sampling:
decision_wait: 10s
num_traces: 50000
policies:
- name: error-policy
type: status_code
status_code:
status_codes: [ERROR]
- name: composite-policy
type: composite
composite:
max_total_spans_per_second: 10000 # 초당 최대 스팬 수 상한
policy_order: [error-policy, latency-policy, probabilistic-policy]
composite_sub_policy:
- name: latency-policy
type: latency
latency:
threshold_ms: 500
- name: probabilistic-policy
type: probabilistic
probabilistic:
sampling_percentage: 3
rate_allocation:
- policy: error-policy
percent: 60 # 허용 처리량의 60%를 에러 트레이스에 우선 배정
- policy: latency-policy
percent: 30
- policy: probabilistic-policy
percent: 10
rate_allocation의 역할 —max_total_spans_per_second로 전체 처리량에 상한이 생겼을 때, 각 정책에 용량을 어떻게 나눌지를 결정합니다.error-policy에 60%를 배정하면 트래픽이 폭증해 처리량 상한에 도달하더라도 에러 트레이스가 우선 수집됩니다.status_code = ERROR정책 자체는 매칭 트레이스를 항상 보존하지만, 처리량 상한 환경에서 에러 트레이스의 우선순위를 보장하기 위해 rate allocation이 필요합니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 에러 완전 수집 | 트레이스 완료 후 판단하므로 에러 스팬이 있는 트레이스 100% 보존 가능 |
| 비용 최적화 | 정상 트레이스 저율 샘플링으로 스토리지 비용 최대 95% 절감 |
| 비즈니스 가치 보존 | 단순 확률 샘플링 대비 의미 있는 트레이스(에러, 이상 지연) 선별 보존 |
| Tempo와의 시너지 | 오브젝트 스토리지 기반이라 샘플링된 트레이스도 저렴하게 장기 보관 가능 |
| Adaptive Traces 연계 | Grafana Cloud에서 관리형 UI로 커스텀 정책 관리 가능 (2025년 GA) |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 메모리 압박 | decision_wait 동안 모든 스팬을 메모리에 보유 → 고트래픽 시 OOM 위험 |
num_traces × 평균 스팬 크기를 기준으로 Alloy 메모리 리밋을 넉넉히 설정 |
| 단일 인스턴스 제약 | 동일 트레이스의 스팬이 반드시 동일 Alloy 인스턴스로 라우팅되어야 함 | otelcol.exporter.loadbalancing으로 트레이스 ID 기반 라우팅 구성 |
| 정책 순서 오류 | probabilistic이 앞에 있으면 에러 트레이스도 드롭될 수 있음 |
항상 status_code → latency → probabilistic 순서 유지 |
| decision_wait 튜닝 | 너무 짧으면 불완전한 트레이스로 판단, 너무 길면 메모리 사용 증가 | 가장 긴 트레이스의 예상 완료 시간을 기준으로 설정 |
용어 보충 —
otelcol.exporter.loadbalancing: 여러 Alloy 인스턴스가 있을 때 동일한 트레이스 ID를 가진 스팬들이 항상 같은 인스턴스로 전달되도록 보장하는 익스포터입니다. Tail sampling의 스케일아웃에 필수입니다.
실무에서 가장 흔한 실수
probabilistic정책을 맨 앞에 배치 — 에러 트레이스가 확률 판단에 의해 드롭됩니다. 반드시 마지막 정책으로 이동시켜야 합니다.num_traces를 너무 낮게 설정 — 예상 동시 활성 트레이스 수보다 낮으면 아직 평가가 완료되지 않은 트레이스가 조기에 드롭됩니다.- 로드밸런싱 없이 다중 Alloy 인스턴스 구성 — 동일 트레이스의 스팬이 다른 인스턴스로 분산되면 tail sampling 판단이 불완전해집니다.
loadbalancing익스포터를 Alloy 앞단에 배치해야 합니다.
마치며
Tail-based sampling은 "에러는 하나도 놓치지 않으면서 비용은 최소화"라는 두 목표를 동시에 달성할 수 있는 현재 가장 실용적인 트레이스 전략입니다.
지금 바로 시작할 수 있는 3단계:
-
현재 트래픽을 측정합니다.
초당 트레이스 수와평균 트레이스 완료 시간을 파악합니다.- 예: 초당 500 트레이스, 완료 시간 8초 →
num_traces = 5000,decision_wait = "10s"
-
예시 1의 기본 패턴을 스테이징 환경에 적용합니다.
status_code(ERROR) → latency(2000ms) → probabilistic(5%)순서로 정책을 배치합니다.- Alloy의
tail_sampling_count_traces_sampled메트릭으로 실제 보존·드롭 비율을 확인합니다.
-
프로덕션 적용 전 아래 체크리스트를 점검합니다.
프로덕션 적용 전 운영 체크리스트
✅ 정책 순서 확인: status_code → latency → probabilistic
✅ num_traces ≥ 초당 트레이스 수 × decision_wait(초)
✅ decision_wait ≥ 서비스 내 가장 긴 트레이스 완료 예상 시간
✅ Alloy 메모리 리밋: num_traces × 평균 스팬 크기 이상으로 설정
✅ Alloy 인스턴스가 2개 이상이라면 otelcol.exporter.loadbalancing 구성 완료
✅ 스테이징 환경에서 tail_sampling_count_traces_sampled 메트릭으로 보존율 검증
✅ probabilistic 정책이 마지막에 위치했는지 재확인다음 글: Grafana Tempo TraceQL로 샘플링된 트레이스에서 에러 패턴을 탐지하고 알림까지 연결하는 방법
참고 자료
- Sampling | Grafana Tempo documentation
- Add tail sampling policies and strategies | Grafana Tempo documentation
- Enable tail-based sampling | Grafana Tempo documentation
- otelcol.processor.tail_sampling | Grafana Alloy documentation
- Introduction to Adaptive Traces | Grafana Cloud documentation
- Maximize data value and cut costs: Adaptive Telemetry for metrics, logs, traces, and profiles in Grafana Cloud | Grafana Labs
- Managing observability costs at scale | Grafana Labs Blog
- Tail Sampling Processor | OpenTelemetry Collector Contrib (GitHub)
- Sampling | OpenTelemetry
- Reduce Grafana Cloud Traces costs | Grafana Cloud documentation