Skip to content
Published on

OpenTelemetry Collector 파이프라인 설계와 운영 가이드: 수집부터 백엔드 연동까지

Authors
  • Name
    Twitter
OpenTelemetry Collector Pipeline

들어가며

분산 시스템의 복잡도가 증가할수록 관측 가능성(Observability)의 중요성은 기하급수적으로 커진다. 수백 개의 마이크로서비스가 상호작용하는 환경에서 Traces, Metrics, Logs 세 가지 텔레메트리 신호를 통합적으로 수집하고, 가공하고, 적절한 백엔드로 라우팅하는 파이프라인의 설계는 플랫폼 엔지니어링의 핵심 역량이다.

OpenTelemetry Collector는 CNCF 프로젝트로서 벤더 중립적인 텔레메트리 파이프라인을 제공한다. 특정 모니터링 솔루션에 종속되지 않으면서, Receiver로 다양한 포맷의 데이터를 수집하고, Processor로 가공과 필터링을 수행한 뒤, Exporter로 원하는 백엔드에 전송하는 유연한 아키텍처를 갖추고 있다. 2026년 현재 Collector는 v0.120 이상으로 성숙해졌으며, 프로덕션 환경에서의 안정성이 충분히 검증되었다.

이 글에서는 OpenTelemetry Collector의 내부 아키텍처부터 Receiver, Processor, Exporter의 실전 설정, Agent/Gateway 배포 패턴, Kubernetes 환경에서의 DaemonSet/Deployment 매니페스트, Tail Sampling 전략, 메모리 관리와 백프레셔 메커니즘, 트러블슈팅 가이드, 그리고 장애 복구 절차까지 운영 관점에서 필요한 모든 내용을 다룬다.

OpenTelemetry Collector 아키텍처

핵심 컴포넌트 구조

OpenTelemetry Collector의 아키텍처는 네 가지 핵심 컴포넌트로 구성된다. Receiver가 외부 소스로부터 텔레메트리 데이터를 수신하고, Processor가 데이터를 가공하며, Exporter가 최종 목적지로 전송한다. 여기에 Extension이 부가적인 기능(헬스체크, 인증, zPages 등)을 제공한다.

[Application / Infrastructure]
        |
        v
+-------------------+
|    Receivers       |  <-- OTLP, Prometheus, Filelog, Kafka, etc.
+-------------------+
        |
        v
+-------------------+
|    Processors      |  <-- Memory Limiter, Batch, Attributes, Tail Sampling
+-------------------+
        |
        v
+-------------------+
|    Exporters       |  <-- OTLP, Prometheus Remote Write, Loki, Kafka, etc.
+-------------------+

+-------------------+
|    Extensions      |  <-- Health Check, zPages, pprof, Bearer Token Auth
+-------------------+

하나의 Collector 인스턴스 안에 여러 개의 파이프라인을 정의할 수 있다. 각 파이프라인은 하나의 시그널 타입(traces, metrics, logs)을 처리하며, 서로 다른 Receiver, Processor, Exporter 조합을 가질 수 있다. 이 설계 덕분에 트레이스는 Tempo로, 메트릭은 Mimir로, 로그는 Loki로 각각 라우팅하는 구성이 단일 Collector 설정 파일 안에서 가능하다.

Core vs Contrib 배포판

OpenTelemetry Collector는 두 가지 배포판을 제공한다.

항목CoreContrib
포함 컴포넌트핵심 Receiver/Processor/Exporter만커뮤니티 기여 컴포넌트 다수 포함
바이너리 크기약 50MB약 200MB+
보안 표면적작음넓음
업데이트 주기격주격주
프로덕션 권장Custom Build 권장테스트/PoC에 적합
주요 사용 사례최소한의 컴포넌트만 필요한 경우다양한 소스/목적지 연동이 필요한 경우

프로덕션 환경에서는 OpenTelemetry Collector Builder(OCB)를 사용하여 필요한 컴포넌트만 포함한 커스텀 바이너리를 빌드하는 것이 보안과 성능 양면에서 권장된다.

데이터 모델과 시그널 타입

Collector 내부에서 데이터는 pdata(Pipeline Data) 형식으로 표현된다. 이 내부 데이터 모델은 OTLP(OpenTelemetry Protocol) 프로토콜 버퍼 정의를 기반으로 하며, 세 가지 시그널 타입을 지원한다.

  • Traces: 분산 트랜잭션의 실행 경로를 나타내는 Span의 집합. TraceID, SpanID, ParentSpanID로 인과 관계를 표현한다.
  • Metrics: 시계열 데이터로, Gauge, Sum, Histogram, ExponentialHistogram, Summary 타입을 지원한다.
  • Logs: 타임스탬프 기반의 이벤트 레코드. SeverityNumber, Body, Attributes, Resource를 포함한다.

Receiver 설정

Receiver는 텔레메트리 데이터의 진입점이다. Push 방식(OTLP, Kafka 등)과 Pull 방식(Prometheus, hostmetrics 등)을 모두 지원하며, 동일한 타입의 Receiver를 이름을 달리하여 여러 개 정의할 수 있다.

OTLP Receiver

OTLP는 OpenTelemetry의 네이티브 프로토콜로, gRPC와 HTTP/protobuf 두 가지 전송 방식을 제공한다. 대부분의 OpenTelemetry SDK는 OTLP Exporter를 기본으로 사용하므로, 이 Receiver는 거의 모든 Collector 구성에 포함된다.

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
        max_recv_msg_size_mib: 8 # 대용량 배치 허용
        max_concurrent_streams: 256 # 동시 스트림 수
        keepalive:
          server_parameters:
            max_connection_idle: 30s
            max_connection_age: 60s
            max_connection_age_grace: 10s
          enforcement_policy:
            min_time: 10s
            permit_without_stream: true
      http:
        endpoint: 0.0.0.0:4318
        cors:
          allowed_origins:
            - 'https://*.company.com'
          allowed_headers:
            - 'Content-Type'
            - 'X-Custom-Header'
          max_age: 600

gRPC 전송은 HTTP/2 기반으로 바이너리 직렬화와 멀티플렉싱을 지원하여 대용량 텔레메트리에 효율적이다. HTTP 전송은 브라우저 기반 계측(Web SDK)이나 방화벽 제약이 있는 환경에서 사용한다.

Prometheus Receiver

Prometheus Receiver는 기존 Prometheus 에코시스템과의 호환성을 제공한다. Prometheus의 scrape_configs 문법을 그대로 사용할 수 있어, 기존에 Prometheus로 수집하던 메트릭을 Collector를 통해 다른 백엔드로 라우팅할 수 있다.

receivers:
  prometheus:
    config:
      scrape_configs:
        - job_name: 'kubernetes-pods'
          scrape_interval: 30s
          scrape_timeout: 10s
          kubernetes_sd_configs:
            - role: pod
          relabel_configs:
            # prometheus.io/scrape 어노테이션이 있는 Pod만 스크래핑
            - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
              action: keep
              regex: true
            # 커스텀 포트 지정
            - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_port]
              action: replace
              target_label: __address__
              regex: (.+)
              replacement: '$$1'
            # 네임스페이스 레이블 추가
            - source_labels: [__meta_kubernetes_namespace]
              action: replace
              target_label: namespace
            # Pod 이름 레이블 추가
            - source_labels: [__meta_kubernetes_pod_name]
              action: replace
              target_label: pod
        - job_name: 'node-exporter'
          scrape_interval: 15s
          static_configs:
            - targets: ['node-exporter.monitoring.svc:9100']

Filelog Receiver

Filelog Receiver는 파일 시스템의 로그 파일을 실시간으로 수집한다. Kubernetes 환경에서 컨테이너 로그를 수집하는 핵심 컴포넌트이며, 오퍼레이터 체인을 통해 로그 파싱, 필터링, 변환을 수행한다.

receivers:
  filelog:
    include:
      - /var/log/pods/*/*/*.log
    exclude:
      - /var/log/pods/*/otel-collector*/*.log
      - /var/log/pods/kube-system_*/*/*.log
    start_at: end # 신규 로그만 수집 (beginning이면 기존 로그부터)
    include_file_path: true
    include_file_name: false
    retry_on_failure:
      enabled: true
      initial_interval: 1s
      max_interval: 30s
    operators:
      # CRI 로그 포맷 파싱 (containerd)
      - type: regex_parser
        id: parser-cri
        regex: '^(?P<time>[^ Z]+Z) (?P<stream>stdout|stderr) (?P<logtag>[^ ]*) ?(?P<log>.*)$'
        timestamp:
          parse_from: attributes.time
          layout: '%Y-%m-%dT%H:%M:%S.%fZ'
      # JSON 로그 본문 파싱
      - type: json_parser
        id: parser-json
        parse_from: attributes.log
        parse_to: body
        on_error: send_quiet # 파싱 실패 시 원본 유지
      # 심각도 매핑
      - type: severity_parser
        parse_from: attributes.level
        mapping:
          fatal: [FATAL, fatal, F]
          error: [ERROR, error, E]
          warn: [WARN, warn, W]
          info: [INFO, info, I]
          debug: [DEBUG, debug, D]

Receiver 타입 비교표

Receiver 타입방식시그널주요 용도
otlpPushTraces, Metrics, LogsOTel SDK 계측 애플리케이션
prometheusPullMetricsPrometheus 호환 메트릭 스크래핑
filelogPullLogs컨테이너/파일 로그 수집
hostmetricsPullMetricsCPU, Memory, Disk, Network 호스트 메트릭
k8s_eventsPullLogsKubernetes 이벤트 수집
kafkaPushTraces, Metrics, LogsKafka 토픽에서 텔레메트리 소비
zipkinPushTracesZipkin 포맷 트레이스 수신
jaegerPushTracesJaeger 포맷 트레이스 수신

Processor 파이프라인

Processor는 Receiver와 Exporter 사이에서 데이터를 가공하는 중간 계층이다. 순서가 중요하며, 파이프라인에 정의된 순서대로 체이닝되어 실행된다. 일반적으로 권장되는 Processor 순서는 다음과 같다.

memory_limiter -> k8sattributes -> resourcedetection -> attributes -> filter -> tail_sampling -> batch

Memory Limiter Processor

Memory Limiter는 Collector의 OOM(Out of Memory)을 방지하는 안전장치다. 반드시 Processor 체인의 가장 첫 번째에 배치해야 하며, 메모리 사용량이 임계값에 도달하면 데이터를 거부하여 프로세스를 보호한다.

processors:
  memory_limiter:
    check_interval: 1s
    limit_mib: 1800 # 하드 리밋 (컨테이너 limit의 80%)
    spike_limit_mib: 500 # 갑작스런 스파이크 허용량
    # limit_percentage: 80     # 또는 비율 기반 설정 (cgroup 인식)
    # spike_limit_percentage: 25

limit_mib는 컨테이너의 메모리 limit 대비 약 80% 수준으로 설정한다. 예를 들어 컨테이너 limit이 2Gi이면 limit_mib를 1600-1800으로 설정한다. 나머지 20%는 Go 런타임과 기타 내부 버퍼를 위한 여유 공간이다. spike_limit_mib는 순간적인 트래픽 버스트를 허용하면서도 limit_mib를 넘지 않도록 하는 완충 역할을 한다.

Batch Processor

Batch Processor는 개별 텔레메트리 레코드를 모아서 배치 단위로 Exporter에 전달한다. 이 배칭은 네트워크 요청 수를 줄이고 압축 효율을 높여 전반적인 파이프라인 처리량을 크게 향상시킨다.

processors:
  batch:
    timeout: 5s # 최대 대기 시간
    send_batch_size: 8192 # 배치 크기 (레코드 수)
    send_batch_max_size: 16384 # 최대 배치 크기 (이 크기를 넘으면 분할)
  # Gateway에서는 더 큰 배치
  batch/gateway:
    timeout: 10s
    send_batch_size: 16384
    send_batch_max_size: 32768

timeout이 먼저 도달하거나 send_batch_size에 먼저 도달하는 조건 중 하나라도 충족되면 배치가 전송된다. 트래픽이 적은 환경에서는 timeout이, 트래픽이 많은 환경에서는 send_batch_size가 주로 동작한다.

Attributes Processor

Attributes Processor는 텔레메트리 데이터의 속성(Attribute)을 추가, 수정, 삭제한다. 민감 정보 제거, 환경 정보 태깅, 레이블 정규화 등에 사용된다.

processors:
  attributes/security:
    actions:
      # 민감 HTTP 헤더 삭제
      - key: http.request.header.authorization
        action: delete
      - key: http.request.header.cookie
        action: delete
      # DB 쿼리 해싱 (민감 데이터 보호)
      - key: db.statement
        action: hash
      # 환경 태그 추가
      - key: deployment.environment
        action: upsert
        value: production
      # IP 주소 마스킹
      - key: net.peer.ip
        action: extract
        pattern: '^(?P<subnet>\d+\.\d+\.\d+)\.\d+$'
      - key: net.peer.ip
        action: delete
      - key: net.peer.subnet
        from_attribute: subnet
        action: upsert

Tail Sampling Processor

Tail Sampling은 전체 트레이스의 모든 Span이 수집된 후에 샘플링 결정을 내리는 방식이다. Head Sampling과 달리 에러가 발생했거나 응답이 느린 트레이스를 누락 없이 보존할 수 있어, 프로덕션 환경에서 디버깅 능력과 비용 절감을 동시에 달성할 수 있다.

processors:
  tail_sampling:
    decision_wait: 30s # 트레이스 완료 대기 시간
    num_traces: 200000 # 메모리에 유지할 최대 트레이스 수
    expected_new_traces_per_sec: 5000
    policies:
      # 정책 1: 에러가 포함된 트레이스는 100% 보존
      - name: error-traces
        type: status_code
        status_code:
          status_codes: [ERROR]
      # 정책 2: 2초 이상 걸린 트레이스는 100% 보존
      - name: high-latency
        type: latency
        latency:
          threshold_ms: 2000
          upper_threshold_ms: 0 # 0이면 상한 없음
      # 정책 3: 핵심 서비스는 50% 보존
      - name: critical-services
        type: and
        and:
          and_sub_policy:
            - name: service-match
              type: string_attribute
              string_attribute:
                key: service.name
                values:
                  - payment-service
                  - auth-service
                  - order-service
            - name: sample-half
              type: probabilistic
              probabilistic:
                sampling_percentage: 50
      # 정책 4: 특정 HTTP 경로 제외 (health check 등)
      - name: drop-health-checks
        type: string_attribute
        string_attribute:
          key: http.route
          values:
            - /healthz
            - /readyz
            - /livez
          invert_match: true
      # 정책 5: 나머지 트래픽은 5%만 샘플링
      - name: default-sampling
        type: probabilistic
        probabilistic:
          sampling_percentage: 5

Tail Sampling의 핵심 주의사항은 동일 TraceID의 모든 Span이 동일한 Collector 인스턴스로 도달해야 한다는 것이다. Gateway가 여러 대인 경우 반드시 TraceID 기반의 일관된 해싱(로드 밸런서의 consistent hashing)이 필요하다.

Processor 타입 비교표

Processor역할필수 여부배치 위치 권장
memory_limiterOOM 방지필수최우선 (첫 번째)
k8sattributesK8s 메타데이터 주입권장memory_limiter 다음
resourcedetection클라우드/호스트 정보 주입권장k8sattributes 다음
attributes속성 추가/수정/삭제선택중간
filter불필요 데이터 드롭선택샘플링 전
tail_sampling트레이스 기반 샘플링선택 (traces)batch 전
transformOTTL 기반 변환선택상황에 따라
batch배치 처리필수최후 (마지막)

Exporter 설정

Exporter는 가공된 텔레메트리 데이터를 최종 목적지로 전송하는 역할을 한다. 동일한 Exporter 타입을 이름을 달리하여 여러 백엔드에 동시 전송할 수 있으며, retry와 queue 설정으로 전송 안정성을 보장한다.

OTLP Exporter

OTLP Exporter는 다른 Collector(Gateway)나 OTLP를 네이티브로 지원하는 백엔드(Tempo, Jaeger, SigNoz 등)에 데이터를 전송한다.

exporters:
  # Traces -> Grafana Tempo
  otlp/tempo:
    endpoint: tempo-distributor.observability.svc:4317
    tls:
      insecure: true # 클러스터 내부 통신
    retry_on_failure:
      enabled: true
      initial_interval: 5s
      max_interval: 30s
      max_elapsed_time: 300s
    sending_queue:
      enabled: true
      num_consumers: 10
      queue_size: 5000
    timeout: 30s

  # Traces -> Jaeger (OTLP 네이티브 지원)
  otlp/jaeger:
    endpoint: jaeger-collector.observability.svc:4317
    tls:
      cert_file: /certs/client.crt
      key_file: /certs/client.key
      ca_file: /certs/ca.crt

Prometheus Remote Write Exporter

Prometheus Remote Write Exporter는 메트릭을 Prometheus 호환 백엔드(Mimir, Thanos, Cortex, VictoriaMetrics)에 전송한다.

exporters:
  prometheusremotewrite/mimir:
    endpoint: https://mimir.observability.svc:9009/api/v1/push
    tls:
      insecure: false
      cert_file: /certs/client.crt
      key_file: /certs/client.key
    headers:
      X-Scope-OrgID: 'tenant-production'
    resource_to_telemetry_conversion:
      enabled: true # Resource 속성을 메트릭 레이블로 변환
    external_labels:
      cluster: 'prod-ap-northeast-2'
      region: 'ap-northeast-2'
    retry_on_failure:
      enabled: true
      initial_interval: 5s
      max_interval: 60s
    sending_queue:
      enabled: true
      num_consumers: 5
      queue_size: 10000

Loki Exporter

Loki Exporter는 로그 데이터를 Grafana Loki에 전송한다. 레이블 매핑 설정이 중요하며, 과도한 레이블 카디널리티는 Loki의 성능을 저하시키므로 주의해야 한다.

exporters:
  loki:
    endpoint: https://loki-gateway.observability.svc:3100/loki/api/v1/push
    headers:
      X-Scope-OrgID: 'tenant-production'
    default_labels_enabled:
      exporter: false
      job: true
      instance: true
      level: true
    retry_on_failure:
      enabled: true
      initial_interval: 5s
      max_interval: 30s
    sending_queue:
      enabled: true
      num_consumers: 5
      queue_size: 5000

인기 백엔드 비교표

백엔드시그널프로토콜주요 특징Exporter 타입
Grafana TempoTracesOTLP gRPC오브젝트 스토리지, 비용 효율otlp
Grafana MimirMetricsPrometheus Remote WritePrometheus 호환, 멀티 테넌트prometheusremotewrite
Grafana LokiLogsHTTP Push레이블 기반 인덱싱, 저비용loki
JaegerTracesOTLP gRPC트레이스 전용, UI 내장otlp
ElasticsearchLogsHTTP풀텍스트 검색 강점elasticsearch
SigNozAllOTLP gRPC올인원 솔루션, ClickHouse 기반otlp
DatadogAllHTTPSaaS, 풍부한 통합datadog

Agent vs Gateway 배포 패턴

OpenTelemetry Collector를 배포하는 방식은 크게 Agent 패턴, Gateway 패턴, 그리고 두 패턴을 조합한 하이브리드 패턴으로 나뉜다. 프로덕션 환경에서는 Agent + Gateway 조합이 가장 널리 사용되며, 각 패턴의 장단점을 이해하고 트래픽 규모에 맞게 선택해야 한다.

패턴별 비교

특성Agent (DaemonSet)Gateway (Deployment)Agent + Gateway
배포 방식각 노드마다 1개클러스터 내 독립 서비스두 계층 조합
수집 범위로컬 노드클러스터 전체로컬 수집 + 중앙 처리
Tail Sampling불가 (트레이스 분산)가능 (중앙 집중)Gateway에서 수행
리소스 사용노드 수만큼 분산집중분산 + 집중
장애 영향 범위해당 노드만전체 파이프라인격리 가능
스케일링노드 추가 시 자동HPA로 수평 확장독립적 스케일링
복잡도낮음중간높음
권장 트래픽소규모중규모중~대규모

Agent + Gateway 하이브리드 아키텍처

[Node 1]                    [Node 2]                    [Node N]
+----------+               +----------+               +----------+
| App Pods |               | App Pods |               | App Pods |
+----+-----+               +----+-----+               +----+-----+
     |                          |                          |
+----+-----+               +----+-----+               +----+-----+
| OTel     |               | OTel     |               | OTel     |
| Agent    |               | Agent    |               | Agent    |
| (DaemonSet)              | (DaemonSet)              | (DaemonSet)
+----+-----+               +----+-----+               +----+-----+
     |                          |                          |
     +------------+-------------+-----------+--------------+
                  |                         |
           +------+------+          +------+------+
           | OTel Gateway |          | OTel Gateway |
           | (Deployment) |          | (Deployment) |
           +------+------+          +------+------+
                  |                         |
     +------------+-------------------------+
     |             |              |
+----+----+  +----+----+  +-----+-----+
|  Tempo  |  |  Mimir  |  |   Loki    |
+---------+  +---------+  +-----------+

Agent에서는 경량 처리(메모리 제한, 기본 배칭, K8s 메타데이터 주입)만 수행하고, Gateway에서 Tail Sampling, 고급 필터링, 최종 백엔드 라우팅을 담당한다. 이 분리를 통해 Agent의 리소스 사용량을 최소화하면서도 Gateway에서 정교한 데이터 처리를 수행할 수 있다.

Kubernetes 환경 배포

Agent DaemonSet 매니페스트

apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
  name: otel-agent
  namespace: observability
spec:
  mode: daemonset
  image: otel/opentelemetry-collector-contrib:0.120.0
  serviceAccount: otel-collector-agent
  env:
    - name: K8S_NODE_NAME
      valueFrom:
        fieldRef:
          fieldPath: spec.nodeName
    - name: K8S_POD_IP
      valueFrom:
        fieldRef:
          fieldPath: status.podIP
  resources:
    requests:
      cpu: 200m
      memory: 256Mi
    limits:
      cpu: 500m
      memory: 512Mi
  volumeMounts:
    - name: varlogpods
      mountPath: /var/log/pods
      readOnly: true
    - name: varlibdockercontainers
      mountPath: /var/lib/docker/containers
      readOnly: true
  volumes:
    - name: varlogpods
      hostPath:
        path: /var/log/pods
    - name: varlibdockercontainers
      hostPath:
        path: /var/lib/docker/containers
  tolerations:
    - operator: Exists
  config:
    receivers:
      otlp:
        protocols:
          grpc:
            endpoint: 0.0.0.0:4317
          http:
            endpoint: 0.0.0.0:4318
      filelog:
        include:
          - /var/log/pods/*/*/*.log
        exclude:
          - /var/log/pods/observability_otel-*/*/*.log
        start_at: end
        include_file_path: true
        operators:
          - type: regex_parser
            id: parser-cri
            regex: '^(?P<time>[^ Z]+Z) (?P<stream>stdout|stderr) (?P<logtag>[^ ]*) ?(?P<log>.*)$'
            timestamp:
              parse_from: attributes.time
              layout: '%Y-%m-%dT%H:%M:%S.%fZ'
      hostmetrics:
        collection_interval: 30s
        scrapers:
          cpu: {}
          memory: {}
          disk: {}
          network: {}
          load: {}
          filesystem:
            exclude_mount_points:
              mount_points: ['/dev/*', '/proc/*', '/sys/*']
              match_type: regexp

    processors:
      memory_limiter:
        check_interval: 1s
        limit_mib: 400
        spike_limit_mib: 100
      k8sattributes:
        auth_type: serviceAccount
        passthrough: false
        extract:
          metadata:
            - k8s.namespace.name
            - k8s.deployment.name
            - k8s.statefulset.name
            - k8s.daemonset.name
            - k8s.pod.name
            - k8s.pod.uid
            - k8s.node.name
            - k8s.container.name
          labels:
            - tag_name: app.label.team
              key: team
              from: pod
        pod_association:
          - sources:
              - from: resource_attribute
                name: k8s.pod.ip
          - sources:
              - from: connection
      batch:
        timeout: 5s
        send_batch_size: 4096

    exporters:
      otlp/gateway:
        endpoint: otel-gateway.observability.svc.cluster.local:4317
        tls:
          insecure: true
        retry_on_failure:
          enabled: true
          initial_interval: 5s
          max_interval: 30s
        sending_queue:
          enabled: true
          queue_size: 2000

    extensions:
      health_check:
        endpoint: 0.0.0.0:13133

    service:
      extensions: [health_check]
      pipelines:
        traces:
          receivers: [otlp]
          processors: [memory_limiter, k8sattributes, batch]
          exporters: [otlp/gateway]
        metrics:
          receivers: [otlp, hostmetrics]
          processors: [memory_limiter, k8sattributes, batch]
          exporters: [otlp/gateway]
        logs:
          receivers: [otlp, filelog]
          processors: [memory_limiter, k8sattributes, batch]
          exporters: [otlp/gateway]
      telemetry:
        logs:
          level: warn
        metrics:
          address: 0.0.0.0:8888

Gateway Deployment 매니페스트

apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
  name: otel-gateway
  namespace: observability
spec:
  mode: deployment
  replicas: 3
  image: otel/opentelemetry-collector-contrib:0.120.0
  serviceAccount: otel-collector-gateway
  resources:
    requests:
      cpu: '1'
      memory: 2Gi
    limits:
      cpu: '2'
      memory: 4Gi
  autoscaler:
    minReplicas: 3
    maxReplicas: 10
    targetCPUUtilization: 70
    targetMemoryUtilization: 80
  podDisruptionBudget:
    minAvailable: 2
  config:
    receivers:
      otlp:
        protocols:
          grpc:
            endpoint: 0.0.0.0:4317
            max_recv_msg_size_mib: 16

    processors:
      memory_limiter:
        check_interval: 1s
        limit_mib: 3200
        spike_limit_mib: 800
      resourcedetection:
        detectors: [env, system, gcp, aws, azure]
        timeout: 5s
        override: false
      attributes/security:
        actions:
          - key: http.request.header.authorization
            action: delete
          - key: db.statement
            action: hash
      filter/metrics:
        metrics:
          exclude:
            match_type: regexp
            metric_names:
              - 'go_.*'
              - 'process_.*'
              - 'promhttp_.*'
      tail_sampling:
        decision_wait: 30s
        num_traces: 200000
        expected_new_traces_per_sec: 5000
        policies:
          - name: error-traces
            type: status_code
            status_code:
              status_codes: [ERROR]
          - name: high-latency
            type: latency
            latency:
              threshold_ms: 2000
          - name: default-sampling
            type: probabilistic
            probabilistic:
              sampling_percentage: 10
      batch:
        timeout: 10s
        send_batch_size: 16384
        send_batch_max_size: 32768

    exporters:
      otlp/tempo:
        endpoint: tempo-distributor.observability.svc:4317
        tls:
          insecure: true
        sending_queue:
          enabled: true
          num_consumers: 10
          queue_size: 10000
      prometheusremotewrite/mimir:
        endpoint: http://mimir-distributor.observability.svc:8080/api/v1/push
        headers:
          X-Scope-OrgID: 'production'
        resource_to_telemetry_conversion:
          enabled: true
        sending_queue:
          enabled: true
          num_consumers: 5
          queue_size: 10000
      loki:
        endpoint: http://loki-gateway.observability.svc:3100/loki/api/v1/push
        headers:
          X-Scope-OrgID: 'production'
        sending_queue:
          enabled: true
          queue_size: 5000

    extensions:
      health_check:
        endpoint: 0.0.0.0:13133
      zpages:
        endpoint: 0.0.0.0:55679
      pprof:
        endpoint: 0.0.0.0:1777

    service:
      extensions: [health_check, zpages, pprof]
      pipelines:
        traces:
          receivers: [otlp]
          processors: [memory_limiter, resourcedetection, attributes/security, tail_sampling, batch]
          exporters: [otlp/tempo]
        metrics:
          receivers: [otlp]
          processors: [memory_limiter, resourcedetection, filter/metrics, batch]
          exporters: [prometheusremotewrite/mimir]
        logs:
          receivers: [otlp]
          processors: [memory_limiter, resourcedetection, attributes/security, batch]
          exporters: [loki]
      telemetry:
        logs:
          level: info
        metrics:
          address: 0.0.0.0:8888

Gateway 로드밸런싱과 TraceID 기반 라우팅

Tail Sampling을 사용하는 Gateway가 여러 대일 때, 동일 TraceID의 Span이 서로 다른 Gateway 인스턴스로 분산되면 샘플링 결정이 불완전해진다. 이 문제를 해결하기 위해 Agent에서 Gateway로 전송할 때 loadbalancing Exporter를 사용한다.

# Agent 측 Exporter 설정 (Gateway로 전송 시)
exporters:
  loadbalancing:
    protocol:
      otlp:
        tls:
          insecure: true
        timeout: 10s
    resolver:
      dns:
        hostname: otel-gateway-headless.observability.svc.cluster.local
        port: 4317
      # 또는 Kubernetes resolver 사용
      # k8s:
      #   service: otel-gateway
      #   ports:
      #     - 4317
    routing_key: traceID # TraceID 기반 일관된 해싱

이 설정을 사용하면 동일 TraceID를 가진 모든 Span이 동일한 Gateway Pod로 라우팅되어 Tail Sampling의 정확성을 보장한다.

메모리 관리와 백프레셔

OpenTelemetry Collector 운영에서 가장 빈번하게 발생하는 문제는 메모리 관련 이슈다. 텔레메트리 트래픽의 급증, Tail Sampling의 대기 버퍼, sending queue의 축적 등이 복합적으로 메모리 사용량을 증가시킨다.

메모리 사용 공식

Collector의 메모리 사용량을 산정할 때 다음 요소를 고려해야 한다.

총 메모리 = Go 런타임 기본 (약 50MB)
          + Receiver 버퍼 (연결 수 x 메시지 크기)
          + Processor 버퍼
            - Batch: send_batch_max_size x 레코드 평균 크기
            - Tail Sampling: num_traces x 트레이스 평균 크기
          + Exporter: queue_size x 배치 크기 x 레코드 평균 크기
          + 내부 오버헤드 (10-20%)

Tail Sampling이 가장 큰 메모리 소비 원인이다. decision_wait 시간 동안 메모리에 트레이스를 유지하므로, decision_wait가 30초이고 초당 5,000개의 새 트레이스가 유입되면 약 150,000개의 트레이스가 동시에 메모리에 존재한다.

백프레셔 메커니즘

Collector의 백프레셔(Backpressure)는 세 단계로 동작한다.

1단계로 Exporter의 sending queue가 가득 차면 Exporter가 Processor에 압력을 전달한다. 2단계로 memory_limiter가 메모리 사용량이 limit_mib에 도달하면 Receiver에 데이터 거부 신호를 보낸다. 3단계로 Receiver가 데이터를 거부하면 클라이언트(SDK 또는 Agent)에 에러를 반환하고, 클라이언트의 retry 로직이 동작한다.

이 백프레셔 체인이 정상적으로 동작하려면 memory_limiter가 반드시 Processor 체인의 첫 번째에 위치해야 한다. 그렇지 않으면 메모리 제한이 작동하기 전에 다른 Processor가 메모리를 소진하여 OOM이 발생할 수 있다.

GOGC 튜닝

Go 런타임의 가비지 컬렉터 튜닝도 메모리 관리에 중요하다. 기본 GOGC 값은 100이며, 이는 힙 크기가 이전 GC 사이클 대비 100% 증가하면 GC를 트리거한다. 메모리 여유가 적은 환경에서는 GOGC를 낮추어 더 빈번한 GC를 유도할 수 있다.

env:
  - name: GOGC
    value: '80' # 기본값 100에서 80으로 낮춤
  - name: GOMEMLIMIT
    value: '3600MiB' # 소프트 메모리 상한 (limit의 90%)

트러블슈팅 가이드

자체 메트릭을 활용한 진단

Collector는 자체 텔레메트리 메트릭을 기본 포트 8888에서 제공한다. 이 메트릭을 Prometheus로 스크래핑하여 Grafana 대시보드로 모니터링하는 것이 필수다.

# 수신 메트릭
otelcol_receiver_accepted_spans          # Receiver가 수락한 Spanotelcol_receiver_refused_spans           # Receiver가 거부한 Span  (백프레셔)
otelcol_receiver_accepted_metric_points  # 수락된 메트릭 포인트 수
otelcol_receiver_accepted_log_records    # 수락된 로그 레코드 수

# Processor 메트릭
otelcol_processor_dropped_spans          # Processor에서 드롭된 Spanotelcol_processor_batch_batch_send_size  # 실제 전송된 배치 크기

# Exporter 메트릭
otelcol_exporter_sent_spans              # Exporter가 전송 성공한 Spanotelcol_exporter_send_failed_spans       # 전송 실패한 Spanotelcol_exporter_queue_size              # 현재 큐에 대기 중인 항목 수
otelcol_exporter_queue_capacity          # 큐 최대 용량

핵심 알럿 공식은 다음과 같다.

  • 데이터 유실 알럿: rate(otelcol_receiver_refused_spans[5m]) > 0 -- Receiver가 데이터를 거부하고 있다면 백프레셔가 동작 중이며, 업스트림에서 재전송하지 않는 데이터는 유실될 수 있다.
  • Exporter 장애 알럿: rate(otelcol_exporter_send_failed_spans[5m]) > 0 -- 백엔드 연결 실패를 의미하며, 네트워크 또는 백엔드 상태를 점검해야 한다.
  • 큐 포화 알럿: otelcol_exporter_queue_size / otelcol_exporter_queue_capacity > 0.8 -- 큐가 80% 이상 차면 곧 데이터 드롭이 발생할 수 있다.

zPages를 활용한 실시간 디버깅

zPages 확장을 활성화하면 브라우저에서 Collector의 내부 상태를 실시간으로 확인할 수 있다.

  • /debug/servicez -- Collector 서비스 정보
  • /debug/pipelinez -- 파이프라인 구성 및 상태
  • /debug/extensionz -- 확장 상태
  • /debug/tracez -- 최근 처리된 트레이스 샘플

흔한 문제와 해결법

문제 1: Collector OOM으로 재시작 반복

원인은 대부분 Tail Sampling의 num_traces가 너무 크거나 decision_wait가 너무 길어서 메모리에 과도한 트레이스가 축적되는 경우다. num_traces를 줄이거나 decision_wait를 10-15초로 단축하고, memory_limiter의 limit_mib를 컨테이너 limit의 75%로 낮춘다.

문제 2: Exporter에서 "context deadline exceeded" 에러

백엔드 응답이 timeout 내에 오지 않는 경우 발생한다. Exporter의 timeout 값을 늘리거나, sending_queue의 num_consumers를 늘려 동시 전송을 증가시킨다. 근본적으로는 백엔드의 처리 용량을 스케일업해야 한다.

문제 3: 트레이스에서 Span이 누락됨

Tail Sampling Gateway가 여러 대인데 TraceID 기반 라우팅이 설정되지 않은 경우 발생한다. Agent에서 loadbalancing Exporter를 사용하여 TraceID 기반 일관된 해싱을 적용한다.

문제 4: Prometheus Receiver에서 "context canceled" 에러

scrape_timeout이 scrape_interval보다 크거나 같으면 발생한다. scrape_timeout을 scrape_interval의 50-80% 수준으로 설정한다.

운영 체크리스트

프로덕션 환경에서 OpenTelemetry Collector를 안정적으로 운영하기 위한 체크리스트다.

배포 전 점검

  • memory_limiter가 모든 파이프라인의 첫 번째 Processor인지 확인
  • batch Processor가 모든 파이프라인의 마지막 Processor인지 확인
  • memory_limiter의 limit_mib가 컨테이너 메모리 limit의 75-80% 이하인지 확인
  • Exporter의 sending_queue가 활성화되어 있고 적절한 크기인지 확인
  • Exporter의 retry_on_failure가 활성화되어 있는지 확인
  • health_check Extension이 설정되어 있고 liveness/readiness probe가 연결되어 있는지 확인
  • TLS 설정이 필요한 Exporter에 인증서가 마운트되어 있는지 확인
  • RBAC 권한이 올바르게 설정되어 있는지 확인 (k8sattributes 사용 시 ClusterRole 필요)

모니터링 설정

  • Collector 자체 메트릭(포트 8888)에 대한 Prometheus scrape 설정 완료
  • Grafana 대시보드 구성 (수신/드롭/전송 Span 수, 큐 사용률, 메모리 사용량)
  • 핵심 알럿 룰 설정 (refused spans, send failed, queue saturation, OOM)
  • zPages 또는 pprof Extension 활성화 (트러블슈팅용)

스케일링 전략

  • Agent DaemonSet에 적절한 resource requests/limits 설정
  • Gateway Deployment에 HPA 설정 (CPU 70%, Memory 80% 타겟)
  • Gateway에 PodDisruptionBudget 설정 (minAvailable 또는 maxUnavailable)
  • Tail Sampling 사용 시 Gateway headless Service와 loadbalancing Exporter 설정
  • Pod Anti-Affinity로 Gateway Pod가 서로 다른 노드에 분산 배치되는지 확인

보안 점검

  • 민감 속성(Authorization 헤더, DB 쿼리 등)이 attributes Processor에서 삭제/해싱되는지 확인
  • Collector 엔드포인트가 불필요하게 외부에 노출되지 않는지 확인
  • ServiceAccount에 최소 권한 원칙이 적용되어 있는지 확인
  • 설정 파일에 하드코딩된 시크릿이 없는지 확인 (환경변수 또는 Secret 사용)

실패 사례와 복구

사례 1: Tail Sampling 메모리 폭주로 인한 연쇄 장애

상황: Gateway 3대에서 Tail Sampling을 운영 중 블랙프라이데이 트래픽 급증으로 트레이스 유입량이 평소 대비 5배 증가했다. decision_wait가 30초, num_traces가 500,000으로 설정되어 있었는데, 실제 메모리에 유지된 트레이스 수가 num_traces를 초과하면서 메모리 사용량이 급등했다.

증상: Gateway Pod가 순차적으로 OOM Kill되면서 재시작을 반복했다. 재시작된 Pod로 트래픽이 집중되어 연쇄적으로 OOM이 발생하는 도미노 현상이 일어났다.

복구 절차:

  1. 긴급 대응으로 Tail Sampling을 비활성화하고 probabilistic head sampling(10%)으로 전환하여 트레이스 유입량을 즉시 감소시켰다.
  2. Gateway의 메모리 limit을 4Gi에서 8Gi로 증설하고 replicas를 3에서 6으로 확장했다.
  3. decision_wait를 30초에서 15초로 단축하고 num_traces를 200,000으로 조정한 뒤 Tail Sampling을 재활성화했다.

교훈: Tail Sampling의 메모리 사용량은 트래픽에 선형적으로 비례한다. 최대 트래픽 시나리오에 대한 부하 테스트를 반드시 수행하고, num_traces와 decision_wait를 보수적으로 설정해야 한다.

사례 2: Exporter 큐 포화로 인한 데이터 유실

상황: Tempo 백엔드의 Ingester가 디스크 풀로 인해 응답이 느려졌다. Collector의 otlp/tempo Exporter에서 timeout 에러가 발생하면서 sending queue가 빠르게 채워졌다.

증상: Exporter queue가 가득 차면서 새로운 트레이스 데이터가 드롭되기 시작했다. otelcol_exporter_send_failed_spans 메트릭이 급등하고, otelcol_exporter_queue_size가 queue_capacity에 도달했다.

복구 절차:

  1. Tempo Ingester의 디스크를 확장하고 문제가 된 Ingester Pod를 재시작했다.
  2. Collector의 sending_queue 크기를 5,000에서 20,000으로 임시 증가시켜 버퍼링 여유를 확보했다.
  3. retry_on_failure의 max_elapsed_time을 600초로 늘려 백엔드 복구 시간 동안 재시도를 유지했다.

교훈: sending_queue는 백엔드 일시 장애에 대한 완충 역할만 수행한다. 장시간 백엔드 장애 시에는 큐가 반드시 포화되므로, 백엔드 모니터링과 신속한 장애 대응이 근본적인 해결책이다. 중요 데이터는 Kafka를 중간 버퍼로 사용하여 영속성을 보장하는 아키텍처도 고려해야 한다.

사례 3: K8s Attributes Processor 권한 누락으로 Pod 메타데이터 미주입

상황: k8sattributes Processor를 설정했으나 namespace, pod name 등의 메타데이터가 텔레메트리에 주입되지 않았다.

증상: 트레이스와 로그에 k8s.namespace.name, k8s.pod.name 등의 속성이 비어 있었다. Collector 로그에 "error": "forbidden" 메시지가 출력되었다.

복구: Collector의 ServiceAccount에 Pods, Namespaces, ReplicaSets에 대한 get, list, watch 권한을 가진 ClusterRole을 바인딩하여 해결했다.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: otel-collector
rules:
  - apiGroups: ['']
    resources: ['pods', 'namespaces', 'nodes']
    verbs: ['get', 'list', 'watch']
  - apiGroups: ['apps']
    resources: ['replicasets', 'deployments', 'statefulsets', 'daemonsets']
    verbs: ['get', 'list', 'watch']
  - apiGroups: ['batch']
    resources: ['jobs', 'cronjobs']
    verbs: ['get', 'list', 'watch']
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: otel-collector
subjects:
  - kind: ServiceAccount
    name: otel-collector-agent
    namespace: observability
roleRef:
  kind: ClusterRole
  name: otel-collector
  apiGroup: rbac.authorization.k8s.io

설정 검증과 테스트

프로덕션에 배포하기 전에 Collector 설정 파일의 유효성을 검증하는 것이 중요하다. 설정 오류로 인한 Collector 시작 실패는 텔레메트리 파이프라인 전체의 중단으로 이어진다.

# 설정 파일 문법 검증
otelcol validate --config=config.yaml

# 드라이런 모드로 기동 테스트
otelcol --config=config.yaml --dry-run

# Docker를 활용한 로컬 테스트
docker run --rm \
  -v $(pwd)/config.yaml:/etc/otelcol/config.yaml \
  otel/opentelemetry-collector-contrib:0.120.0 \
  validate --config=/etc/otelcol/config.yaml

CI/CD 파이프라인에 설정 검증 단계를 포함시키면, 잘못된 설정이 프로덕션에 배포되는 것을 사전에 방지할 수 있다. Helm Chart를 사용하는 경우 helm template 렌더링 후 생성된 ConfigMap의 설정 파일에 대해 validate를 실행한다.

고급 운영 팁

멀티 테넌트 환경에서의 라우팅

여러 팀이 공유하는 Collector에서 테넌트별로 데이터를 분리하여 서로 다른 백엔드에 전송해야 하는 경우, routing Connector를 활용한다.

connectors:
  routing:
    table:
      - statement: route() where attributes["team"] == "platform"
        pipelines: [traces/platform]
      - statement: route() where attributes["team"] == "payments"
        pipelines: [traces/payments]
    default_pipelines: [traces/default]

service:
  pipelines:
    traces/ingress:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [routing]
    traces/platform:
      receivers: [routing]
      processors: [batch]
      exporters: [otlp/tempo-platform]
    traces/payments:
      receivers: [routing]
      processors: [batch]
      exporters: [otlp/tempo-payments]
    traces/default:
      receivers: [routing]
      processors: [batch]
      exporters: [otlp/tempo-default]

Collector 자체 모니터링 대시보드 핵심 패널

운영용 Grafana 대시보드에 반드시 포함해야 할 핵심 패널 목록이다.

  • 처리량 패널: 초당 수신/전송 Span, Metric Point, Log Record 수
  • 드롭율 패널: refused + dropped / accepted 비율 (0%가 정상)
  • Exporter 큐 패널: 각 Exporter의 queue_size와 capacity 비율
  • 메모리 패널: 프로세스 RSS 메모리와 memory_limiter 임계값
  • Exporter 지연 패널: 전송 소요 시간의 P50, P95, P99
  • 재시도 패널: retry 횟수와 실패율

참고자료