OTel Collector DaemonSet으로 노드 신호를 통합 처리하고 백프레셔를 흘려보내는 구조
Kubernetes 클러스터를 운영하다 보면 어느 순간 관측 데이터 수집 구조가 제각각이 되어버린 광경을 목격하게 됩니다. Prometheus는 노드 메트릭을 긁어가고, Fluentd는 로그를 따로 수집하고, 트레이스는 또 별도 에이전트가 담당하는 식이죠. 저도 팀에서 이런 구조를 물려받아 운영해본 적이 있는데, 각 에이전트의 설정을 따로 맞추고, RBAC도 각자 관리하고, 백엔드가 잠깐 다운됐을 때 어느 에이전트가 데이터를 드롭했는지 파악하는 것 자체가 일이었습니다.
OpenTelemetry Collector를 DaemonSet으로 배포하면 이 세 가지 신호를 노드당 하나의 프로세스가 처리합니다. 호스트 메트릭, kubelet 통계, 컨테이너 로그를 네트워크 홉 없이 로컬에서 수집하고, service.pipelines에는 traces·metrics·logs 세 개의 파이프라인이 각각 존재하지만 동일한 Collector 프로세스 안에서 동일한 프로세서 체인을 공유합니다. 그러면 백프레셔 제어와 메타데이터 태깅을 한 곳에서 일관되게 적용할 수 있습니다.
이 글에서는 통합 파이프라인 구성 방법, 백프레셔에 대응하는 세 가지 컴포넌트(memory_limiter, sending_queue, retry_on_failure)의 실제 동작 방향, 그리고 운영하면서 자주 밟는 지뢰들을 짚어봅니다. 2026년 1월 릴리스된 v1.49.0/v0.143.0 이후 달라진 점도 함께 다룹니다.
왜 DaemonSet이고, 왜 하나의 Collector로 묶는가
노드 로컬 접근이 핵심이다
호스트 메트릭(/proc, /sys)이나 컨테이너 로그(/var/log/pods)는 그 노드에서 직접 읽어야 합니다. Deployment로 Collector를 띄우면 이 경로에 접근하기 위해 복잡한 우회 경로가 필요하거나, 결국 각 노드에 별도 에이전트를 두어야 하는 모순에 빠집니다.
DaemonSet은 노드가 추가될 때 자동으로 Collector Pod를 배포합니다. 스케일아웃 시 관측 인프라가 자동으로 따라온다는 뜻입니다.
신호별 파이프라인은 유지하되, 프로세서를 공유한다
정확히 말하면 OTel Collector는 신호 타입별로 파이프라인을 나누도록 설계되어 있어서 traces·metrics·logs를 하나의 파이프라인 정의로 묶을 수는 없습니다. 대신 동일한 컴포넌트 ID(이름)를 세 파이프라인에서 참조하면 내부적으로 같은 인스턴스가 공유됩니다. 예를 들어 k8sattributes라는 프로세서를 traces/metrics/logs에서 모두 참조하면 동일 인스턴스가 세 신호에 동일한 k8s 메타데이터를 붙여줍니다. 반대로 k8sattributes/traces, k8sattributes/metrics처럼 서로 다른 ID를 붙이면 별개 인스턴스로 취급됩니다. 설정 내용이 같더라도 이름이 다르면 공유되지 않는다는 점이 중요합니다.
memory_limiter도 원리는 같습니다. 동일 ID를 참조하면 하나의 인스턴스가 되고, 이 프로세서는 프로세스 전체의 메모리 사용량을 기준으로 임계값 초과 여부를 판단합니다. 파이프라인마다 독립된 메모리 예산이 있는 게 아니라, 한 프로세스가 통째로 위험 수위에 도달하면 모든 파이프라인 앞단에서 수신을 거부하는 방식입니다. 그럼에도 각 파이프라인 processors 배열의 첫 번째 자리에 반드시 명시해야 하는 이유는 인스턴스가 독립이라서가 아니라, 파이프라인 내 뒤쪽 프로세서가 데이터를 확장·복제해 메모리를 소비하기 전에 차단해야 하기 때문입니다.
위 그림은 물리 구조가 아니라 논리적 공유 관계입니다. 실제 service.pipelines에는 traces/metrics/logs가 각각 존재하고, 각 파이프라인이 같은 이름의 프로세서 인스턴스를 참조한다고 이해하면 됩니다.
2-Tier 아키텍처가 지금의 권장 패턴이다
대규모 클러스터에서는 DaemonSet만으로 모든 처리를 하지 않습니다. 2026년 기준으로 **DaemonSet(경량 수집) → Deployment Gateway(집계·가공·라우팅)**의 2계층 구조가 공식 블루프린트에서 권장하는 패턴입니다.
DaemonSet은 최대한 가볍게 유지하고(수집 + 기본 태깅 + 배치 정도), 무거운 변환·필터링·테일 샘플링 로직은 Gateway로 위임합니다.
실제 구성: 코드로 보는 통합 파이프라인
Collector 설정 전문
아래는 노드 수준 메트릭·로그·트레이스를 처리하는 완전한 설정입니다.
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
hostmetrics:
collection_interval: 30s
scrapers:
cpu: {}
memory: {}
disk: {}
filesystem: {}
network: {}
kubeletstats:
collection_interval: 30s
auth_type: serviceAccount
endpoint: ${env:K8S_NODE_IP}:10250
# 아래 옵션은 kubelet 인증서를 별도 신뢰 체인에 넣지 않은 개발 환경 기준
# 프로덕션에서는 insecure_skip_verify를 끄고 CA 번들을 마운트하는 방식을 권장
insecure_skip_verify: true
filelog:
include:
- /var/log/pods/*/*/*.log
include_file_path: true
operators:
- type: container
id: container-parser
processors:
memory_limiter:
check_interval: 1s
limit_percentage: 80
spike_limit_percentage: 20
k8sattributes:
auth_type: serviceAccount
passthrough: false
extract:
metadata:
- k8s.pod.name
- k8s.namespace.name
- k8s.node.name
batch:
send_batch_size: 1000
timeout: 10s
exporters:
otlp:
endpoint: otel-gateway:4317
sending_queue:
enabled: true
num_consumers: 10
queue_size: 5000
storage: file_storage
retry_on_failure:
enabled: true
initial_interval: 5s
max_interval: 30s
max_elapsed_time: 300s
extensions:
file_storage:
directory: /var/lib/otelcol/queue
service:
extensions: [file_storage]
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, k8sattributes, batch]
exporters: [otlp]
metrics:
receivers: [otlp, hostmetrics, kubeletstats]
processors: [memory_limiter, k8sattributes, batch]
exporters: [otlp]
logs:
receivers: [otlp, filelog]
processors: [memory_limiter, k8sattributes, batch]
exporters: [otlp]여기서 processors 블록의 정의 순서가 실행 순서를 결정하는 게 아닙니다. 실제 실행 순서는 각 파이프라인 아래 processors 배열의 순서가 결정합니다. memory_limiter가 배열 첫 번째에 없으면 이미 뒤쪽 프로세서가 데이터를 부풀린 뒤에 제한이 걸리므로 OOM Kill 방어 효과가 떨어집니다.
DaemonSet 매니페스트 핵심 부분
env:
- name: K8S_NODE_IP
valueFrom:
fieldRef:
fieldPath: status.hostIP
- name: GOMEMLIMIT
value: "400MiB" # 컨테이너 limit의 약 80%
resources:
limits:
memory: 500Mi
cpu: 500m
requests:
memory: 200Mi
cpu: 100m
volumeMounts:
- name: varlogpods
mountPath: /var/log/pods
readOnly: true
- name: otelcol-queue
mountPath: /var/lib/otelcol/queue
volumes:
- name: varlogpods
hostPath:
path: /var/log/pods
- name: otelcol-queue
hostPath:
path: /var/lib/otelcol/queueGOMEMLIMIT을 컨테이너 메모리 limit의 약 80%로 설정하는 이유는 흔한 오해와 다릅니다. Go 1.19 이후 런타임은 cgroup 메모리 제한을 스스로 인식할 수 있고, OTel Collector도 최근 릴리스는 Go 1.21+ 기반이라 이 부분은 문제가 아닙니다. GOMEMLIMIT이 필요한 진짜 이유는 hard limit(OOM Kill)과 soft limit(GC 트리거)을 분리해서 제어하기 위해서입니다. 컨테이너 limit은 그대로 두고 soft limit을 그보다 낮게 잡아두면, GC가 hard limit에 도달하기 전에 적극적으로 회수를 시작해서 memory_limiter가 발동할 여유를 확보합니다. memory_limiter(수신 차단) + GOMEMLIMIT(GC 조기 트리거) + 컨테이너 limit(최종 안전망)의 3중 방어가 2026년 기준 표준화된 조합입니다.
퍼시스턴트 큐가 왜 중요한가
file_storage 익스텐션은 sending_queue의 백엔드로 동작합니다. 메모리 큐는 Collector가 재시작되면 그 안의 데이터가 사라지지만, 디스크 기반 WAL(Write-Ahead Log) 큐를 쓰면 재시작 후에도 미전송 데이터를 복구합니다. 백엔드 장기 장애를 대비할 때는 노드당 큐 크기(queue_size: 5000)와 디스크 용량 계획을 함께 산정해야 합니다.
RBAC: 어떤 컴포넌트가 무슨 권한을 요구하는가
kubeletstats와 k8sattributes는 요구하는 API 대상이 다릅니다. 한 뭉치의 ClusterRole로 몰아넣으면 트러블슈팅할 때 어느 컴포넌트에서 권한이 부족한지 파악하기 어렵습니다. 목적 단위로 나눠서 이해하는 편이 좋습니다.
kubeletstats리시버가 kubelet(포트 10250)에 직접 통신하는 경우:nodes/stats권한이 필요합니다. 이 글의 설정처럼endpoint: ${env:K8S_NODE_IP}:10250로 직접 붙는 방식이 여기에 해당합니다.kubeletstats를 API 서버 프록시 경로(/api/v1/nodes/{node}/proxy/stats/summary)로 사용하는 경우:nodes/proxy권한이 추가로 필요합니다.k8sattributes프로세서: Pod·네임스페이스 메타데이터를 룩업하기 위해pods,namespaces에 대한get/list/watch가 필요합니다.
세 컴포넌트를 함께 쓸 때의 예시입니다.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: otel-collector
rules:
# kubeletstats: kubelet 직접 통신 방식
- apiGroups: [""]
resources: ["nodes/stats"]
verbs: ["get"]
# kubeletstats를 API 서버 프록시로 쓸 때만 추가
# - apiGroups: [""]
# resources: ["nodes/proxy"]
# verbs: ["get"]
# k8sattributes: 메타데이터 룩업
- apiGroups: [""]
resources: ["pods", "namespaces"]
verbs: ["get", "list", "watch"]권한이 부족하면 리시버가 조용히 빈 데이터를 반환할 뿐 명확한 예외를 던지지 않는 경우가 많으므로, 배포 직후 Collector 자체 메트릭에서 리시버별 수신 카운트를 반드시 확인해야 합니다.
백프레셔의 두 방향을 구분해서 보기
세 컴포넌트를 흔히 "안전망"으로 뭉뚱그리기 쉬운데, 실제로는 작동 방향이 반대입니다. memory_limiter는 파이프라인 앞단에서 upstream(리시버)을 향해 수신을 거부하는 차단 방향이고, sending_queue와 retry_on_failure는 파이프라인 뒷단에서 downstream(백엔드) 실패를 흡수하는 버퍼링 방향입니다.
두 방향을 정리하면 이렇습니다.
| 컴포넌트 | 방향 | 역할 | 배치 위치 |
|---|---|---|---|
memory_limiter |
상류 차단 | 프로세스 메모리 80% 초과 시 수신 거부, OOM Kill 예방 | 각 파이프라인 processors 첫 번째 |
sending_queue |
하류 버퍼링 | 익스포터 앞 버퍼, 백엔드 장애 시 데이터 보존 | 익스포터 설정 내부 |
retry_on_failure |
하류 재시도 | 전송 실패 시 지수 백오프 재시도, 기본 최대 5분 | 익스포터 설정 내부 |
retry_on_failure와 sending_queue는 독립적으로 동작합니다. 큐 없이 재시도만 켜두면 재시도 대기 데이터가 메모리에만 존재하게 되고, 백엔드가 장기 다운되면 결국 오래된 데이터부터 드롭됩니다. 둘 다 명시적으로 활성화하고, 큐에는 가급적 file_storage를 붙여 재시작에도 살아남게 해두는 것이 실전 조합입니다.
트레이드오프와 흔한 함정
장단점 비교
| 항목 | 내용 |
|---|---|
| 노드 로컬 수집 | 네트워크 홉 없이 hostmetrics, kubeletstats, filelog에 직접 접근 |
| 데이터 중복 방지 | 노드당 Collector 1개이므로 동일 노드 메트릭 중복 수집 없음 |
| 스케일 자동화 | 노드 추가 시 DaemonSet이 Collector를 자동 배포 |
| 백프레셔 격리 | 노드 단위로 큐가 분리되어 단일 노드 장애가 전체에 영향 없음 |
| 메모리 비용 선형 증가 | 클러스터가 클수록 전체 Collector 메모리 비용이 노드 수에 비례해 증가 |
| 디스크 용량 계획 필요 | file_storage 사용 시 백엔드 장기 장애에 대비한 hostPath 여유 필요 |
| 클러스터 범위 메트릭 수집 불가 | kube-state-metrics 같은 클러스터 범위 메트릭은 별도 Deployment Collector 필요 |
실무에서 자주 빠지는 함정
1. memory_limiter 위치 오류
가장 자주 보이는 실수입니다. 아래처럼 배치하면 batch 프로세서가 데이터를 묶어 메모리를 확장한 뒤에야 memory_limiter가 동작하므로 방어 타이밍이 늦습니다.
# 잘못된 순서
processors: [k8sattributes, batch, memory_limiter]
# 올바른 순서
processors: [memory_limiter, k8sattributes, batch]2. 컴포넌트 ID를 잘못 나눠서 공유가 깨진 경우
k8sattributes나 memory_limiter를 신호별로 튜닝하고 싶어서 k8sattributes/traces, k8sattributes/metrics처럼 이름을 나눠 붙였는데, 정작 설정은 완전히 동일한 경우가 종종 있습니다. 이러면 별개 인스턴스로 실체화되어 메모리와 워처 커넥션이 두 배가 됩니다. 신호별로 다른 설정이 필요할 때만 ID를 나누는 편이 안전합니다.
3. kubelet TLS 검증 비활성화의 무비판적 복사
예시 코드의 insecure_skip_verify: true는 개발 환경 기준 값입니다. 프로덕션에서는 kubelet의 서버 인증서를 CA 번들로 마운트하고 이 옵션을 끄는 것이 원칙입니다. 노드 간 트래픽이 네트워크 정책으로 격리되어 있더라도, 이 옵션이 켜져 있는 한 kubelet 엔드포인트 위조 공격 표면이 남습니다.
2026년 1월 릴리스 이후 대시보드 마이그레이션
앞의 설정을 그대로 배포하고 나면 Collector 자체 메트릭으로 파이프라인 상태를 모니터링하게 되는데, 여기서 최근 버전 변경 사항 하나가 발목을 잡습니다.
2026년 1월 v1.49.0/v0.143.0 릴리스부터 시맨틱 컨벤션 v1.38.0을 채택하면서 신호별로 분리되어 있던 프로세서 메트릭이 otelcol_processor_incoming_items, otelcol_processor_outgoing_items로 통합됐습니다. traces/metrics/logs 구분은 otel.signal 속성으로 옮겨갔습니다. 기존 Grafana 대시보드를 그대로 가져오면 신호별 그래프가 빈 화면으로 나올 수 있으니, 업그레이드 전에 아래 항목을 훑어보시면 좋습니다.
- 프로세서 처리량 패널: 옛 메트릭 이름을
otelcol_processor_incoming_items{otel.signal="traces"}형태로 치환 - 알림 규칙(예: 드롭율 스파이크): 라벨 필터에
otel.signal추가 - 대시보드 변수: 신호를 하드코딩하지 말고
otel.signal라벨을 변수화
배포 직후 가장 먼저 확인해야 할 것들
문서로 배운 걸 그대로 붙여넣고 끝내면 반드시 며칠 뒤에 사고가 납니다. 롤아웃 직후 순서대로 다음을 확인하시는 걸 추천합니다.
- 리시버별 수신 카운트:
otelcol_receiver_accepted_*가 리시버마다 0이 아닌지 확인합니다. 0이면 대체로 RBAC 누락, hostPath 오타, kubelet 엔드포인트 도달 실패 중 하나입니다. - memory_limiter 발동 여부:
otelcol_processor_refused_*와 프로세스 RSS를 함께 봅니다. 정상 부하에서 이미 발동하고 있다면 컨테이너 limit이나GOMEMLIMIT을 재조정할 시점입니다. - 큐 잔량 추이:
otelcol_exporter_queue_size가 우상향하면 백엔드가 흡수 속도를 못 따라오고 있다는 신호입니다. Gateway 스케일업이나 배치 파라미터(send_batch_size,timeout) 조정을 검토합니다. - file_storage 디렉터리 사용량: 노드 디스크가 예상보다 빨리 차오르면
queue_size와 시간당 유입량을 곱해 최악의 케이스를 다시 계산합니다. - 드롭 카운터:
otelcol_exporter_send_failed_*가 지속적으로 증가하면retry_on_failure재시도 예산(max_elapsed_time: 300s)이 실제 백엔드 장애 회복 시간보다 짧다는 뜻일 수 있습니다.
이 다섯 개만 대시보드 한 페이지에 모아두어도 실제 사고 예방에 가장 큰 도움이 됩니다. 나머지 튜닝은 이 지표들이 흔들릴 때 그 방향을 보고 결정하시면 됩니다.
참고 자료
- OpenTelemetry 공식: Kubernetes Getting Started
- OpenTelemetry 공식: Kubernetes Collector Components
- OpenTelemetry 공식: Collector Helm Chart
- OpenTelemetry 공식: Resiliency (Persistent Queue, Retry)
- How to Deploy the OpenTelemetry Collector as a DaemonSet in Kubernetes – OneUptime
- How to Configure Backpressure-Aware Pipelines – OneUptime
- Batching, Queuing, and Retries in the OpenTelemetry Collector – Dash0
- Mastering the OpenTelemetry Memory Limiter Processor – Dash0
- OpenTelemetry Collector Backpressure: Fixing Drops with memory_limiter and Queues – Michal Drozd
- OpenTelemetry Collector Persistence and Retry Mechanisms Under the Hood – Axoflow
- OpenTelemetry Collector in Kubernetes: Architecture, Deployment & Best Practices – Groundcover
- OpenTelemetry Collector v1.49.0/v0.143.0: What's New – FusionReactor
- memory_limiter Processor GitHub README
- Improve OTel Collector Resilience using Persistent File Storage – Dynatrace Docs
- Managed Telemetry Platforms for Kubernetes Workloads Blueprint – OpenTelemetry