핵심 요약: 카디널리티 폭발은 데이터 품질 문제가 아니라 과금·메모리·쿼리 성능을 동시에 흔드는 구조적 비용 문제다. active series 기반 과금 구조에서는 라벨 조합 하나가 그대로 청구액과 head block 메모리로 전가된다. 해법은 metrics에 고해상도 원본을 넣지 않고, 원본은 logs/traces로, metrics에는 저카디널리티 집계만 남기는 역할 분리다.

왜 라벨 하나가 청구서와 메모리를 동시에 흔들었나?

결론부터 말하면, 원인은 새로운 트래픽 증가가 아니라 메트릭에 붙은 고카디널리티 라벨 하나였다. 배포 자체는 정상이었고 트래픽 볼륨도 평소와 크게 다르지 않았는데, active series 수만 비정상적으로 늘었다.

이 글의 핵심은 아래로 요약된다.

  • 카디널리티 증가는 저장비용·쿼리비용·인덱스/메모리 부담을 동시에 키운다.
  • Prometheus는 active series가 늘수록 head block 메모리부터 먼저 압박받는다.
  • 상용 백엔드는 이 부담을 대부분 과금으로 그대로 전가한다.
  • eBPF 무계측 수집은 배포 마찰을 줄이지만 비즈니스 라벨링의 대체재는 아니다.
  • 실용적 해법은 수집 이전 단계에서 고카디널리티 속성을 차단하는 것이다.

증상은 이랬다. 배포 다음 날 오전, Prometheus 인스턴스의 메모리 사용량이 평소 대비 큰 폭으로 늘었다. 같은 주기에 Grafana Cloud 청구서의 active series 항목이 급등했는데, 과금 기준 자체가 active series이고 10,001–100,000 구간에서 1,000 active series당 $6.50가 부과되는 구조라, 시계열 수 증가가 곧바로 금액 증가로 이어졌다Grafana Cloud Pricing. 처음에는 단순 트래픽 증가로 보고 넘겼지만, 이틀 뒤 대시보드 쿼리 응답 시간까지 눈에 띄게 느려지면서 원인 조사에 들어갔다.

트래픽이 늘어서 그런 것 아닌가?

기각. 요청 수(RPS)는 배포 전후로 거의 동일했다. 만약 순수 트래픽 증가였다면 샘플 수(samples ingested)는 늘어도 unique series 수는 크게 늘지 않아야 한다. AWS Managed Service for Prometheus의 과금은 1,000만 samples ingested당 $0.90로 트래픽에 비례하는데, 실제로는 samples ingested 증가폭보다 active series 증가폭이 훨씬 컸다AWS AMP Pricing. 즉 같은 요청 수라도 라벨 조합이 늘어난 것이었다.

특정 익스포터가 중복 수집한 것 아닌가?

기각. 익스포터 로그와 스크레이프 설정을 확인했지만 중복 타깃은 없었다. 문제는 수집 경로가 아니라 메트릭에 붙는 라벨 자체의 차원이었다. high-cardinality 속성은 10개 정도만 붙어도 수백만 개의 unique series로 폭증할 수 있다는 설명이 있는데, 이번 사례도 규모만 작았을 뿐 같은 패턴이었다.

eBPF 기반 수집기가 노드를 늘려서 그런 것 아닌가?

기각. 최근 도입한 eBPF 계열 수집기도 의심 대상이었지만, 이 계열 플랫폼의 과금은 보통 series 수가 아니라 node 수에 묶여 있어 series 폭증과는 별개 구조였다. eBPF는 커널 레벨에서 애플리케이션 코드 삽입 없이 텔레메트리를 얻는 대신, user_id·tenant_id 같은 애플리케이션 도메인 라벨을 풍부하게 붙이는 방식과는 거리가 있다. 실제로 이번 폭증은 eBPF 경로가 아니라 애플리케이션 계측 코드에서 붙인 커스텀 메트릭 쪽에서 발생했다.

무엇이 실제 원인이었나?

원인은 특정 API 핸들러에서 요청 처리 지연 메트릭에 request_id를 라벨로 그대로 붙인 코드였다. 의도는 특정 느린 요청을 추적하려는 것이었지만, request_id는 요청마다 유일한 값이라 메트릭 시계열이 요청 수만큼 새로 생성됐다. 결과적으로 서비스·엔드포인트 단위로 몇 백 개 수준이어야 할 series가 수십만 단위로 불어났고, Prometheus head block 메모리가 100만 active series당 4–6GB 수준까지 압박받는다는 경험칙에 근접하는 지점까지 갔다.

무엇을 바꿨나?

첫 조치는 해당 메트릭에서 request_id 라벨을 즉시 제거하는 것이었다. 이후 재발 방지를 위해 Collector 단계에 라벨 화이트리스트를 걸어, 사전에 정의되지 않은 고카디널리티 속성은 수집 시점에 drop되도록 했다. 그리고 요청 단위 추적이 필요한 정보는 metrics가 아니라 로그와 트레이스로 옮겼다. user_id, request_id처럼 유일성이 높은 필드는 metrics에서 빼고, 필요하면 trace span 속성이나 로그 필드로만 남기는 방식이다. 이는 원시 고카디널리티는 logs/traces에, metrics에는 서비스·엔드포인트·상태 코드 같은 안정적 차원만 남기는 분리 원칙과 일치한다.

일반화 가능한 교훈

이 사고에서 얻을 수 있는 교훈은 스택과 무관하게 적용된다. 메트릭에 라벨을 추가하기 전에 "이 라벨의 unique 값 개수가 요청 수·사용자 수만큼 늘어나는가"를 먼저 물어야 한다는 것이다. 답이 그렇다면 그 정보는 metrics가 아니라 logs나 traces의 몫이다. 비용 통제의 핵심은 결국 "무엇을 잃어도 되는가"를 미리 명시하는 데 있다. user·session·request 수준 질문이 실제로 필요한지, 아니면 서비스·지역·상태 코드 수준의 운영 지표만으로 충분한지에 따라 설계는 완전히 달라진다. 2026년 기준으로 봐도 이 원칙은 크게 바뀌지 않았고, 오히려 cardinality-driven billing이 일반화되면서 더 중요해지고 있다.

핵심 정리

  • 카디널리티 폭발의 진짜 위험은 저장용량이 아니라 ingestion·processing·query 전 구간의 비용 증가다.
  • Prometheus 환경에서는 active series 증가가 head block 메모리를 먼저 압박하고, 상용 백엔드는 그 부담을 과금으로 전가한다.
  • eBPF 무계측 수집은 배포 마찰을 줄이지만 애플리케이션 도메인 라벨을 풍부히 붙이는 해법은 아니며, 과금도 보통 node 수 기준이다.
  • request_id·user_id 같은 유일성 높은 필드는 metrics가 아니라 logs/traces에 두고, metrics에는 저카디널리티 차원만 남겨야 한다.
  • 가장 실용적인 대응은 수집 이전 단계에서 고카디널리티 속성을 drop하거나 bucketize하는 것이다.

더 알아보기

관측성 파이프라인 설계: 메트릭·로그·트레이스 수집의 실제 — 이 주제의 종합 가이드