LLM WikiAccess-protected knowledge portal
← 스터디 홈
6편 · 약 15분

실전 운영과 성능 고려사항

프로덕션 OTel이 어려운 이유

개발 환경에서는 ConsoleExporter에 스팬 몇 개를 찍어보는 것으로 충분하다. 프로덕션은 다르다. 트래픽이 1000 RPS라면, 요청당 스팬 20개를 계측할 때 초당 2만 개의 스팬이 생긴다. 하루 17억 개다.

세 가지 압박이 동시에 온다.

볼륨: 대부분의 트레이스는 "정상, 느리지 않음"이다. 이것들을 전부 저장하면 비용이 폭증한다.

오버헤드: 계측 코드가 실제 요청 레이턴시에 영향을 준다. 잘못 설정된 동기 Exporter는 요청마다 수십 ms를 더한다.

카디널리티: user.idrequest.uuid처럼 고유값이 많은 속성을 메트릭 레이블로 쓰면, Prometheus 메모리가 수십 GB로 불어난다.

이 챕터는 이 세 가지 문제를 다루는 실전 패턴을 정리한다.

샘플링 전략: Head vs Tail

가장 근본적인 볼륨 제어 수단은 샘플링이다.

Head 샘플링

트레이스가 시작하는 순간 보존 여부를 결정한다. SDK의 TraceIdRatioBased 샘플러가 대표적이다.

from opentelemetry.sdk.trace.sampling import TraceIdRatioBased
sampler = TraceIdRatioBased(rate=0.1)  # 10%만 보존

장점: 결정이 즉시 이루어져 Collector 부담이 없다. 단점: 에러 트레이스도 90%가 버려진다. 중요한 트레이스를 놓칠 확률이 높다.

Tail 샘플링

트레이스의 모든 스팬이 도착한 뒤 결정한다. Collector의 tail_sampling processor가 담당한다. 에러·느린 요청은 100% 보존하고, 정상 요청만 낮은 비율로 샘플링하는 정책이 가능하다.

processors:
  tail_sampling:
    decision_wait: 10s        # 스팬이 모이길 기다리는 시간
    num_traces: 100000        # 메모리에 유지할 최대 트레이스 수
    expected_new_traces_per_sec: 10
    policies:
      - name: always-sample-errors
        type: status_code
        status_code: { status_codes: [ERROR] }

      - name: always-sample-slow
        type: latency
        latency: { threshold_ms: 500 }

      - name: sample-rest
        type: probabilistic
        probabilistic: { sampling_percentage: 5 }

단점: 트레이스 전체가 동일한 Collector 인스턴스에 도달해야 한다. 게이트웨이를 여러 인스턴스로 수평 확장할 때 loadbalancingexporter로 trace_id 기준 라우팅이 필요하다.

Head 샘플링 (SDK) 요청 시작 TraceIdRatioBased 10% 확률로 keep / 90% drop 스팬 생성 (10%) → Exporter → 백엔드 스팬 없음 (90%) 계측 코드 미실행 장점 • 결정 즉시, Collector 부담 없음 • 메모리 사용 최소 단점 • 에러 트레이스도 90% 버려짐 • 중요한 이상 트레이스를 놓칠 수 있음 Tail 샘플링 (Collector) 모든 스팬이 Collector에 도착 (decision_wait 대기) 트레이스 전체를 메모리에 버퍼링 ERROR 100% keep 항상 보존 Slow (>500ms) 100% keep 항상 보존 정상 요청 5% keep 낮은 비율 장점 • 에러·느린 트레이스 100% 보존 • 정책 기반 스마트 샘플링 단점 • 버퍼링 → 메모리 사용 큼 • 게이트웨이 단일 인스턴스 or 로드밸런싱 필요
Head 샘플링 vs Tail 샘플링 비교

Memory Limiter와 Batch 튜닝

Memory Limiter 용량 계산

프로덕션 Collector의 메모리 한도는 예상 사용량에 30% 여유를 더해 설정한다.

항목크기
기본 오버헤드200 MiB
OTLP receiver (2개)100 MiB
Batch processor 버퍼50 MiB
Export 큐 (5000개)500 MiB
Tail sampling 버퍼500 MiB
소계1350 MiB
limit_mib (30% 여유)1800 MiB
processors:
  memory_limiter:
    check_interval: 1s
    limit_mib: 1800
    spike_limit_mib: 400   # 순간 급증 허용 폭

spike_limit_mib는 갑작스러운 트래픽 급증 때 순간적으로 초과할 수 있는 양이다. limit_mib - spike_limit_mib = 1400 MiB가 실제 soft_limit이다. soft limit을 넘으면 신규 데이터를 거부(503 반환)하고, limit_mib까지 올라가면 강제 drop한다.

Batch Processor 튜닝

processors:
  batch:
    timeout: 5s             # 기본 200ms → 5초로 증가 시 처리량 향상
    send_batch_size: 4096   # 기본 1024 → 4096
    send_batch_max_size: 8192

처리량보다 레이턴시가 중요한 트레이싱 파이프라인은 짧게(200ms / 1024), 볼륨이 큰 로그·메트릭은 길게(5s / 4096) 설정한다.

카디널리티 관리

메트릭에서 카디널리티 폭발은 Prometheus 메모리를 파괴한다. OTel에서 자주 발생하는 패턴과 해결책이다.

문제 패턴: http.url 전체를 레이블로 사용하면 요청마다 다른 시계열이 생긴다.

# 나쁜 예 — /orders/123, /orders/456, /orders/789... 무한 증가
http_server_duration{http.url="/orders/123"} ...

해결: attributes processor로 고유값 속성을 삭제하거나, filter processor로 URL을 패턴별로 정규화한다.

processors:
  attributes:
    actions:
      - key: http.url
        action: delete          # 메트릭에서는 제거
      - key: http.route         # /orders/{id} 형태의 경로 패턴만 유지
        action: insert
        from_attribute: http.target

일반 원칙: 트레이스 속성은 고유해도 괜찮다. 트레이스는 개별 스팬을 저장하지 시계열로 집계하지 않는다. 메트릭 레이블은 제한된 값만 허용한다. 레이블 조합의 수 = 시계열 수임을 기억하라.

SDK 오버헤드 최소화

계측 방식별 레이턴시 오버헤드 기준(참고치, 환경마다 다름):

계측 방식추가 레이턴시
자동 계측 (Zero-code)1~5 ms
수동 계측 (OTel API 직접)0.1~0.5 ms
BatchSpanProcessor (비동기)~0 ms (백그라운드)
SimpleSpanProcessor (동기)전체 네트워크 RTT

프로덕션에서 SimpleSpanProcessor는 절대 금지다. 요청 처리 중에 백엔드 응답을 기다린다.

자동 계측은 편리하지만 과도한 스팬을 만들 수 있다. filter processor로 헬스체크, 내부 probe, 정적 파일 요청 등의 노이즈를 Collector에서 제거한다.

processors:
  filter/drop-healthcheck:
    traces:
      span:
        - 'attributes["http.route"] == "/healthz"'
        - 'attributes["http.route"] == "/readyz"'
        - 'attributes["http.route"] == "/metrics"'

Collector 자체 모니터링

Collector는 자기 자신의 메트릭을 Prometheus 형식으로 /metrics 엔드포인트에서 내보낸다. 주요 지표:

메트릭의미경보 기준
otelcol_processor_refused_spansmemory_limiter가 거부한 스팬 수> 0 → 메모리 한도 증가 검토
otelcol_exporter_send_failed_spans내보내기 실패 수> 0 → 백엔드 연결 확인
otelcol_receiver_accepted_spans수신 스팬 수 (처리량 baseline)급락 시 앱 계측 확인
otelcol_processor_batch_timeout_trigger_send타임아웃으로 배치 전송된 횟수높으면 batch_size 증가 고려
process_runtime_heap_alloc_bytesCollector 힙 메모리limit의 80% 초과 경보

zpages(/debug/tracez, /debug/pipelinez)는 Collector 내부를 실시간으로 볼 수 있는 디버그 UI다. 개발 환경에서 파이프라인 문제를 진단할 때 유용하다.

OTel Collector receiver_accepted_spans ↑ 수신 처리량 baseline processor_refused_spans ⚠ memory_limiter 거부 → 메모리 증가 exporter_send_failed_spans ⛔ 내보내기 실패 → 백엔드 확인 process_runtime_heap_alloc_bytes 힙 메모리 → limit 80% 초과 경보 /metrics Prometheus scrape :8889/metrics 30s 간격 AlertManager 연결 Grafana Collector 대시보드 refused / failed 경보 메모리 추이 시각화 zpages :55679/debug/tracez 파이프라인 실시간 디버그 UI (개발용)
OTel 프로덕션 운영 메트릭 흐름

비용 관리 실전 체크리스트

OTel 관측성 비용의 대부분은 저장소(Grafana Cloud, Datadog, New Relic)에서 발생한다. 데이터를 줄이는 순서:

  1. filter processor — 헬스체크, 메트릭 수집 경로, 내부 probe의 트레이스·로그를 Collector에서 drop.
  2. tail sampling — 정상·성공 트레이스를 5~10% 샘플링. 에러·느린 트레이스는 100% 보존.
  3. attributes processor — 저장 비용이 큰 고카디널리티 속성(URL 전체, 사용자 ID) 제거.
  4. 메트릭 집계 수준 조정 — 히스토그램 버킷을 꼭 필요한 범위로만 설정.
  5. 로그 레벨 필터 — DEBUG 로그는 Collector에서 drop, WARN 이상만 전송.

실제 팀 경험치: 이 다섯 단계를 적용하면 데이터 볼륨을 40~70% 줄일 수 있다.

Kubernetes 운영 체크리스트

항목권장 설정
DaemonSet agent 리소스requests: 200m CPU / 400Mi, limits: 1 CPU / 1Gi
Gateway Deployment 레플리카최소 2개, HPA CPU 60% 기준
tail_sampling 사용 시loadbalancingexporter로 trace_id 기준 라우팅 필수
health_checkKubernetes liveness probe / :13133
설정 관리ConfigMap으로 관리, 변경 시 롤링 업데이트
시크릿exporter endpoint·auth는 환경변수 또는 k8s Secret 참조
업그레이드otelcol-contrib 릴리스 노트 확인 (processor API 변경 잦음)

OTel Operator (Kubernetes)

opentelemetry-operator를 쓰면 OpenTelemetryCollector CRD로 Collector를 선언적으로 관리하고, Instrumentation CRD로 자동 계측을 Pod에 주입할 수 있다.

apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
  name: my-instrumentation
spec:
  exporter:
    endpoint: http://otel-collector:4317
  propagators:
    - tracecontext
    - baggage
  sampler:
    type: parentbased_traceidratio
    argument: "0.1"
  python:
    image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-python:latest

instrumentation.opentelemetry.io/inject-python: "true" 어노테이션을 Pod에 추가하면 init container가 SDK를 자동 주입한다. 코드 변경 없이 계측이 가능하지만, 자동 계측이 생성하는 스팬 양을 반드시 확인하고 filter로 제어해야 한다.

한 줄 정리

프로덕션 OTel 운영의 핵심은 tail sampling으로 에러 트레이스를 100% 보존하면서 정상 트레이스를 5~10%로 줄이고, memory_limiter로 OOM을 막고, 고카디널리티 속성을 메트릭에서 제거하는 것이다. Collector 자체 메트릭을 Prometheus로 수집해 refused/failed 경보를 반드시 걸어야 한다.

References

  • https://opentelemetry.io/docs/collector/configuration/
  • https://oneuptime.com/blog/post/2026-01-25-memory-limiter-opentelemetry/view
  • https://oneuptime.com/blog/post/2026-02-06-batch-processor-opentelemetry-collector/view
  • https://oneuptime.com/blog/post/2026-02-06-reduce-opentelemetry-performance-overhead-production/view
  • https://oneuptime.com/blog/post/2026-02-06-cut-observability-costs-opentelemetry-filtering-sampling/view
  • https://oneuptime.com/blog/post/2026-02-06-handle-high-cardinality-metrics-opentelemetry/view
  • https://www.michal-drozd.com/en/blog/otel-tail-sampling/
  • https://last9.io/guides/opentelemetry/deploying-opentelemetry-at-scale-production-patterns-that-work/
  • https://betterstack.com/community/guides/observability/opentelemetry-sampling/
  • https://medium.com/@alokrahuldevocs/day-164-opentelemetry-collector-processors-the-control-layer-of-your-observability-pipeline-066cceeb82d8