Tempo spanmetrics 전량 폐기 — batch timeout과 ingestion slack의 함정
140만 span이 수신됐지만 전량 폐기됐다. Alloy Kafka consumer의 batch timeout이 Tempo의 ingestion slack을 초과해서 모든 spanmetrics가 생성되지 않은 문제
한 줄 요약#
Tempo가 140만 span을 수신했지만 전량
outside_metrics_ingestion_slack으로 폐기했다. Alloy Kafka consumer의 batch timeout(10s)이 dev 환경의 저트래픽에서 항상 최악 케이스로 작동해서, 전체 파이프라인 지연이 Tempo의 30초 slack을 넘긴 것이 원인이었다.
Impact#
- 영향 범위: DB Health 대시보드의 span metrics 기반 패널 전체
- 증상:
traces_spanmetrics_latency_bucket,traces_spanmetrics_calls_total모두 0건 - 발생일: 2026-03-26
🔥 증상: spanmetrics만 No data#
DB Health 대시보드에서 ticketing 서비스를 선택하니 span metrics 기반 패널이 전부 No data였어요.
이상한 건 HikariCP Connection Pool 메트릭은 정상이라는 거예요.
db_client_connections_*는 잘 보이는데, traces_spanmetrics_*만 없습니다.
두 메트릭의 수집 경로가 다르기 때문이에요:
- HikariCP: Alloy scraping → Mimir (직접)
- spanmetrics: App → Alloy → Kafka → Tempo → metrics_generator → Mimir
Tempo의 metrics_generator 쪽에 문제가 있다고 좁혔습니다.
🤔 진단: 140만 span, 전량 폐기#
Tempo의 /metrics 엔드포인트를 확인했어요.
tempo_metrics_generator_spans_received_total = 1,406,413
tempo_metrics_generator_spans_discarded_total = 1,406,413
reason="outside_metrics_ingestion_slack"
tempo_metrics_generator_registry_active_series = 0
tempo_metrics_generator_registry_series_added_total = 0
140만 span을 수신했지만 전량 폐기. 시리즈 0건 생성.
폐기 사유가 outside_metrics_ingestion_slack이에요.
이건 "span의 타임스탬프가 Tempo 수신 시각 대비 너무 오래됐다"는 의미입니다.
Tempo의 metrics_ingestion_time_range_slack 기본값은 30초예요.
span이 생성된 후 30초 이내에 Tempo에 도착해야 metrics_generator가 처리합니다.
30초를 넘기면 "너무 오래된 데이터"로 판단해서 폐기해요.
🤔 원인: 파이프라인 지연 합산이 30초를 넘는다#
전체 트레이스 파이프라인의 지연을 계산해봤어요.
App에서 span 생성 (타임스탬프 기록)
→ Alloy OTLP receiver
→ tail_sampling (decision_wait: 5s) +5초
→ batch "traces" (timeout: 5s) +5초
→ Kafka exporter → Kafka topic +~1초
→ Kafka consumer (Alloy)
→ batch "kafka_traces" (timeout: 10s) +10초 ← 병목
→ Tempo OTLP exporter
──────────────────────────────────────────────
최악 합계: ~21초 + Kafka lag
최악 케이스에서 21초 + Kafka lag. 간헐적으로 Kafka consumer lag이 10초 이상 추가되면 30초를 넘깁니다.
batch timeout의 함정#
여기서 핵심은 batch timeout의 동작 방식이에요.
batch processor는 두 가지 조건 중 하나가 충족되면 전송합니다:
send_batch_size(256개)에 도달하면 즉시 전송timeout(10s)에 도달하면 모아진 것만 전송
스파이크 트래픽에서는 256개가 금방 차서 timeout이 무의미해요. 저트래픽에서는 256개가 안 차서 항상 timeout까지 대기합니다.
dev 환경은 synthetic traffic만 소량 흐르는 저트래픽 환경이에요. batch가 채워지지 않아서 매번 timeout 10초 전체를 대기합니다. 이 10초가 파이프라인 전체 지연의 절반을 차지했어요.
✅ 해결#
1. batch timeout 줄이기: 10s → 2s#
# alloy-values.yaml
batch "kafka_traces":
timeout: "2s" # 10s → 2s
send_batch_size: 256 # 유지
수정 후 파이프라인 최악 지연:
5(tail_sampling) + 5(batch_traces) + 1(Kafka) + 2(batch_kafka) = ~13초
30초 slack 대비 2배 이상 여유가 생겼어요.
스파이크 트래픽에서는 어떨까요? send_batch_size=256이 먼저 트리거되니까 timeout 값은 무관합니다. 2초든 10초든 부하가 높으면 배치가 금방 차서 즉시 전송돼요.
2. ingestion slack 명시#
# tempo-values.yaml
metrics_generator:
metrics_ingestion_time_range_slack: 30s # 기본값과 동일하지만 명시
기본값과 같지만, 파이프라인 지연 대비 마진이 충분함을 주석으로 문서화했어요. 나중에 파이프라인에 새 버퍼를 추가할 때 이 값을 확인하게 됩니다.
📚 배운 점#
Batch timeout은 부하에 따라 완전히 다르게 작동한다#
batch processor를 설정할 때 보통 "timeout 10s, batch_size 256"이면 적당하다고 생각해요. 스파이크 트래픽에서는 맞는 말이에요. 256개가 금방 차니까 timeout은 안전장치 역할만 합니다.
하지만 저트래픽에서는 timeout이 곧 지연 시간이에요. 256개를 절대 못 채우니까 매번 10초를 꼬박 기다립니다. dev 환경처럼 트래픽이 적은 곳에서 timeout이 큰 값이면, 실질적으로 모든 데이터가 10초씩 지연되는 거예요.
파이프라인에 버퍼를 추가하면 ingestion slack을 재계산해야 한다#
Kafka 버퍼를 도입한 건 Tempo OOM 문제 때문이었어요. 그때는 "Kafka가 spike를 흡수한다"는 장점만 봤지, 파이프라인 전체 지연이 늘어난다는 점은 놓쳤습니다.
관측성 파이프라인에 새 컴포넌트(Kafka, queue, 추가 processor 등)를 넣을 때는 반드시:
- 각 단계의 최악 지연을 합산한다
- 다운스트림의 time-based 제약(ingestion slack, out-of-order window 등)과 비교한다
- 합산 지연이 제약의 50% 이내인지 확인한다 (마진 확보)
dev 환경의 "저트래픽 함정"#
prod에서는 잘 동작하는 설정이 dev에서 깨지는 경우가 있어요. 이 케이스가 정확히 그랬습니다.
prod의 고트래픽 → batch가 빨리 차서 timeout 무관 → 정상 dev의 저트래픽 → batch가 안 차서 항상 timeout → 지연 누적 → 폐기
prod과 dev의 차이를 batch timeout 하나에서도 고려해야 합니다.