- Authors
- Name
- 들어가며
- 비용 구조 분석: 어디서 비용이 발생하는가
- 샘플링 전략: Head vs Tail vs Adaptive
- 로그 필터링 파이프라인
- 메트릭 카디널리티 관리
- 스토리지 티어링 아키텍처
- 통합 비용 최적화 아키텍처
- 장애 사례와 복구 절차
- 운영 주의사항
- 비용 최적화 체크리스트
- 트러블슈팅 가이드
- 마치며
- 참고자료

들어가며
Observability 비용이 클라우드 인프라 지출의 상위 항목으로 부상하고 있다. 2025년 글로벌 Observability 시장 규모는 285억 달러를 돌파했고, 2026년 말까지 341억 달러에 도달할 전망이다. Elastic의 2026 Observability Survey에 따르면 IT 의사결정권자의 54%가 경영진으로부터 Observability 지출에 대한 정당성 요구가 증가했다고 보고한다.
비용 폭증의 근본 원인은 데이터 볼륨에 있다. 마이크로서비스 아키텍처가 확산되면서 트레이스가 전체 Observability 비용의 6070%를 차지하고, 로그가 2030%를 점유한다. 수백 개의 서비스에서 초당 수백만 건의 스팬과 로그를 생성하면, 스토리지와 인덱싱 비용이 선형이 아닌 기하급수적으로 증가한다.
그러나 단순히 데이터를 줄이는 것이 해답은 아니다. Elastic의 동일 조사에서 70%의 조직이 데이터 삭감이 아닌 기존 지출의 최적화를 우선시한다고 응답했다. 핵심은 신호와 노이즈를 분리하여, 비용을 절감하면서도 관측 가능성의 품질을 유지하는 것이다.
이 글에서는 OpenTelemetry Collector를 중심으로 한 텔레메트리 파이프라인의 비용 최적화 전략을 다룬다. Head Sampling과 Tail Sampling의 차이와 정책 설계, 로그 필터링 파이프라인 구축, 메트릭 카디널리티 폭발 관리, 그리고 Hot/Warm/Cold 스토리지 티어링 아키텍처를 실전 설정과 코드 예시 중심으로 설명한다. 마지막으로 장애 사례와 복구 절차, 비용 최적화 체크리스트까지 포함하여, 플랫폼 엔지니어가 즉시 적용할 수 있는 가이드를 제공한다.
비용 구조 분석: 어디서 비용이 발생하는가
Observability 비용을 최적화하려면 먼저 비용이 어디서 어떻게 발생하는지 정확히 이해해야 한다. 텔레메트리 파이프라인의 비용은 크게 네 가지 축으로 분류할 수 있다.
시그널별 비용 비중
| 시그널 타입 | 비용 비중 | 주요 비용 요인 | 최적화 난이도 |
|---|---|---|---|
| Traces | 60~70% | 높은 카디널리티, 대용량 페이로드, 스팬 수 폭발 | 높음 |
| Logs | 20~30% | 비구조화 데이터, 높은 볼륨, 풀텍스트 인덱싱 | 중간 |
| Metrics | 5~15% | 타임시리즈 카디널리티, 레이블 조합 폭발 | 중간 |
| Profiles | 1~5% | CPU/메모리 프로파일 데이터 크기 | 낮음 |
비용 발생 단계별 분석
텔레메트리 데이터의 생명주기를 따라 비용이 발생하는 지점을 세분화하면 다음과 같다.
[생성] --> [수집/전송] --> [처리/가공] --> [인덱싱] --> [저장] --> [쿼리]
| | | | | |
SDK 네트워크 Collector 백엔드 스토리지 컴퓨팅
오버헤드 대역폭 CPU/메모리 인덱스 I/O 디스크/S3 쿼리 비용
비용 최적화의 핵심 원칙은 **"가능한 한 파이프라인의 앞단에서 불필요한 데이터를 제거하라"**는 것이다. 생성 단계에서 제거하면 이후 모든 비용이 절약되지만, 저장 단계에서 제거하면 그 전 단계의 비용은 이미 지불한 상태다. 이를 First-Mile Processing 또는 텔레메트리 최적화라고 부른다.
Observability 비용 공식
월간 Observability 비용을 대략적으로 산출하는 공식을 정리하면 다음과 같다.
월간 비용 = (인제스트 볼륨 x 인제스트 단가) + (저장 볼륨 x 스토리지 단가) + (쿼리 수 x 쿼리 단가)
인제스트 볼륨 = 서비스 수 x 서비스당 RPS x 스팬/로그 비율 x 평균 페이로드 크기 x 보존 기간
대규모 플랫폼에서 하루 수십억 건의 데이터 포인트를 처리하면 카디널리티가 빠르게 증가하고, 이는 Observability 스택 전체에서 가장 큰 비용 요인이 된다. 인덱스가 수백만 개의 시리즈를 포함하면 RAM과 디스크 사용량이 급증하며, 인제스트 지연과 데이터 드롭이 발생한다.
샘플링 전략: Head vs Tail vs Adaptive
샘플링은 Observability 비용 절감에서 가장 즉각적인 효과를 보이는 전략이다. 모든 트레이스를 100% 수집하는 대신, 의미 있는 데이터만 선별적으로 보존하여 볼륨을 극적으로 줄일 수 있다.
샘플링 전략 비교
| 항목 | Head Sampling | Tail Sampling | Adaptive Sampling |
|---|---|---|---|
| 결정 시점 | 트레이스 시작 시 (SDK 레벨) | 트레이스 완료 후 (Collector 레벨) | 실시간 트래픽 패턴 기반 동적 |
| 결정 근거 | 확률 또는 트레이스 ID | 전체 스팬 분석 (지연, 에러, 속성) | 서비스별 트래픽 볼륨과 에러율 |
| 비용 절감률 | 높음 (네트워크 대역폭까지 절약) | 중간 (Collector까지는 전체 전송) | 중~높음 |
| 데이터 품질 | 낮음 (에러 트레이스 누락 가능) | 높음 (에러/고지연 트레이스 100% 보존) | 높음 |
| 구현 복잡도 | 낮음 | 높음 (메모리, 라우팅 필요) | 매우 높음 |
| 메모리 사용 | 최소 | 높음 (decision_wait 동안 모든 스팬 버퍼링) | 중간 |
| 적합 시나리오 | 트래픽 극히 높은 비핵심 서비스 | 프로덕션 핵심 서비스 | 트래픽 변동이 큰 대규모 플랫폼 |
Head Sampling 설정
Head Sampling은 SDK 레벨에서 트레이스 생성 여부를 결정한다. 가장 단순하며 네트워크 대역폭까지 절약할 수 있지만, 에러나 고지연 트레이스를 놓칠 수 있다. Consistent Probability Sampling은 동일한 트레이스 ID에 대해 모든 서비스가 동일한 샘플링 결정을 내리도록 보장한다.
# OpenTelemetry Collector - Probabilistic Sampler Processor
processors:
probabilistic_sampler:
# 전체 트레이스의 10%만 샘플링
sampling_percentage: 10
# hash_seed: 트레이스 ID 해싱 시드 (다중 Collector 인스턴스에서 동일 결정을 위해 고정)
hash_seed: 22
service:
pipelines:
traces:
receivers: [otlp]
processors: [probabilistic_sampler, batch]
exporters: [otlp/tempo]
Python SDK에서 Head Sampling을 설정하는 방법은 다음과 같다.
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.sampling import TraceIdRatioBased, ParentBasedTraceIdRatio
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
# 10% Head Sampling - ParentBased는 부모 스팬의 결정을 따름
sampler = ParentBasedTraceIdRatio(0.1)
provider = TracerProvider(sampler=sampler)
# Batch processor로 효율적 전송
provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(endpoint="http://otel-collector:4317"),
max_queue_size=2048,
max_export_batch_size=512,
schedule_delay_millis=5000,
)
)
trace.set_tracer_provider(provider)
Tail Sampling 설정
Tail Sampling은 트레이스의 모든 스팬이 수집된 후에 샘플링 결정을 내린다. 에러가 발생한 트레이스, 지연이 높은 트레이스를 100% 보존하면서 정상 트레이스만 낮은 비율로 샘플링할 수 있어 데이터 품질 손실 없이 비용을 절감할 수 있다.
주의할 점은 Tail Sampling이 동작하려면 동일 트레이스의 모든 스팬이 같은 Collector 인스턴스에 도착해야 한다는 것이다. 다중 Collector 환경에서는 트레이스 ID 기반 로드밸런서 또는 groupbytrace 프로세서가 필수적이다.
# OpenTelemetry Collector - Tail Sampling Processor
# 반드시 Gateway 모드의 Collector에서 사용할 것
processors:
tail_sampling:
# 첫 번째 스팬 수신 후 결정까지 대기 시간 (트레이스 완료 대기)
decision_wait: 30s
# 메모리에 보관할 최대 트레이스 수
num_traces: 100000
# 초당 예상 신규 트레이스 수 (내부 메모리 할당 최적화)
expected_new_traces_per_sec: 1000
policies:
# 정책 1: 에러가 있는 트레이스는 100% 보존
- name: error-policy
type: status_code
status_code:
status_codes:
- ERROR
# 정책 2: 500ms 이상 지연된 트레이스는 100% 보존
- name: latency-policy
type: latency
latency:
threshold_ms: 500
# 정책 3: 특정 서비스의 트레이스 100% 보존
- name: critical-service-policy
type: string_attribute
string_attribute:
key: service.name
values:
- payment-service
- auth-service
- order-service
# 정책 4: 나머지 정상 트레이스는 5%만 샘플링
- name: normal-traffic-policy
type: probabilistic
probabilistic:
sampling_percentage: 5
# 정책 5: 복합 정책 - AND 조건
- name: composite-policy
type: and
and:
and_sub_policy:
- name: is-health-check
type: string_attribute
string_attribute:
key: http.route
values:
- /healthz
- /readyz
- /livez
- name: drop-most
type: probabilistic
probabilistic:
sampling_percentage: 0.1
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, tail_sampling, batch]
exporters: [otlp/tempo]
Tail Sampling 아키텍처: 2-Tier Collector 패턴
프로덕션 환경에서 Tail Sampling을 안정적으로 운영하려면 Agent Collector와 Gateway Collector를 분리하는 2-Tier 아키텍처가 필수적이다.
[서비스 A] --OTLP--> [Agent Collector (DaemonSet)]
[서비스 B] --OTLP--> [Agent Collector (DaemonSet)] --트레이스ID 기반 라우팅-->
[서비스 C] --OTLP--> [Agent Collector (DaemonSet)]
[Gateway Collector #1] --> [Tempo]
[Gateway Collector #2] --> [Tempo]
[Gateway Collector #3] --> [Tempo]
(Tail Sampling 수행)
Agent Collector에서는 loadbalancing exporter를 사용하여 동일 트레이스 ID의 스팬을 동일 Gateway로 라우팅한다.
# Agent Collector 설정 - LoadBalancing Exporter
exporters:
loadbalancing:
protocol:
otlp:
tls:
insecure: true
resolver:
dns:
hostname: otel-gateway-headless.observability.svc.cluster.local
port: 4317
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [loadbalancing]
로그 필터링 파이프라인
로그는 Observability 비용에서 두 번째로 큰 비중을 차지한다. 특히 디버그 로그, 헬스체크 로그, 반복적인 인프라 로그가 전체 볼륨의 대부분을 차지하면서 실질적인 분석 가치는 낮은 경우가 많다. OpenTelemetry Collector의 Filter Processor와 Transform Processor를 활용하면 파이프라인 레벨에서 불필요한 로그를 제거하고 필요한 정보만 백엔드로 전송할 수 있다.
로그 레벨 기반 필터링
프로덕션 환경에서 DEBUG, TRACE 레벨 로그를 Collector 단에서 드롭하면 일반적으로 30~50%의 로그 볼륨을 줄일 수 있다.
# OpenTelemetry Collector - Log Level Filtering
processors:
# DEBUG/TRACE 로그 드롭
filter/drop-debug:
error_mode: ignore
logs:
log_record:
- 'severity_number < SEVERITY_NUMBER_INFO'
# 특정 로그 본문 패턴 드롭 (헬스체크, readiness probe)
filter/drop-healthcheck:
error_mode: ignore
logs:
log_record:
- 'IsMatch(body, ".*GET /health.*")'
- 'IsMatch(body, ".*GET /readyz.*")'
- 'IsMatch(body, ".*GET /livez.*")'
- 'IsMatch(body, ".*kube-probe.*")'
# 특정 네임스페이스의 로그만 보존
filter/namespace:
error_mode: ignore
logs:
log_record:
- 'resource.attributes["k8s.namespace.name"] == "kube-system"'
# 속성 변환 - 불필요한 필드 제거로 페이로드 축소
transform/reduce-attributes:
error_mode: ignore
log_statements:
- context: log
statements:
- delete_key(attributes, "log.file.path")
- delete_key(attributes, "log.iostream")
- truncate_all(attributes, 256)
- limit(attributes, 20)
service:
pipelines:
logs:
receivers: [otlp, filelog]
processors:
- memory_limiter
- filter/drop-debug
- filter/drop-healthcheck
- filter/namespace
- transform/reduce-attributes
- batch
exporters: [otlp/loki]
로그 샘플링과 집계
모든 로그를 개별적으로 저장하는 대신, 반복적인 로그를 집계하여 카운트만 기록하는 전략도 효과적이다. 예를 들어, 동일한 에러 메시지가 초당 1,000건 발생하면 1건만 저장하고 카운트 속성을 추가하는 방식이다.
# OpenTelemetry Collector - Group By Attributes로 로그 집계
processors:
groupbyattrs:
keys:
- service.name
- severity_text
- log.template
# 중복 로그 집계 후 카운트 속성 추가
transform/aggregate-count:
error_mode: ignore
log_statements:
- context: log
statements:
- set(attributes["log.dedup_count"], 1) where attributes["log.dedup_count"] == nil
로그 필터링 효과 측정
필터링 정책을 적용한 후에는 반드시 효과를 측정해야 한다. Collector 자체 메트릭을 활용하면 필터링 전후의 볼륨 변화를 정량적으로 추적할 수 있다.
#!/bin/bash
# Collector 내부 메트릭으로 필터링 효과 측정
# Prometheus 엔드포인트에서 수집
# 필터링 전 수신된 로그 수
RECEIVED=$(curl -s http://localhost:8888/metrics | \
grep 'otelcol_receiver_accepted_log_records' | \
awk '{sum += $2} END {print sum}')
# 필터링 후 전송된 로그 수
EXPORTED=$(curl -s http://localhost:8888/metrics | \
grep 'otelcol_exporter_sent_log_records' | \
awk '{sum += $2} END {print sum}')
# 드롭률 계산
if [ "$RECEIVED" -gt 0 ]; then
DROP_RATE=$(echo "scale=2; (1 - $EXPORTED / $RECEIVED) * 100" | bc)
echo "수신 로그: $RECEIVED"
echo "전송 로그: $EXPORTED"
echo "드롭률: ${DROP_RATE}%"
echo "예상 비용 절감: 월간 $(echo "scale=0; $DROP_RATE * 150 / 100" | bc) USD (기준: 150 USD/월)"
fi
# 프로세서별 드롭 카운트 확인
echo ""
echo "=== 프로세서별 드롭 현황 ==="
curl -s http://localhost:8888/metrics | \
grep 'otelcol_processor_dropped_log_records' | \
sort -t' ' -k2 -rn
메트릭 카디널리티 관리
메트릭 카디널리티 폭발(Cardinality Explosion)은 Observability 비용을 예측 불가능하게 만드는 주요 원인이다. 모든 고유한 타임시리즈는 데이터베이스 인덱스에 별도의 항목을 요구하며, 수백만 개의 시리즈가 생성되면 RAM과 디스크 사용량이 급증하고 인제스트 지연과 쿼리 성능 저하가 발생한다.
카디널리티 폭발 원인
카디널리티 폭발은 일반적으로 다음과 같은 레이블 사용 패턴에서 발생한다.
| 위험 패턴 | 예시 | 카디널리티 증가 | 해결 방법 |
|---|---|---|---|
| 사용자 ID를 레이블로 | user_id="u12345" | 사용자 수만큼 시리즈 증가 | 레이블 제거, 로그/트레이스로 이동 |
| 요청 경로 원본 | path="/api/users/12345" | 무한 증가 | 경로 정규화 path="/api/users/:id" |
| Pod 이름 | pod="web-7f8c9-xk2m" | 배포마다 증가 | Deployment 이름만 사용 |
| 전체 에러 메시지 | error="Connection refused: 10.0.1.42:3306" | IP별 시리즈 생성 | 에러 코드로 분류 |
| 타임스탬프 레이블 | request_time="1709856000" | 초당 시리즈 생성 | 레이블에서 제거 |
Collector 레벨 카디널리티 제어
OpenTelemetry Collector에서 메트릭 레이블을 정리하고 카디널리티를 제어하는 설정은 다음과 같다.
# OpenTelemetry Collector - 메트릭 카디널리티 관리
processors:
# 고카디널리티 속성 제거
metricstransform/drop-high-cardinality:
transforms:
- include: http.server.request.duration
action: update
operations:
# user_id, session_id 등 고카디널리티 레이블 제거
- action: delete_label_value
label: user_id
- action: delete_label_value
label: session_id
# URL 경로 정규화
transform/normalize-paths:
error_mode: ignore
metric_statements:
- context: datapoint
statements:
- replace_pattern(attributes["url.path"], "^/api/users/[0-9]+", "/api/users/:id")
- replace_pattern(attributes["url.path"], "^/api/orders/[0-9]+", "/api/orders/:id")
- replace_pattern(attributes["url.path"], "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", ":uuid")
# 속성 수 제한
attributes/limit:
actions:
- key: http.request.header.x-request-id
action: delete
- key: http.request.header.authorization
action: delete
# 메트릭 집계 - 세밀한 레이블을 제거하고 집계된 메트릭만 전송
filter/metrics-allowlist:
error_mode: ignore
metrics:
metric:
# 특정 메트릭만 허용 (화이트리스트 방식)
- 'name == "http.server.request.duration"'
- 'name == "http.server.active_requests"'
- 'name == "process.runtime.go.goroutines"'
- 'IsMatch(name, "^system\\..*")'
service:
pipelines:
metrics:
receivers: [otlp, prometheus]
processors:
- memory_limiter
- metricstransform/drop-high-cardinality
- transform/normalize-paths
- attributes/limit
- filter/metrics-allowlist
- batch
exporters: [prometheusremotewrite/mimir]
Observability Budget 패턴
최근 대규모 조직에서 도입하는 패턴이 Observability Budget이다. 각 서비스가 방출할 수 있는 텔레메트리 데이터의 최대량을 사전에 정의하고, 예산을 초과하면 Collector가 자동으로 데이터를 드롭하는 방식이다.
#!/usr/bin/env python3
"""
Observability Budget Enforcer
서비스별 텔레메트리 예산을 모니터링하고 경고를 발생시키는 스크립트
"""
import requests
import yaml
import sys
from datetime import datetime
# 서비스별 Observability Budget 정의 (일간 기준)
BUDGETS = {
"payment-service": {
"traces_per_day": 5_000_000,
"logs_per_day": 10_000_000,
"metrics_series": 50_000,
"alert_threshold": 0.8, # 80% 도달 시 경고
},
"user-service": {
"traces_per_day": 2_000_000,
"logs_per_day": 5_000_000,
"metrics_series": 30_000,
"alert_threshold": 0.8,
},
"default": {
"traces_per_day": 1_000_000,
"logs_per_day": 3_000_000,
"metrics_series": 20_000,
"alert_threshold": 0.8,
},
}
PROMETHEUS_URL = "http://prometheus:9090"
def query_prometheus(query: str) -> float:
"""Prometheus에서 현재 값을 조회한다."""
resp = requests.get(
f"{PROMETHEUS_URL}/api/v1/query",
params={"query": query},
)
result = resp.json().get("data", {}).get("result", [])
if result:
return float(result[0]["value"][1])
return 0.0
def check_budget(service_name: str) -> dict:
"""서비스의 현재 텔레메트리 사용량을 예산 대비 확인한다."""
budget = BUDGETS.get(service_name, BUDGETS["default"])
today = datetime.now().strftime("%Y-%m-%d")
# 오늘 생성된 트레이스 수
traces_today = query_prometheus(
f'sum(increase(traces_spanmetrics_calls_total{{service_name="{service_name}"}}[24h]))'
)
# 오늘 생성된 로그 수
logs_today = query_prometheus(
f'sum(increase(loki_distributor_lines_received_total{{service="{service_name}"}}[24h]))'
)
# 현재 활성 메트릭 시리즈 수
active_series = query_prometheus(
f'count({{service_name="{service_name}"}})'
)
usage = {
"service": service_name,
"date": today,
"traces": {
"current": int(traces_today),
"budget": budget["traces_per_day"],
"usage_pct": round(traces_today / budget["traces_per_day"] * 100, 1),
},
"logs": {
"current": int(logs_today),
"budget": budget["logs_per_day"],
"usage_pct": round(logs_today / budget["logs_per_day"] * 100, 1),
},
"metrics_series": {
"current": int(active_series),
"budget": budget["metrics_series"],
"usage_pct": round(active_series / budget["metrics_series"] * 100, 1),
},
}
# 예산 초과 여부 확인
for signal_type, data in usage.items():
if isinstance(data, dict) and "usage_pct" in data:
if data["usage_pct"] >= budget["alert_threshold"] * 100:
print(
f"[WARNING] {service_name}: {signal_type} 예산 "
f"{data['usage_pct']}% 사용 ({data['current']:,} / {data['budget']:,})"
)
return usage
if __name__ == "__main__":
services = sys.argv[1:] if len(sys.argv) > 1 else list(BUDGETS.keys())
for svc in services:
if svc == "default":
continue
result = check_budget(svc)
print(f"\n=== {svc} Observability Budget ===")
for key, val in result.items():
if isinstance(val, dict):
print(f" {key}: {val['current']:,} / {val['budget']:,} ({val['usage_pct']}%)")
스토리지 티어링 아키텍처
모든 텔레메트리 데이터를 동일한 스토리지 계층에 저장하는 것은 비용 관점에서 비효율적이다. 최근 7일간의 데이터는 빠른 쿼리를 위해 고성능 스토리지에, 그 이후의 데이터는 저비용 스토리지로 이동하는 Hot/Warm/Cold 티어링이 비용 최적화의 핵심이다.
스토리지 티어 비교
| 항목 | Hot Tier | Warm Tier | Cold Tier | Archive |
|---|---|---|---|---|
| 보존 기간 | 0~7일 | 7~30일 | 30~90일 | 90일~1년+ |
| 스토리지 유형 | NVMe SSD / EBS gp3 | EBS st1 / S3 Standard | S3 Infrequent Access | S3 Glacier |
| GB당 월 비용 | $0.10~0.16 | $0.025~0.045 | $0.0125 | $0.004 |
| 쿼리 지연 | ms 단위 | 초 단위 | 분 단위 | 시간 단위 |
| 적합 데이터 | 실시간 모니터링, 알림 | 최근 장애 분석, 트렌드 | 감사, 컴플라이언스 | 법적 보존 의무 |
| 비용 절감률 (vs Hot) | 기준 | 70~75% | 87~92% | 97% |
Elasticsearch Hot-Warm-Cold 아키텍처
로그 백엔드로 Elasticsearch를 사용하는 경우 ILM(Index Lifecycle Management)을 통해 자동 티어링을 구성할 수 있다.
# Elasticsearch ILM Policy - 로그 티어링
# PUT _ilm/policy/observability-logs-policy
{
'policy':
{
'phases':
{
'hot':
{
'min_age': '0ms',
'actions':
{
'rollover': { 'max_primary_shard_size': '50gb', 'max_age': '1d' },
'set_priority': { 'priority': 100 },
},
},
'warm':
{
'min_age': '7d',
'actions':
{
'shrink': { 'number_of_shards': 1 },
'forcemerge': { 'max_num_segments': 1 },
'allocate': { 'require': { 'data': 'warm' } },
'set_priority': { 'priority': 50 },
},
},
'cold':
{
'min_age': '30d',
'actions':
{
'searchable_snapshot':
{ 'snapshot_repository': 's3-repo', 'force_merge_index': true },
'allocate': { 'require': { 'data': 'cold' } },
'set_priority': { 'priority': 0 },
},
},
'delete': { 'min_age': '365d', 'actions': { 'delete': {} } },
},
},
}
Grafana Tempo + S3 티어링
트레이스 백엔드로 Grafana Tempo를 사용하는 경우 블록 스토리지 기반 티어링을 구성할 수 있다. Tempo는 기본적으로 오브젝트 스토리지(S3)를 백엔드로 사용하므로 S3 Intelligent-Tiering을 활용하면 자동으로 비용을 최적화할 수 있다.
S3 Intelligent-Tiering은 접근 패턴에 따라 자동으로 데이터를 이동한다. 30일간 접근이 없으면 Infrequent Access 티어로 이동하여 40% 절감, 90일간 접근이 없으면 Archive Instant Access 티어로 이동하여 68% 절감을 제공한다. T-Mobile은 1.87PB 데이터 레이크에 이 전략을 적용하여 S3 비용을 40% 절감했다.
Grafana Mimir 메트릭 티어링
메트릭 백엔드로 Grafana Mimir를 사용하는 경우 compactor와 store-gateway 설정으로 데이터 보존과 다운샘플링을 조합한다.
# Mimir 설정 - 메트릭 보존 및 다운샘플링
limits:
# 원본 해상도 메트릭은 14일 보존
compactor_blocks_retention_period: 14d
compactor:
# 다운샘플링 활성화
downsample:
# 7일 이후 5분 해상도로 다운샘플
- resolution: 5m
retention: 7d
# 30일 이후 1시간 해상도로 다운샘플
- resolution: 1h
retention: 30d
store_gateway:
sharding_ring:
replication_factor: 3
# S3 백엔드 설정
blocks_storage:
backend: s3
s3:
bucket_name: mimir-metrics
endpoint: s3.ap-northeast-2.amazonaws.com
storage_class: INTELLIGENT_TIERING
bucket_store:
sync_dir: /data/mimir-sync
# 인덱스 캐시로 쿼리 성능 유지
index_cache:
backend: memcached
memcached:
addresses: dns+memcached.observability.svc:11211
통합 비용 최적화 아키텍처
지금까지 설명한 샘플링, 필터링, 카디널리티 관리, 스토리지 티어링을 통합한 전체 아키텍처를 도식화하면 다음과 같다.
[마이크로서비스 클러스터]
(SDK Head Sampling 10%)
|
v
+-------------------------------+
| Agent Collector (DaemonSet) |
| - Memory Limiter |
| - Filter/drop-debug |
| - Filter/drop-healthcheck |
| - Attributes/limit |
+-------------------------------+
| | |
Traces Logs Metrics
| | |
v v v
+---------------+ +---------+ +-----------+
| Gateway | | Gateway | | Gateway |
| Collector | | (Logs) | | (Metrics) |
| (Traces) | | | | |
| - Tail Sampling| | - Group | | - Cardina |
| - LoadBalance | | ByAttr| | lity |
+-------+--------+ +----+----+ +-----+-----+
| | |
v v v
+-------+--------+ +------+------+ +----+------+
| Tempo | | Loki | | Mimir |
| (S3 + Intelli- | | (S3 + ILM) | | (S3 + |
| gent Tiering) | | | | Downsample|
+----------------+ +-------------+ +-----------+
| | |
v v v
+--------------------------------------------------+
| Grafana Dashboard |
| - 실시간 모니터링 (Hot Tier) |
| - 장애 분석 (Warm Tier) |
| - 컴플라이언스 감사 (Cold/Archive Tier) |
+--------------------------------------------------+
이 아키텍처를 적용하면 다음과 같은 비용 절감 효과를 기대할 수 있다.
| 최적화 레이어 | 적용 기법 | 예상 볼륨 감소 | 비용 절감 효과 |
|---|---|---|---|
| SDK Head Sampling | 10% 확률 샘플링 | 트레이스 90% 감소 | 네트워크 + 스토리지 비용 대폭 절감 |
| Agent 로그 필터링 | DEBUG/헬스체크 드롭 | 로그 30~50% 감소 | 인제스트 + 스토리지 비용 절감 |
| Gateway Tail Sampling | 에러/고지연 보존 + 정상 5% | 나머지 트레이스 95% 감소 | 최종 스토리지 비용 절감 |
| 카디널리티 제어 | 고카디널리티 레이블 제거 | 시리즈 수 50~70% 감소 | 인덱싱 + 쿼리 비용 절감 |
| 스토리지 티어링 | Hot/Warm/Cold + S3 IT | 없음 (동일 데이터, 저비용 저장) | 스토리지 비용 40~68% 절감 |
종합적으로 이 전략들을 모두 적용하면 전체 Observability 비용을 60~80% 절감하면서도 에러 트레이스와 이상 징후에 대한 관측 가능성은 100% 유지할 수 있다.
장애 사례와 복구 절차
비용 최적화 전략을 적용할 때 발생할 수 있는 장애 시나리오와 그 복구 방법을 사전에 파악해두어야 한다.
장애 사례 1: Tail Sampling OOM (Out of Memory)
증상: Gateway Collector가 반복적으로 OOM으로 재시작된다. decision_wait 동안 메모리에 축적되는 스팬 수가 num_traces 한도를 초과하면서 메모리가 급증한다.
원인: 트래픽 급증 시 num_traces와 decision_wait 설정이 메모리 용량을 초과한다. 예를 들어 decision_wait: 60s에 초당 10,000개의 새로운 트레이스가 유입되면 최대 600,000개의 트레이스를 메모리에 보관해야 한다.
복구 절차:
decision_wait를 30s 이하로 줄인다 (트레이스 완료율과 트레이드오프).num_traces를 메모리 용량에 맞춰 조정한다. 경험적으로 트레이스당 약 1~5KB로 추정하여 계산한다.memory_limiter프로세서를 Tail Sampling 앞에 배치하여 메모리 임계값 초과 시 데이터를 드롭하도록 한다.- Gateway Collector의 리소스를 수직 확장하거나 인스턴스 수를 수평 확장한다.
# OOM 방지를 위한 Memory Limiter + Tail Sampling 조합
processors:
memory_limiter:
check_interval: 1s
limit_mib: 3800 # 4GB 컨테이너에서 200MB 여유
spike_limit_mib: 800 # 일시적 버스트 허용
tail_sampling:
decision_wait: 20s # 30s에서 20s로 축소
num_traces: 50000 # 메모리 기반 계산
expected_new_traces_per_sec: 2500
policies:
- name: error-always
type: status_code
status_code:
status_codes: [ERROR]
- name: latency-always
type: latency
latency:
threshold_ms: 500
- name: probabilistic-default
type: probabilistic
probabilistic:
sampling_percentage: 5
service:
pipelines:
traces:
receivers: [otlp]
# memory_limiter가 반드시 첫 번째
processors: [memory_limiter, tail_sampling, batch]
exporters: [otlp/tempo]
장애 사례 2: 과도한 필터링으로 인한 블라인드 스팟
증상: 프로덕션에서 장애가 발생했는데 관련 로그와 트레이스가 없어 원인을 분석할 수 없다. 필터링 정책이 너무 공격적으로 설정되어 핵심 데이터까지 드롭되었다.
원인: 로그 필터가 정규식 패턴을 너무 넓게 잡아 관련 로그까지 매칭시키거나, 메트릭 레이블 제거가 과도하여 특정 인스턴스의 이상을 식별하지 못하는 경우 발생한다.
복구 절차:
- 필터링 정책 변경 시 반드시 Shadow Mode를 먼저 적용한다. 실제로 드롭하지 않고 드롭될 데이터의 카운트만 기록하여 영향도를 사전에 평가한다.
- 핵심 서비스(결제, 인증, 주문)에 대해서는 필터링 예외 규칙을 적용한다.
error_mode: ignore를 활용하여 조건 평가 실패 시에도 데이터를 드롭하지 않도록 한다.
장애 사례 3: 카디널리티 폭발로 인한 Mimir/Prometheus 인제스트 실패
증상: Prometheus 또는 Mimir에서 too many active series 에러가 발생하며 메트릭 인제스트가 거부된다. Grafana 대시보드에 빈 패널이 나타난다.
원인: 새로운 서비스 배포 시 레이블 값에 고유 식별자(UUID, 타임스탬프, Pod 이름 등)가 포함되어 시리즈 수가 폭발적으로 증가한다.
복구 절차:
- 즉시 Collector의
metricstransform프로세서로 문제 레이블을 드롭한다. - Mimir의
max_global_series_per_user한도를 임시로 상향한다. - 원인이 된 서비스의 SDK 계측 코드를 수정하여 고카디널리티 속성을 제거한다.
- 장기적으로 CI/CD 파이프라인에 카디널리티 검증 스텝을 추가한다.
장애 사례 4: 티어링 전환 중 쿼리 실패
증상: ILM 정책에 의해 인덱스가 Warm이나 Cold 티어로 전환되는 동안 해당 시간대의 로그 쿼리가 실패하거나 타임아웃된다.
원인: 티어 전환 중 샤드 재배치, 포스 머지, 스냅샷 생성 등의 작업이 클러스터 리소스를 점유하여 쿼리 성능이 저하된다.
복구 절차:
- ILM 전환 작업을 트래픽이 낮은 시간대(새벽 2~5시)로 스케줄링한다.
- 전환 중에도 쿼리가 가능하도록
searchable_snapshot을 활용한다. - Warm/Cold 노드의 리소스를 충분히 확보하여 전환 작업이 빠르게 완료되도록 한다.
운영 주의사항
비용 최적화 전략을 적용할 때 반드시 유의해야 할 사항들을 정리한다.
샘플링 관련
- Tail Sampling의
decision_wait를 너무 짧게 설정하면 늦게 도착하는 스팬이 누락되어 불완전한 트레이스가 저장된다. 일반적으로 서비스 간 최대 지연의 2배 이상으로 설정한다. - Head Sampling과 Tail Sampling을 동시에 적용하면 SDK에서 이미 드롭한 트레이스는 Tail Sampling에서 복구할 수 없다. 두 전략을 함께 사용할 때는 Head Sampling 비율을 보수적으로(50% 이상) 설정한다.
- Tail Sampling Gateway를 스케일아웃할 때 반드시 트레이스 ID 기반 라우팅(LoadBalancing Exporter 또는 Consistent Hashing)을 유지해야 한다. 잘못된 라우팅은 동일 트레이스의 스팬이 다른 인스턴스로 분산되어 샘플링 결정이 왜곡된다.
필터링 관련
- 필터링 정책을 변경할 때는 반드시 점진적으로 적용한다. 카나리 파이프라인에서 먼저 테스트하고, 드롭될 데이터의 특성을 검증한 후 전체에 배포한다.
- 에러 레벨 로그는 절대 필터링하지 않는다. ERROR, FATAL 레벨의 로그를 드롭하면 장애 대응 시 치명적인 블라인드 스팟이 발생한다.
- Collector 자체 메트릭(
otelcol_processor_dropped_*)을 반드시 모니터링한다. 드롭률이 예상 범위를 벗어나면 즉시 알림이 발생하도록 설정한다.
카디널리티 관련
- 새로운 서비스 배포 전에 카디널리티 영향 분석을 수행한다. CI/CD 파이프라인에 메트릭 레이블의 예상 카디널리티를 검증하는 스텝을 추가한다.
- Recording Rule을 활용하여 자주 사용하는 고카디널리티 쿼리를 사전 집계한다. 이렇게 하면 쿼리 시점의 연산 비용과 시간을 줄일 수 있다.
티어링 관련
- Cold Tier에서 데이터를 조회할 때 비용이 발생한다. S3 Glacier에서의 복원 요청은 건당 비용이 부과되므로, 빈번한 Cold 조회가 예상되면 S3 Intelligent-Tiering의 Archive Instant Access 티어를 활용한다.
- 보존 기간 정책은 법적/규제 요구사항을 반드시 확인한 후 설정한다. 금융, 의료 등의 규제 산업에서는 특정 기간 동안 데이터 보존이 법적으로 의무화되어 있다.
비용 최적화 체크리스트
다음 체크리스트를 활용하여 현재 Observability 파이프라인의 비용 최적화 수준을 점검할 수 있다.
Phase 1: 즉시 적용 가능한 Quick Wins (1~2주)
- 프로덕션 환경에서 DEBUG/TRACE 레벨 로그를 Collector에서 드롭하고 있는가?
- 헬스체크, readiness probe 관련 로그와 트레이스를 필터링하고 있는가?
- 메트릭 레이블에 사용자 ID, 요청 ID 등 고유 식별자가 포함되어 있지 않은가?
- URL 경로 레이블이 정규화되어 있는가? (
/api/users/123대신/api/users/:id) - Collector에
memory_limiter프로세서가 설정되어 있는가? - Collector 내부 메트릭(드롭률, 큐 크기, 메모리 사용량)을 모니터링하고 있는가?
Phase 2: 중기 최적화 (2~4주)
- Tail Sampling 정책이 에러/고지연 트레이스를 100% 보존하면서 정상 트래픽을 5~10%로 제한하는가?
- 2-Tier Collector 아키텍처(Agent + Gateway)가 구성되어 있는가?
- 트레이스 ID 기반 라우팅(LoadBalancing Exporter)이 설정되어 있는가?
- 로그 속성에서 불필요한 필드를 제거하고 있는가? (파일 경로, 스트림 유형 등)
- 서비스별 Observability Budget이 정의되어 있는가?
- 카디널리티 모니터링 대시보드가 구축되어 있는가?
Phase 3: 장기 인프라 최적화 (1~3개월)
- Hot/Warm/Cold 스토리지 티어링이 구성되어 있는가?
- S3 Intelligent-Tiering 또는 동급 자동 티어링이 활성화되어 있는가?
- 메트릭 다운샘플링 정책이 적용되어 있는가? (7일 이후 5분, 30일 이후 1시간 해상도)
- ILM/보존 정책이 규제 요구사항을 충족하는가?
- CI/CD 파이프라인에 카디널리티 검증 게이트가 포함되어 있는가?
- 비용 모니터링 대시보드에서 시그널별, 서비스별 비용 추세를 추적하고 있는가?
- Adaptive Sampling으로의 전환을 검토했는가?
트러블슈팅 가이드
Collector 메모리 사용량이 계속 증가하는 경우
# 1. Collector 메모리 사용량 확인
kubectl top pods -n observability -l app=otel-gateway
# 2. pprof로 메모리 프로파일링 (Extension 활성화 필요)
curl -s http://localhost:1777/debug/pprof/heap > heap.prof
go tool pprof -top heap.prof
# 3. zPages에서 파이프라인 상태 확인
curl -s http://localhost:55679/debug/tracez | jq .
# 4. Tail Sampling 큐 상태 확인
curl -s http://localhost:8888/metrics | grep tail_sampling
필터링 정책이 예상대로 동작하지 않는 경우
error_mode를propagate로 변경하여 에러를 로그에 출력한다.debugexporter를 파이프라인에 추가하여 필터링 후 남은 데이터를 확인한다.- OTTL(OpenTelemetry Transformation Language) 표현식의 문법을 재확인한다. 특히
body와attributes접근 방식의 차이에 주의한다.
# 디버깅용 파이프라인 설정
exporters:
debug:
verbosity: detailed
sampling_initial: 5
sampling_thereafter: 200
service:
pipelines:
logs/debug:
receivers: [otlp]
processors: [filter/drop-debug]
exporters: [debug]
카디널리티 폭발의 원인 서비스를 찾는 방법
# Prometheus에서 카디널리티 상위 메트릭 확인
curl -s http://prometheus:9090/api/v1/status/tsdb | \
jq '.data.seriesCountByMetricName | sort_by(-.value) | .[0:10]'
# 특정 메트릭의 레이블 카디널리티 분석
curl -s 'http://prometheus:9090/api/v1/query?query=count(http_server_request_duration_seconds_bucket) by (service_name)' | \
jq '.data.result | sort_by(-.value[1] | tonumber) | .[0:10]'
# Mimir에서 테넌트별 활성 시리즈 수 확인
curl -s http://mimir:8080/api/v1/cardinality/active_series | \
jq '.data | sort_by(-.active_series) | .[0:10]'
마치며
Observability 비용 최적화는 단순히 데이터를 줄이는 것이 아니라, 신호와 노이즈를 정밀하게 분리하여 관측 가능성의 품질을 유지하면서 비용 효율을 극대화하는 엔지니어링 활동이다.
이 글에서 다룬 전략을 요약하면 다음과 같다.
- 샘플링: Head Sampling으로 네트워크 비용까지 절감하되, 핵심 서비스에는 Tail Sampling을 적용하여 에러와 고지연 트레이스를 100% 보존한다.
- 필터링: DEBUG/헬스체크 로그를 Collector 단에서 드롭하고, 속성 정리로 페이로드 크기를 줄인다.
- 카디널리티 관리: 고카디널리티 레이블을 제거하고, URL 경로를 정규화하며, Observability Budget으로 서비스별 사용량을 통제한다.
- 스토리지 티어링: Hot/Warm/Cold 아키텍처로 데이터의 나이에 따라 저비용 스토리지로 자동 이동시킨다.
이 전략들을 체계적으로 적용하면 전체 Observability 비용을 60~80% 절감하면서도 장애 대응에 필요한 데이터는 완전히 보존할 수 있다. 비용 최적화는 일회성 작업이 아니라, 서비스 성장과 함께 지속적으로 조정해야 하는 운영 프로세스다. 체크리스트를 정기적으로 점검하고, Collector 내부 메트릭과 비용 추세를 지속적으로 모니터링하여 최적의 균형점을 유지하는 것이 핵심이다.
참고자료
- OpenTelemetry Sampling 공식 문서
- OpenTelemetry Collector Contrib - Tail Sampling Processor
- OpenTelemetry Collector Contrib - Filter Processor
- OpenTelemetry Sampling Milestones (2025)
- ClickHouse - A Practical Guide to Observability TCO and Cost Reduction
- Netdata - Metric Cardinality in Observability Platforms
- Logz.io - How to Optimize Your Observability Spend
- Amazon S3 Intelligent-Tiering Storage Class
- Grafana Cloud - Reduce Application Observability Costs
- OpenTelemetry Collector Configuration