카디널리티가 왜 문제인가
Prometheus에서 하나의 시계열(time series)은 메트릭 이름과 라벨 조합으로 유일하게 식별된다. 라벨 하나가 추가되거나 라벨 값의 종류가 늘어나면 시계열 개수는 곱셈으로 증가한다. 예를 들어 http_requests_total에 path 라벨을 붙였는데 URL에 사용자 ID나 UUID가 들어가면, 요청마다 새 시계열이 생성된다. 이렇게 값의 경우의 수가 사실상 무한대인 라벨을 고카디널리티(high cardinality) 라벨이라 부른다.
시계열은 대부분 메모리의 head block에 인덱스와 함께 상주한다. 카디널리티가 폭발하면 메모리 사용량이 급증하고, 쿼리 시 인덱스 스캔 범위가 넓어져 응답이 느려지며, 최악의 경우 Prometheus가 OOM으로 재시작을 반복한다.
먼저 현황을 진단한다
추측 대신 실제 어떤 메트릭이 시계열을 많이 만드는지 확인해야 한다. Prometheus는 자기 자신의 통계를 노출하므로 다음 쿼리로 상위 메트릭을 뽑을 수 있다.
topk(10, count by (__name__)({__name__=~".+"}))
특정 메트릭 내부에서 어떤 라벨이 카디널리티를 키우는지 보려면 라벨별 값 개수를 센다.
count by (job)(
count by (job, le)(http_request_duration_seconds_bucket)
)
더 정밀한 분석에는 TSDB 관리 API가 유용하다. 라벨별 값 개수와 상위 시계열을 JSON으로 반환한다.
curl -s http://localhost:9090/api/v1/status/tsdb | jq '.data.seriesCountByMetricName[:10]'
어떤 라벨이 위험한가
값이 사실상 무한대이거나 시간에 따라 계속 새 값이 생기는 라벨이 위험하다. 아래는 실무에서 자주 사고를 내는 라벨 유형이다.
| 라벨 예시 | 문제 | 대안 |
|---|---|---|
| user_id, request_id | 사용자 수만큼 증가 | 메트릭 제거, 로그/트레이스로 이관 |
| full_path (/user/1234) | 경로마다 신규 시계열 | 라우트 패턴화 (/user/:id) |
| error_message | 메시지 문구마다 증가 | 에러 코드 enum으로 축소 |
| timestamp, ip | 무한 증가 | 라벨에서 제거 |
수집 단계에서 방어하기
가장 확실한 방법은 문제 라벨이 저장되기 전에 relabeling으로 제거하거나 정규화하는 것이다. metric_relabel_configs는 저장 직전 단계에서 동작한다.
scrape_configs:
- job_name: 'api'
static_configs:
- targets: ['api:8080']
metric_relabel_configs:
# id 라벨을 통째로 삭제
- regex: 'user_id|request_id'
action: labeldrop
# /user/123 -> /user/:id 로 정규화
- source_labels: [path]
regex: '/user/[0-9]+'
target_label: path
replacement: '/user/:id'
# 특정 노이즈 메트릭 자체를 드롭
- source_labels: [__name__]
regex: 'go_gc_duration_seconds.*'
action: drop
애플리케이션 코드에서의 라벨 설계
애초에 계측 코드에서 경로를 패턴으로 넣는 것이 근본 해법이다. 원시 URL 대신 라우트 템플릿을 라벨로 쓴다.
from prometheus_client import Counter
# 나쁜 예: request.path 그대로 사용 -> 경로마다 시계열 폭발
# 좋은 예: 라우트 패턴과 유한한 상태 코드만 라벨로
REQS = Counter(
"http_requests_total",
"HTTP requests",
["method", "route", "status"],
)
def observe(method, route_template, status):
# route_template = "/user/:id" 형태의 고정 문자열
REQS.labels(method=method, route=route_template, status=str(status)).inc()
라벨 값은 유한 집합이어야 한다는 원칙을 지킨다. 메서드(약 7종), 상태 코드(수십 종), 라우트(수십~수백 종)의 곱은 관리 가능하지만, 여기에 사용자 식별자가 끼면 즉시 무너진다.
한계와 주의점
세밀한 식별 정보가 정말 필요하다면 메트릭이 아니라 로그나 분산 트레이싱에 담아야 한다. 메트릭은 집계와 추세 관찰용이지 개별 이벤트 추적용이 아니다. relabeling으로 사후 정리는 가능하지만, 이미 생성된 시계열은 head block과 이후 블록에 남아 리텐션 기간이 지나야 사라진다.
운영 중 폭발을 조기에 잡으려면 sample_limit과 label_limit으로 타깃별 상한을 걸고, prometheus_tsdb_head_series에 알림을 설정해 급증 시점을 감지하는 것이 안전하다.