본문 바로가기
Infra2026년 7월 30일6분 읽기

테일 샘플링 — 에러 트레이스는 남기고 트레이스 저장 비용은 줄이는 법

YS
김영삼
조회 6
테일 샘플링 — 에러 트레이스는 남기고 트레이스 저장 비용은 줄이는 법

Tail-based sampling(테일 샘플링)은 하나의 트레이스를 이루는 스팬들이 모두 도착한 뒤에 그 트레이스를 저장할지 버릴지 결정하는 방식이다. 요청 시작 시점에 주사위를 굴리는 head 샘플링과 반대다. 트레이스 전체를 보고 나서 "이건 에러가 있으니 남기고, 이건 200ms 넘게 걸렸으니 남기고, 나머지 정상 요청은 1%만 남긴다" 같은 판단을 내린다. OpenTelemetry Collector의 tail_sampling 프로세서가 대표적 구현이다.

트레이스 100%를 저장하면 스토리지 청구서가 무섭다. 그렇다고 무작정 1%만 남기면, 정작 문제가 된 에러 트레이스는 십중팔구 버려진다. 테일 샘플링은 이 둘 사이의 균형을 "내용을 보고" 잡는다.

왜 head 샘플링으론 부족한가

head 샘플링은 요청이 들어오는 순간, 아직 결과를 모르는 상태에서 확률로 결정한다. 빠르고 메모리도 안 먹지만 치명적 약점이 있다. 에러가 날지, 느릴지를 시작 시점엔 알 수 없다. 그래서 1% head 샘플링을 걸면, 정확히 그 1%의 확률로만 에러 트레이스가 남는다. 사고 나서 트레이스를 찾는데 없다. 몇 번 데이면 테일 샘플링으로 넘어오게 된다.

동작 원리 — 잠깐 모아 뒀다가 판단

테일 샘플링 Collector는 같은 trace_id의 스팬을 일정 시간(decision wait) 동안 메모리에 모은다. 그 창이 닫히면 정책을 평가해 통째로 저장하거나 버린다. 그래서 두 가지 제약이 생긴다. 첫째, 한 트레이스의 모든 스팬이 같은 Collector 인스턴스로 가야 한다(그래야 다 모인다). 둘째, 대기 창만큼의 스팬을 담을 메모리가 필요하다.

processors:
  tail_sampling:
    decision_wait: 10s          # 스팬을 모으는 대기 창
    num_traces: 100000          # 동시에 추적할 트레이스 수
    policies:
      - name: errors
        type: status_code
        status_code: { status_codes: [ERROR] }
      - name: slow
        type: latency
        latency: { threshold_ms: 500 }
      - name: baseline
        type: probabilistic
        probabilistic: { sampling_percentage: 1 }

이 설정의 뜻은 명확하다. 에러가 있으면 무조건 저장, 500ms 넘으면 저장, 그 외 정상·빠른 요청은 1%만 저장. 정책은 OR로 합쳐진다. 하나라도 걸리면 유지다.

분산 배포의 함정 — load balancing exporter

Collector를 여러 대로 수평 확장하면 문제가 생긴다. 같은 트레이스의 스팬이 서로 다른 Collector로 흩어지면, 각자 절반씩만 보고 잘못된 판단을 한다. 해결책은 2계층 구조다. 앞단 Collector가 trace_id 기준으로 라우팅해서, 한 트레이스는 반드시 같은 뒷단 Collector로 보낸다.

# 앞단(gateway) — trace_id로 일관 라우팅
exporters:
  loadbalancing:
    routing_key: traceID
    protocol:
      otlp: { }
    resolver:
      dns: { hostname: otel-tailsampler-headless }

이 구조를 모르고 그냥 오토스케일 뒤에 테일 샘플러를 여러 개 붙였다가, 에러 트레이스가 반쪽만 저장되는 걸 보고 한참 헤맸다. routing_key: traceID 한 줄이 핵심이다.

구분Head 샘플링Tail 샘플링
결정 시점요청 시작트레이스 완료 후
에러·지연 보존확률에 맡김규칙으로 보장
메모리 비용거의 없음대기 창만큼 필요
수평 확장단순trace_id 라우팅 필요

튜닝 감각

decision_wait는 가장 긴 트레이스가 완결되는 시간보다 살짝 길게 잡는다. 너무 짧으면 늦게 도착한 스팬이 잘려 나가고, 너무 길면 메모리가 붓는다. num_traces는 초당 트레이스 유입량 × decision_wait보다 넉넉히. 개인적으로는 처음엔 넉넉하게 잡아 메모리 그래프를 보며 조여 가는 편이 안전했다.

자주 묻는 질문

테일 샘플링을 쓰면 데이터가 얼마나 줄어드나요?

워크로드에 따라 다르지만, 정상 트래픽이 대부분인 서비스라면 저장량을 90% 이상 줄이면서도 에러·지연 트레이스는 사실상 전부 보존하는 구성이 흔합니다. 버리는 것은 대개 "정상적으로 빠르게 끝난" 요청들입니다.

head와 tail을 섞어 쓸 수 있나요?

가능합니다. SDK 단에서 head로 명백한 과잉 트래픽을 먼저 쳐내 Collector 부하를 줄이고, 남은 것을 tail로 정교하게 거르는 2단 구성이 실전에서 자주 쓰입니다. 다만 head로 이미 버린 트레이스는 tail이 되살릴 수 없다는 점은 감안해야 합니다.

메모리를 많이 먹는다는데 안전한가요?

대기 창 동안의 스팬을 모두 들고 있어야 하므로 메모리가 필요합니다. decision_waitnum_traces를 워크로드에 맞게 잡고, memory_limiter 프로세서를 함께 걸어 상한을 두면 OOM을 방지할 수 있습니다.

지연(latency) 정책의 임계값은 어떻게 정하나요?

정상 요청의 p95~p99 근처에서 시작해 조정하는 걸 권합니다. 너무 낮으면 정상 트래픽까지 다 잡혀 절감 효과가 사라지고, 너무 높으면 잡고 싶은 "약간 느린" 요청을 놓칩니다.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록