OTel Collector Tail Sampling 메모리 최적화: 프로덕션 OOM을 막는 decision\_wait·num\_traces 설정 가이드
고트래픽 서비스를 운영하다 보면 OpenTelemetry Collector가 갑자기 OOMKilled 상태로 재시작되는 상황을 마주치게 됩니다. 모니터링 파이프라인이 30분마다 죽어난다면, 그 원인은 높은 확률로 tailsamplingprocessor의 파라미터 설정에 있습니다. Tail Sampling은 에러·지연 트레이스를 정밀하게 선별할 수 있는 강력한 도구이지만, 잘못 설정하면 메모리 폭탄이 됩니다.
다른 최적화 가이드와 달리, 이 글은 실제 프로덕션 OOM 사례 두 건과 Michal Drozd의 벤치마크 수치를 바탕으로 decision_wait와 num_traces를 올바르게 조합하는 것만으로 메모리 사용량을 최대 75% 줄일 수 있다는 것을 구체적인 계산과 설정 예시로 보여드립니다. 2-티어 아키텍처 설정, 안전망 역할을 하는 memory_limiter 배치, 그리고 Prometheus 알림 규칙까지 — 오늘 바로 가져가 적용할 수 있는 설정 가이드입니다.
핵심 개념
OTel Collector와 Tail Sampling이란
OpenTelemetry Collector는 애플리케이션에서 발생하는 트레이스·메트릭·로그를 수집, 처리, 전달하는 벤더 중립 파이프라인입니다. 마이크로서비스 환경에서 각 서비스가 직접 백엔드(Jaeger, Tempo 등)로 데이터를 보내는 대신, Collector가 중간에서 일괄 처리를 담당합니다.
샘플링 방식에는 두 가지가 있습니다.
| 방식 | 결정 시점 | 특징 |
|---|---|---|
| Head Sampling | 트레이스 시작 시점 | 빠르고 메모리 부담이 없습니다. 단, 에러·지연 여부를 알기 전에 결정해야 합니다 |
| Tail Sampling | 모든 스팬 수집 후 | 에러·지연 트레이스를 정확히 선별할 수 있습니다. 단, 판단 전까지 전체 스팬을 메모리에 보관해야 합니다 |
Tail Sampling이 메모리를 많이 소비하는 이유가 바로 여기에 있습니다. 샘플링 결정을 내리기 전까지 트레이스에 속한 모든 스팬을 메모리에 유지해야 합니다.
tailsamplingprocessor의 핵심 파라미터
tailsamplingprocessor는 네 가지 파라미터로 메모리 사용량이 결정됩니다.
| 파라미터 | 기본값 | 역할 |
|---|---|---|
decision_wait |
30s | 첫 스팬 수신 후 샘플링 결정까지 대기 시간 |
num_traces |
50,000 | 메모리에 동시 보관하는 최대 트레이스 수 |
expected_new_traces_per_sec |
0 | 초당 예상 신규 트레이스 수 (내부 데이터 구조 사전 할당용) |
maximum_trace_size_bytes |
미설정 | 단일 트레이스 최대 크기 (초과 시 즉시 드롭) |
메모리 사용량 계산 공식
설정을 바꾸기 전에 필요 메모리를 미리 계산해보는 것이 중요합니다.
필요 메모리 ≈ traces_per_second × decision_wait_seconds × avg_spans_per_trace × bytes_per_span예를 들어 1,000 TPS 환경에서 decision_wait: 30s, 트레이스당 평균 10개 스팬, 스팬당 1KB라면:
1,000 × 30 × 10 × 1KB = 300MB (안전 마진 2배 적용 시 600MB)안전 마진 규칙: 계산된 필요량의 1.5~2배를 컨테이너 메모리 limit으로 설정하는 것을 권장합니다. Collector 자체 기본 오버헤드 약 200MB도 반드시 포함해야 합니다.
왜 수평 확장이 안 되는가: loadbalancingexporter의 역할
tailsamplingprocessor는 상태를 메모리에 유지합니다. 동일한 트레이스의 모든 스팬이 반드시 같은 Collector 인스턴스에 도달해야 올바른 샘플링 결정이 가능합니다. 일반적인 수평 확장(pod 복제 + 라운드로빈 로드 밸런서)은 하나의 트레이스를 여러 Collector에 쪼개어 샘플링 결정 자체를 망가뜨립니다.
이 문제를 해결하는 것이 loadbalancingexporter입니다.
| 항목 | 내용 |
|---|---|
| 동작 방식 | trace ID를 해시 키로 삼아 항상 동일한 백엔드 Collector로 라우팅 |
| 역할 | 1티어(게이트웨이)에서 트래픽을 분산하고, 2티어(샘플링)에서 상태 일관성을 보장 |
| 주의 사항 | 백엔드 부하를 고려하지 않으므로, 각 샘플링 Collector의 메모리를 동일하게 맞춰두는 것이 좋습니다 |
loadbalancingexporter: 고트래픽 환경에서 Tail Sampling을 안전하게 사용하기 위한 필수 전제 조건입니다. 이 익스포터 없이는 수평 확장이 오히려 데이터 정합성을 해칩니다.
실전 적용
예시 1: decision_wait 60s가 부른 30분 주기 OOM
환경: 컨테이너 메모리 512MB, 트래픽 1,000 TPS
공식에 대입해보면 문제가 바로 드러납니다.
1,000 TPS × 60s × 10 spans × 1KB = 600MB > 512MB (컨테이너 한계)Collector는 30분 안에 메모리를 채우고 OOMKilled로 재시작되는 사이클을 반복했습니다.
processors:
tail_sampling:
decision_wait: 15s # 60s → 15s로 단축 (메모리 약 75% 감소)
num_traces: 50000 # 1,000 TPS × 15s = 최대 15,000 트레이스 → 50,000은 충분한 여유
expected_new_traces_per_sec: 1000num_traces: 50000을 그대로 유지한 이유는, 1,000 TPS × 15s 기준으로 동시 최대 15,000 트레이스만 필요하기 때문에 50,000이 이미 충분한 버퍼를 제공하기 때문입니다. 트래픽이 더 높은 환경이라면 이 값도 함께 재계산하는 것을 권장합니다.
| 조정 전 | 조정 후 | 변화율 |
|---|---|---|
| decision_wait: 60s | decision_wait: 15s | 4배 단축 |
| 메모리: ~600MB | 메모리: ~150MB | 75% 감소 |
decision_wait를 절반으로 줄이면 메모리도 절반에 가깝게 줄어드는 근사 선형 관계입니다. 단, GC 지연이나 스팬 도착 분포에 따라 실제 감소폭은 다를 수 있습니다. 배치 작업이나 비동기 처리가 많은 환경에서는 실제 트레이스 완료 시간을 먼저 측정한 후 조정하는 것을 권장합니다.
예시 2: 트래픽 스파이크 시 num_traces 한계 초과
환경: 기본값 num_traces: 50,000, 갑작스러운 5배 트래픽 증가
num_traces 한계를 초과하면 가장 오래된 트레이스부터 드롭됩니다. 에러 트레이스는 처리 지연으로 버퍼에 더 오래 남아있을 가능성이 높기 때문에, 정작 보존해야 할 트레이스가 먼저 사라지는 역설적 상황이 발생합니다.
num_traces만 늘리는 것은 OOM 위험을 높입니다. memory_limiter를 tail_sampling 앞에 배치해 안전망을 먼저 구축하는 것이 핵심입니다.
processors:
memory_limiter:
check_interval: 1s
limit_mib: 3600 # 컨테이너 4GB의 90%
spike_limit_mib: 800
tail_sampling:
decision_wait: 15s
num_traces: 100000
expected_new_traces_per_sec: 5000
maximum_trace_size_bytes: 50000 # 50KB 초과 트레이스 즉시 드롭
service:
pipelines:
traces:
processors: [memory_limiter, tail_sampling] # 순서가 중요합니다maximum_trace_size_bytes: 50000 설정은 디버그 로그를 스팬에 가득 담은 비정상 트레이스가 단독으로 수백 MB를 점유하는 상황을 차단합니다. Michal Drozd의 벤치마크에 따르면, 3,000 TPS 환경에서 이 설정을 적용한 결과 메모리가 1.4GB에서 890MB로 줄었습니다.
memory_limiter: 메모리 사용량이 지정한 한계에 도달하면 새 데이터를 거부(backpressure)하거나 드롭해서 Collector 자체가 OOM으로 죽지 않도록 보호하는 프로세서입니다.tail_sampling보다 반드시 앞에 위치해야 합니다.
예시 3: 고트래픽 프로덕션을 위한 2-티어 아키텍처
5,000 TPS 이상의 고트래픽 환경에서는 단일 Collector로 Tail Sampling을 처리하는 것이 한계에 부딪힙니다. 1티어(게이트웨이)와 2티어(샘플링 전용)를 분리하는 패턴이 현재 프로덕션 표준으로 자리잡고 있습니다.
아래는 두 개의 별도 Collector 프로세스에 대한 설정입니다. 하나의 파일이 아닙니다.
# 파일 1: gateway-collector.yaml (1티어 — 게이트웨이 Collector)
exporters:
loadbalancing:
routing_key: "traceID" # trace ID 기준 라우팅으로 상태 일관성 보장
protocol:
otlp:
timeout: 1s
resolver:
static:
hostnames:
- sampling-collector-1:4317
- sampling-collector-2:4317
- sampling-collector-3:4317# 파일 2: sampling-collector.yaml (2티어 — 샘플링 Collector)
processors:
tail_sampling:
decision_wait: 15s
num_traces: 200000
expected_new_traces_per_sec: 3000
maximum_trace_size_bytes: 100000
policies:
# 정책은 OR로 평가됩니다: 하나라도 해당되면 트레이스를 보존합니다
- name: errors-policy
type: status_code
status_code: {status_codes: [ERROR]}
- name: slow-traces-policy
type: latency
latency: {threshold_ms: 1000}
- name: probabilistic-policy
type: probabilistic
probabilistic: {sampling_percentage: 10}이 구조에서 1티어는 stateless라 수평 확장이 자유롭고, 2티어만 수직 확장(메모리 증설)의 대상이 됩니다. 각 샘플링 Collector의 메모리를 동일하게 설정하는 것을 권장합니다.
모니터링 및 선제 대응
OOM이 발생하기 전에 경고를 받는 것이 중요합니다. 아래 Prometheus 알림 규칙 세 가지를 등록해두면 문제가 커지기 전에 대응할 수 있습니다.
# Prometheus 알림 규칙
groups:
- name: tail_sampling
rules:
- alert: TailSamplingMemoryHigh
expr: otelcol_processor_tail_sampling_count_traces_on_memory > 250000
for: 5m # 일시적 스파이크를 걸러내고 지속적 초과 시에만 발화
annotations:
summary: "메모리 적재 트레이스가 임계값을 초과했습니다"
- alert: TailSamplingDropHigh
expr: rate(otelcol_processor_tail_sampling_count_spans_dropped[5m]) > 100
for: 5m
annotations:
summary: "스팬 드롭이 발생하고 있습니다"
- alert: TailSamplingDroppedTooEarly
expr: rate(otelcol_processor_tail_sampling_sampling_trace_dropped_too_early[5m]) > 10
for: 5m
annotations:
summary: "decision_wait가 너무 짧거나 num_traces가 부족합니다"| 메트릭 | 의미 |
|---|---|
otelcol_processor_tail_sampling_count_traces_on_memory |
현재 메모리 적재 트레이스 수 |
otelcol_processor_tail_sampling_sampling_trace_dropped_too_early |
decision_wait 전 드롭된 트레이스 |
otelcol_processor_tail_sampling_sampling_trace_removal_age |
트레이스 버퍼 유지 시간 |
otelcol_process_memory_rss |
Collector 실제 RSS 메모리 |
sampling_trace_removal_age 값이 decision_wait에 근접하기 시작하면 드롭이 임박했다는 신호입니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 정밀한 샘플링 | 에러·지연 트레이스를 100% 보존하면서 정상 트레이스만 확률 샘플링 가능 |
| 예측 가능한 메모리 제어 | decision_wait 단축 → 메모리 비례 감소, 공식으로 사전 계산 가능 |
| 비용 절감 | 불필요한 트레이스 저장 비용을 정책 기반으로 정밀 제거 |
| 대형 트레이스 보호 | maximum_trace_size_bytes로 이상 트레이스가 전체 파이프라인을 망가뜨리는 상황 방지 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 상태 보존 필수 | 스팬이 여러 Collector에 분산되면 샘플링 결정 불가 | loadbalancingexporter로 trace ID 기반 라우팅 |
| 수평 확장 불가 | Collector 복제만으로는 해결되지 않음 | 2-티어 분리, 2티어는 수직 확장 |
| 긴 트레이스 손실 위험 | decision_wait를 너무 짧게 설정하면 배치·비동기 스팬 누락 | 실제 트레이스 완료 시간을 먼저 측정 후 설정 |
| num_traces 한계 초과 | 오래된 트레이스(에러 포함)부터 드롭 | memory_limiter 선행 배치 + 트래픽 기반 적정값 설정 |
실무에서 가장 흔한 실수
-
batch→tail_sampling순서로 배치: 같은 트레이스의 스팬들이 서로 다른 배치로 쪼개져 샘플링 결정이 엉켜버립니다. 올바른 순서는tail_sampling→batch입니다. -
memory_limiter없이num_traces를 무작정 늘리기:num_traces는 메모리와 직접 비례합니다. 안전망 없이 늘리면 OOM을 더 빠르게 유발합니다. 반드시memory_limiter를 먼저 배치한 후 조정하는 것을 권장합니다. -
expected_new_traces_per_sec를 0으로 방치: 이 값이 0이면 내부 Go map 구조의 사전 할당이 생략됩니다. 트레이스가 늘어날 때마다 동적 재할당이 발생하여 GC 압력이 높아집니다. 실제 TPS에 맞게 설정해두면 메모리 할당 패턴이 훨씬 안정적으로 유지됩니다.
마치며
공식 한 줄로 필요 메모리를 계산하고(TPS × decision_wait × avg_spans × bytes_per_span), memory_limiter로 안전망을 구축하고, 알림으로 선제 대응 체계를 갖추는 것이 OOM 없는 Tail Sampling 운영의 세 가지 핵심입니다.
지금 바로 시작할 수 있는 3단계입니다.
- 현재 메트릭을 확인하는 것을 권장합니다:
otelcol_processor_tail_sampling_count_traces_on_memory와otelcol_process_memory_rss를 Grafana에서 확인하고,TPS × decision_wait_seconds × avg_spans × bytes_per_span공식으로 이론값과 실제값을 비교할 수 있습니다. decision_wait를 15s로 낮추고memory_limiter를 파이프라인 첫 번째에 배치하는 것을 권장합니다: 대부분의 동기 요청 체인은 15s로 충분합니다.limit_mib는 컨테이너 메모리의 85~90%로 설정하는 것이 좋습니다.TailSamplingDroppedTooEarly알림을 등록하는 것을 권장합니다: 이 알림이 울리면decision_wait가 너무 짧거나num_traces가 부족하다는 신호입니다. 메트릭 추이를 보며 점진적으로 조정할 수 있습니다.
다음 편
loadbalancingexporter심화 가이드: 트래픽 편향 문제를 해결하고 샘플링 Collector 간 부하를 균등하게 분산하는 2-티어 아키텍처 운영 전략을 다룹니다. DNS resolver 설정과 헬스체크 연동까지 포함합니다.
참고 자료
- Tail Sampling Processor README | opentelemetry-collector-contrib —
decision_wait,num_traces등 모든 파라미터의 공식 레퍼런스 - Tail-Based Sampling in OpenTelemetry: Sizing, Memory Crashes and Cost Model | Michal Drozd — 이 글의 벤치마크 수치 출처. 설정별 메모리 프로파일링 결과를 포함합니다
- Sampling | OpenTelemetry 공식 문서 — Head Sampling vs Tail Sampling 개념을 더 깊이 이해하려면 이 문서를 참고하는 것을 권장합니다
- Tail Sampling with OpenTelemetry: Why it's useful | OpenTelemetry Blog — Tail Sampling 도입 배경과 활용 사례
- Load Balancing Exporter README | opentelemetry-collector-contrib — 2-티어 아키텍처를 더 깊이 이해하려면 이 문서를 참고하는 것을 권장합니다
- Scaling the Collector | OpenTelemetry — 수평/수직 확장 전략 공식 가이드
- Mastering the OpenTelemetry Memory Limiter Processor | Dash0 —
memory_limiter파라미터 상세 설명 - otelcol.processor.tail_sampling | Grafana Alloy 문서 — Grafana Alloy 환경에서의 설정 방법
- Gateway deployment pattern | OpenTelemetry — 게이트웨이 패턴 공식 아키텍처 가이드