- Authors
- Name
- 들어가며
- OpenTelemetry Collector 아키텍처
- Receiver 설정
- Processor 파이프라인
- Exporter 설정
- Agent vs Gateway 배포 패턴
- Kubernetes 환경 배포
- 메모리 관리와 백프레셔
- 트러블슈팅 가이드
- 운영 체크리스트
- 실패 사례와 복구
- 설정 검증과 테스트
- 고급 운영 팁
- 참고자료

들어가며
분산 시스템의 복잡도가 증가할수록 관측 가능성(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는 두 가지 배포판을 제공한다.
| 항목 | Core | Contrib |
|---|---|---|
| 포함 컴포넌트 | 핵심 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 타입 | 방식 | 시그널 | 주요 용도 |
|---|---|---|---|
| otlp | Push | Traces, Metrics, Logs | OTel SDK 계측 애플리케이션 |
| prometheus | Pull | Metrics | Prometheus 호환 메트릭 스크래핑 |
| filelog | Pull | Logs | 컨테이너/파일 로그 수집 |
| hostmetrics | Pull | Metrics | CPU, Memory, Disk, Network 호스트 메트릭 |
| k8s_events | Pull | Logs | Kubernetes 이벤트 수집 |
| kafka | Push | Traces, Metrics, Logs | Kafka 토픽에서 텔레메트리 소비 |
| zipkin | Push | Traces | Zipkin 포맷 트레이스 수신 |
| jaeger | Push | Traces | Jaeger 포맷 트레이스 수신 |
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_limiter | OOM 방지 | 필수 | 최우선 (첫 번째) |
| k8sattributes | K8s 메타데이터 주입 | 권장 | memory_limiter 다음 |
| resourcedetection | 클라우드/호스트 정보 주입 | 권장 | k8sattributes 다음 |
| attributes | 속성 추가/수정/삭제 | 선택 | 중간 |
| filter | 불필요 데이터 드롭 | 선택 | 샘플링 전 |
| tail_sampling | 트레이스 기반 샘플링 | 선택 (traces) | batch 전 |
| transform | OTTL 기반 변환 | 선택 | 상황에 따라 |
| 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 Tempo | Traces | OTLP gRPC | 오브젝트 스토리지, 비용 효율 | otlp |
| Grafana Mimir | Metrics | Prometheus Remote Write | Prometheus 호환, 멀티 테넌트 | prometheusremotewrite |
| Grafana Loki | Logs | HTTP Push | 레이블 기반 인덱싱, 저비용 | loki |
| Jaeger | Traces | OTLP gRPC | 트레이스 전용, UI 내장 | otlp |
| Elasticsearch | Logs | HTTP | 풀텍스트 검색 강점 | elasticsearch |
| SigNoz | All | OTLP gRPC | 올인원 솔루션, ClickHouse 기반 | otlp |
| Datadog | All | HTTP | SaaS, 풍부한 통합 | 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가 수락한 Span 수
otelcol_receiver_refused_spans # Receiver가 거부한 Span 수 (백프레셔)
otelcol_receiver_accepted_metric_points # 수락된 메트릭 포인트 수
otelcol_receiver_accepted_log_records # 수락된 로그 레코드 수
# Processor 메트릭
otelcol_processor_dropped_spans # Processor에서 드롭된 Span 수
otelcol_processor_batch_batch_send_size # 실제 전송된 배치 크기
# Exporter 메트릭
otelcol_exporter_sent_spans # Exporter가 전송 성공한 Span 수
otelcol_exporter_send_failed_spans # 전송 실패한 Span 수
otelcol_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이 발생하는 도미노 현상이 일어났다.
복구 절차:
- 긴급 대응으로 Tail Sampling을 비활성화하고 probabilistic head sampling(10%)으로 전환하여 트레이스 유입량을 즉시 감소시켰다.
- Gateway의 메모리 limit을 4Gi에서 8Gi로 증설하고 replicas를 3에서 6으로 확장했다.
- 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에 도달했다.
복구 절차:
- Tempo Ingester의 디스크를 확장하고 문제가 된 Ingester Pod를 재시작했다.
- Collector의 sending_queue 크기를 5,000에서 20,000으로 임시 증가시켜 버퍼링 여유를 확보했다.
- 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 횟수와 실패율
참고자료
- OpenTelemetry Collector Configuration -- 공식 설정 레퍼런스. Receiver, Processor, Exporter의 모든 옵션을 참조할 수 있다.
- OpenTelemetry Collector Architecture -- 공식 아키텍처 문서. 내부 데이터 흐름과 컴포넌트 간 상호작용을 설명한다.
- OpenTelemetry Collector Complete Guide - SigNoz -- SigNoz에서 제공하는 실전 가이드. 다양한 설정 예제와 벤치마크를 포함한다.
- OpenTelemetry Collector Guide - Dash0 -- Dash0에서 제공하는 Collector 운영 가이드. 배포 패턴과 프로덕션 팁을 다룬다.
- Build Logs-to-Metrics Pipelines - Last9 -- 로그에서 메트릭을 생성하는 파이프라인 구축 가이드. Connector를 활용한 시그널 변환을 설명한다.