OpenTelemetry로 비용 효율적인 멀티 클러스터 옵저버빌리티 플랫폼 구축하기(2)
들어가며…
지난 포스팅에서는 클라우드 네이티브로의 전환에 맞춰 OpenTelemetry와 CNCF 생태계의 LGTM 스택(Loki, Grafana, Tempo, Mimir)으로 옵저버빌리티 시스템을 전면 교체한 이유와 결과에 대해 설명한 바 있습니다.
이번 포스팅에서는 도입 과정에서 진행되었던 상세한 설정과 주요 시행착오 및 해결 방안에 대해 공유할 계획입니다. SaaS 플랫폼 전환 중 발생할 수 있는 비용 이슈와 성능 문제에 대한 고민이 있으시다면, 이번 포스팅이 나름의 인사이트가 될 수 있기를 바랍니다.
데이터 수집 표준, OTel Collector 상세 설정
Receiver 설정
Collector로 데이터가 들어오는 입구입니다. 저희는 다음과 같은 Receiver를 조합하여 사용했습니다.
otlp(OTLP Receiver): OpenTelemetry의 표준 프로토콜입니다. 자동 계측(Auto-instrumentation)된 애플리케이션이나 다른 Collector로부터 데이터를 받습니다.(가장 표준적인 방식)트레이스
각 앱에서 Auto-instrumentation / SDK로 Collector에 OTLP export
로그
filelog: 노드(Node)에 저장된 로그 파일(*.log)을 수집합니다.
메트릭
hostmetrics: Collector가 실행 중인 호스트(노드) 자체의 CPU, 메모리, Disk, Network 등 기본 리소스 메트릭을 수집합니다.prometheus: 가장 큰 작업은 기존 Prometheus의 메트릭 수집 작업을 OTel Collector로 이전하는 것이었습니다. OTel의prometheus리시버는 기존scrape_configs를 거의 그대로 지원했습니다.
Processor 설정
Collector의 가장 강력한 기능이 모여있는 곳입니다. 데이터가 Exporter로 가기 전에 중간 처리를 담당합니다. 저희는 다음과 같은 프로세서를 사용합니다.
batch: 데이터를 하나씩 보내지 않고, 일정 크기나 시간 동안 모아서 한 번에 보냅니다. 백엔드의 부하를 획기적으로 줄여줍니다.attributes: 데이터에k8s.cluster.name같은 추가 정보를 태깅하거나 수정합니다.metricstransform: 고카디널리티 레이블(container_id,image_id,pod_uid)을 제거하여 Mimir 의 부하를 감소시킵니다.memory_limiter: Collector가 너무 많은 데이터를 처리하다가 OOM(Out of Memory)으로 중단되는 것을 방지합니다. 반드시 설정할 것을 권장합니다.tail_sampling: 모든 트레이스를 저장하는 대신, 에러가 발생했거나 특정 조건을 만족하는 트레이스만 선별적으로 저장하여 비용을 절감합니다.filter:scp-dev테넌트의 메트릭 중 특정 메트릭은 제외하는 등, 불필요한 데이터를 전송 전에 버릴 수 있습니다.
Exporter 설정
처리된 데이터를 최종 백엔드 스토리지로 내보내는 출구입니다. 저희는 다음과 같은 Exporter 를 사용합니다.
otlphttp(OTLP HTTP Exporter): OTLP 형식의 데이터를 HTTP POST로 전송합니다. Mimir, Loki, Tempo로 데이터를 보낼 때 사용한 핵심 방식입니다.logging(로깅 Exporter): 데이터를 백엔드로 보내는 대신, Collector 자신의 로그에 출력합니다. (설정 디버깅 시 매우 유용)
otlphttp 익스포터에 헤더를 추가하여 멀티 테넌시를 구축하고, retry_on_failure (재시도) 및 sending_queue (발신 큐) 설정을 통해 데이터 유실을 방지하는 것이 핵심입니다.
# 각 클러스터의 OTel Collector 설정
exporters:
otlphttp/tempo:
headers:
X-Scope-OrgID: "scp-dev" # "scp-stag", "scp-prod" 등 환경별 고유 ID
retry_on_failure:
enabled: true
initial_interval: 5s
max_interval: 30s
max_elapsed_time: 5m
sending_queue:
enabled: true
num_consumers: 5
queue_size: 1000
otlphttp/loki:
headers:
X-Scope-OrgID: "scp-dev"
otlphttp: # Mimir (Metrics)
headers:
X-Scope-OrgID: "scp-dev"Pipelines 설정
파이프라인은 위 세 가지 구성 요소를 엮어 실제 데이터 흐름을 정의하는 설계도입니다.
OTel Collector의 가장 큰 장점은 데이터 유형(Trace, Metric, Log)별로 서로 다른 파이프라인을 정의할 수 있다는 것입니다. 즉, 여러 개의 receivers, processors, exporters를 엮어 원하는 동작을 자유롭게 설정할 수 있습니다.
아래는 저희가 사용하는 설정의 일부 예시입니다.
# config.yaml 예시
service:
pipelines:
traces:
receivers: [ otlp ]
processors: [ memory_limiter, k8sattributes, resource, transform/go, attributes/normalize_http, filter/spans, tail_sampling, batch ]
exporters: [ otlphttp/tempo, spanmetrics, servicegraph, debug/spanmetrics ]
metrics:
receivers: [ otlp, prometheus, hostmetrics ]
processors: [ memory_limiter, k8sattributes, metricstransform, resource, batch ]
exporters: [ otlphttp ]
logs:
receivers: [ otlp, filelog ]
processors: [ memory_limiter, k8sattributes, resource, batch ]
exporters: [ otlphttp/loki ]주요 설정:
memory_limiter는 어떤 처리를 하든 가장 먼저 배치하여 전체 파이프라인을 보호해야 합니다.k8sattributes,resource,attributes같은 enrichment 타입의 프로세서는 필터링, 샘플링, 배치 전에 추가되어야 후 작업에서 어떤 데이터인지 식별할 수 있습니다.filter,transform: 불필요한 데이터는 리소스 절약을 위해 샘플링 전에 미리 제거되어야 합니다.batch: 모든 처리가 끝난 최종 데이터를 묶어서 Exporter로 한 번에 보냅니다.
주요 시행착오 및 해결
Collector 배포 전략과 ‘Target Allocator’
1차 시도: DaemonSet, 그리고 40배 폭증한 메트릭
우리는 로그(filelog)와 호스트 메트릭(hostmetrics) 수집을 위해 DaemonSet 모드를 선택했습니다. 모든 노드에 Collector가 하나씩 배포되니 가장 직관적인 방식이었습니다.
하지만 Mimir로 수집되는 메트릭이 기존 Prometheus Node Exporter 대비 20배에서 40배까지 폭증했습니다.
원인은 DaemonSet으로 배포된 모든 Collector가 kubernetes-nodes-cadvisor나 kubernetes-kubelet 같은 클러스터 전역 타겟을 중복으로 스크레이핑하고 있었기 때문입니다. 14개 노드에 14개의 Collector가 있다면, Kubelet 메트릭이 14번 중복 수집된 것입니다.
이에 Collector 설정에 ${NODE_NAME}을 이용한 필터를 추가해, 각 Collector가 자신이 속한 노드의 메트릭만 수집하도록 시도했습니다.
# 1차 시도: 실패한 필터링
- source_labels: [__meta_kubernetes_node_name]
regex: ${NODE_NAME}
action: keep하지만 메트릭 이상 현상은 해결되지 않았고 메모리 사용량만 폭증했습니다.
2차 시도: StatefulSet + Target Allocator (TA)
OTel 공식 문서를 다시 확인 한 결과, OTel Collector의 Prometheus 리시버는 대규모 환경에서 수평 확장이 어렵고, 이 샤딩 문제를 해결하기 위해 Target Allocator 사용을 권장하고 있었습니다.
OTel Collector의 한계:
기본적으로 OTel Collector는 수평 확장이 불가능
동일한 엔드포인트를 여러 인스턴스가 스크레이핑할 경우 중복된 메트릭 수집이 발생
Target Allocator의 역할:
Target Allocator는 OTel Collector 인스턴스 간에 스크레이핑 대상을 분산(Consistent Hashing)
모든 엔드포인트가 한 번에 하나의 OTel Collector 인스턴스에서만 스크레이핑되도록 보장, 엔드포인트가 지속적으로 변경되는 환경에서도 효과적
Collector가 스케일 아웃되면(e.g., 3대 -> 5대), 전체 타겟을 자동으로 리밸런싱합니다.
3차 시도: DaemonSet + Target Allocator (TA)
하지만 StatefulSet은 Collector Pod를 모든 노드에 배포하는 것을 보장하지 않으므로, 각 노드의 로그 파일을 읽어야 하는 filelog 리시버와는 맞지 않았습니다. 이에 다시 DaemonSet 으로 전환했습니다.
해결책은 targetAllocator.allocationStrategy 에 있었습니다.
StatefulSet+ TA =consistent-hashing(기본값)DaemonSet+ TA =per-node
per-node 전략은 TA가 Prometheus 타겟을 수집한 뒤, 해당 타겟이 실행 중인 노드를 식별하여, "오직 그 노드 위에 떠 있는 DaemonSet Collector에게만" 스크레이핑 작업을 할당하도록 만듭니다.
최종 설정:
opentelemetryCollector:
enabled: true
name: develop
mode: daemonset
targetAllocator:
enabled: true
allocationStrategy: per-nodeCollector 상태 확인 메트릭
Target Allocator를 도입한 후에는 두 가지 메트릭을 통해 분배가 잘 되었는지 확인할 수 있습니다.
거부된 샘플
otelcol_receiver_refused_metric_points_total
# HELP otelcol_receiver_refused_metric_points_total Number of metric points that could not be pushed into the pipeline. [alpha]
# TYPE otelcol_receiver_refused_metric_points_total counter
otelcol_receiver_refused_metric_points_total{otel_scope_name="go.opentelemetry.io/collector/receiver/receiverhelper",otel_scope_version="",receiver="prometheus",transport="http"} 0Collector가 수용하지 못하고 거부한 메트릭 포인트 수입니다.
이 값이 0이 아닌 지속적으로 증가한다면, Collector의 리소스가 부족하거나
memory_limiter등에 의해 데이터가 유실되고 있다는 신호입니다.
Target Allocator 균등하게 분배되어 있는지
opentelemetry_allocator_targets_per_collector
각 Collector 인스턴스에 할당된 타겟의 수입니다.
per-node전략에서는 노드별 타겟 수에 따라 편차가 있는 것이 정상이지만,consistent-hashing전략에서는 이 값이 거의 균등해야 합니다.
Operator 버전
Target Allocator를 활성화한 후, Collector의 Prometheus 메트릭 수집이 잘 작동하지 않으면 Operator 가 사용할 이미지가 모두 동일 레포 + 같은 버전 바라보고 있지는 않은지 확인합니다.
저희의 경우 TA 는 0.127.0, Collector 0.128.0 를 사용할 때 아래와 같이 scrape pool 이 생성되지 않는 에러가 발생했습니다.
2025-06-27T05:31:27.578Z error error creating new scrape pool {"resource": {"service.instance.id": "bfa11ae0-f6ad-4d5b-97e8-088b8cd0a7f4", "service.name": "otelcol-contrib", "service.version": "0.128.0"}, "otelcol.component.id": "prometheus", "otelcol.component.kind": "receiver", "otelcol.signal": "metrics", "err": "invalid metric name escaping scheme, got empty string instead of escaping scheme", "scrape_pool": "otel-collector"}
github.com/prometheus/prometheus/scrape.(*Manager).reload
github.com/prometheus/[email protected]/scrape/manager.go:188
github.com/prometheus/prometheus/scrape.(*Manager).reloader
github.com/prometheus/[email protected]/scrape/manager.go:161이는 Prometheus 메트릭 escaping 방식이 버전별로 달라서 발생했던 것으로, 상위 버전으로 통일시켜 해결 할 수 있었습니다.
0.127.0: 구버전 Prometheus 의존성 기반. 새로운 이스케이핑 스킴 설정 옵션을 인지하지 못하거나 불완전하게 구현.
0.128.0: 최신 Prometheus 의존성 기반.
prometheusreceiver에서 이스케이핑 스킴 설정에 대한 더 엄격한 유효성 검사 시행.
예시:
opentelemetry-operator:
enabled: true
manager:
image:
repository: "otel/opentelemetry-operator"
tag: "0.131.0"
extraArgs:
- --enable-go-instrumentation=true
collectorImage:
repository: "otel/opentelemetry-collector-contrib"
tag: "0.131.0"
targetAllocatorImage:
repository: "otel/target-allocator"
tag: "0.131.0"작은 노드에서의 Collector 배포 제한
EKS medium 노드(2GB 메모리)에 Collector를 배포했을 때, Collector가 메모리 부족으로 락(lock)되면서 노드 전체가 사용 불가능해지고 재시작되는 심각한 문제가 발생했습니다.
memory limit, memory_limiter 프로세서를 설정했음에도 불구하고, 작은 노드에서는 Collector의 Graceful Shutdown 자체가 충분한 메모리를 필요로 하기 때문에 프로세스가 정상적으로 종료되지 않고 행 상태에 빠지는 문제가 있었습니다.
# Default memory limiter configuration for the collector based on k8s resource limits.
memory_limiter:
check_interval: 1s
limit_percentage: 75
spike_limit_percentage: 15이는 Graceful shutdown 자체에도 메모리가 필요하기 때문인데, 작은 노드에서는 넉넉히 리소스를 배정하는데에 문제가 있었기에 저희는 아래와 같은 규칙을 두어 메모리가 2기가보다 작은 노드에는 콜렉터를 띄우지 않도록 설정했습니다.
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: karpenter.k8s.aws/instance-size
operator: NotIn
values:
- medium
- matchExpressions:
- key: karpenter.k8s.aws/instance-memory
operator: NotIn
values:
- "2048"옵저버빌리티 컴포넌트 자체가 시스템의 병목이 되어서는 안 됩니다. Collector는 데이터를 수집하고 처리하는 중요한 역할을 하므로, 최소한 4GB 이상의 메모리를 가진 노드에만 배포하는 것을 권장합니다.
다시 한번, 결론
다시 한번 결론을 언급해보자면, Datadog에서 OpenTelemetry 기반 오픈소스 LGTM 스택으로 마이그레이션한 후, 우리는 아래의 내용을 달성할 수 있었습니다.
막대한 비용 절감(72% 감소)
더 나은 커버리지(5% → 100% APM)
벤더 종속성 탈피(오픈 표준 기반)
팀 역량 강화(전체 옵저버빌리티 스택 직접 운영)
비용 효율적인 멀티 클러스터 옵저버빌리티 플랫폼 구축을 위해 OpenTelemetry Collector 구축을 고려하는 분들께, 좋은 참고가 되길 바라며 포스팅을 마칩니다.
참고)
How to build a cost-effective observability platform with OpenTelemetry
- CNCF, 2025. 12. 16,
- Posted by Grace Park, DevOps Engineer, STCLab SRE Team