LLM WikiAccess-protected knowledge portal

WIKI

시계열 데이터 모델링: cardinality, retention, downsampling

모델링 실수가 DB를 죽인다 시계열 데이터베이스의 장애 원인 중 큰 비중을 차지하는 것은 하드웨어 부족이 아니라 잘못된 데이터 모델링 이다. 레이블 하나를 잘못 설계하면 시리즈 수가 수억 개로 불어나 OOM을 유발하고, 보존 정책이 없으면 디스크가 예상보다 10배 빠르게 차오른다. 이 챕터는 세 가지 핵심 개념 — 카디널리티 , 보존 정책 , 다운샘플링 — 을 중심으로 실제 운영에서 적용 가능한 모델링 원칙을 다룬다. 카디널리

경로human/study/content/time-series-databases/05-time-series-data-modeling-cardinality-retention-downsampling.md
카테고리Study
태그#cardinality #data #downsampling #modeling #mysql #retention #study

모델링 실수가 DB를 죽인다

시계열 데이터베이스의 장애 원인 중 큰 비중을 차지하는 것은 하드웨어 부족이 아니라 잘못된 데이터 모델링이다. 레이블 하나를 잘못 설계하면 시리즈 수가 수억 개로 불어나 OOM을 유발하고, 보존 정책이 없으면 디스크가 예상보다 10배 빠르게 차오른다.

이 챕터는 세 가지 핵심 개념 — 카디널리티, 보존 정책, 다운샘플링 — 을 중심으로 실제 운영에서 적용 가능한 모델링 원칙을 다룬다.


카디널리티(Cardinality): 시리즈 수의 폭발

정의

카디널리티는 TSDB에 저장된 고유 시리즈(time series)의 수를 뜻한다. 시리즈 하나는 메트릭 이름과 레이블 집합의 조합이다.

http_requests_total{service="api", status="200", region="ap-northeast-2"}
http_requests_total{service="api", status="404", region="us-east-1"}

위 두 줄은 레이블 값이 다르므로 별개의 시리즈다. 레이블 값의 가짓수가 곱해질수록 시리즈 수는 기하급수적으로 늘어난다.

시리즈 수 ≈ ∏ (각 레이블의 고유 값 수)

예: service(10) × status(10) × region(3) × endpoint(50)
  = 10 × 10 × 3 × 50 = 15,000 시리즈

카디널리티 폭발(Cardinality Explosion)

언바운드(unbounded) 레이블이 폭발의 주범이다. 요청마다 달라지는 값을 레이블에 넣으면 시리즈가 무한히 생성된다.

위험한 레이블 예시이유
user_id="u-1234567"사용자마다 시리즈 생성
request_id="abc-xyz-..."요청마다 시리즈 생성
session_token="tok-..."세션마다 시리즈 생성
url="/api/items/12345"동적 경로로 폭발
commit_sha="a3f4b2..."배포마다 시리즈 축적

메모리 영향: Prometheus 기준 100만 시리즈에 약 6.5GB RAM이 필요하고, VictoriaMetrics에서도 850MB가 필요하다. 카디널리티가 1,000만을 넘으면 대부분의 단일 노드 TSDB는 한계에 달한다.

카디널리티 진단

# Prometheus — 현재 카디널리티
curl http://localhost:9090/api/v1/label/__name__/values | jq '.data | length'

# Prometheus — 카디널리티 상위 메트릭
curl http://localhost:9090/api/v1/status/tsdb | jq '.data.seriesCountByMetricName[:10]'

# VictoriaMetrics — Cardinality Explorer (UI)
# http://vm-host:8428/vmui/#/cardinality

# 특정 메트릭의 레이블 분포
curl http://localhost:9090/api/v1/query \
  --data 'query=count by (endpoint) (http_requests_total)'

레이블 설계 원칙

레이블 카디널리티 설계 판단 기준 바운드 레이블 (권장) 고유 값 수가 수십~수백 이내 status_code = "200" | "404" | "500" → 고유 값: 10여 개 region = "ap-northeast-2" | "us-east-1" → 고유 값: 3~20개 service = "api" | "worker" | "gateway" → 고유 값: 수십 개 언바운드 레이블 (금지) 고유 값 수가 무한 증가 user_id = "u-00001" | "u-00002" | ... → 고유 값: 사용자 수만큼 폭발 request_id = "abc-def-..." (UUID) → 고유 값: 요청마다 새 시리즈 url = "/api/items/12345/detail" → 경로 파라미터로 폭발 언바운드 값이 필요한 경우 → 레이블에서 제거하고 로그나 트레이스로 이동시켜라 메트릭은 집계·통계, 로그는 개별 이벤트, 트레이스는 요청 흐름 — 각자의 역할이 다르다
레이블 설계: 바운드 vs 언바운드

실전 레이블 설계 규칙

  1. 레이블당 고유 값은 수백 이내로 제한한다. 1,000을 넘기 전에 재검토한다.
  2. 동적 경로 파라미터(/users/{id})는 패턴으로 치환한다(/users/:id).
  3. 집계 후 버킷화: user_tier = "free" | "pro" | "enterprise" 처럼 사용자 ID 대신 그룹으로 레이블을 붙인다.
  4. metric_relabel_configs로 스크랩 시 정리: 불필요한 레이블 드롭, 값 정규화.
  5. sample_limit으로 방어: 메트릭 하나가 폭발하면 스크랩 자체를 중단해 피해를 제한한다.
# Prometheus scrape 설정 예
scrape_configs:
  - job_name: 'api'
    sample_limit: 10000          # 10,000 시리즈 초과 시 스크랩 실패
    metric_relabel_configs:
      # user_id 레이블 완전 제거
      - action: labeldrop
        regex: user_id
      # 동적 URL을 패턴으로 치환
      - source_labels: [url]
        regex: '/api/items/(\d+)(.*)'
        target_label: url
        replacement: '/api/items/:id$2'

보존 정책(Retention Policy): 데이터 생명 주기 설계

보존 비용의 현실

시계열 데이터는 단조 증가한다. 보존 정책이 없으면 1초 해상도로 수집하는 메트릭 1만 개는 하루에 864GB를 생성할 수 있다(압축 전). 보존 정책은 디스크 용량을 지키는 동시에 쿼리 성능도 보호한다.

보존 기간 결정 기준

데이터 용도권장 보존 기간
실시간 알림·트러블슈팅7~15일 (원시 해상도)
단기 용량 계획·추세1~3개월
장기 추세·계절성 분석1~2년 (다운샘플링 후)
감사·규정 준수5~7년 (집계 지표만)

계층형 보존(Tiered Retention)

원시 데이터를 무한정 보관하는 것은 비용 대비 효과가 낮다. 계층형 보존은 데이터 나이에 따라 해상도를 낮추고 저비용 스토리지로 이동한다.

🔥 Hot — 로컬 SSD 보존: 0~15일 해상도: 원시(1s~15s) 용도: 실시간 알림, 트러블슈팅 비용: 높음 (NVMe SSD)
🌡 Warm — 오브젝트 스토리지 보존: 15일~3개월 해상도: 5분 집계 용도: 추세 분석, 주간 리뷰 비용: 중간 (S3 Standard)
❄️ Cold — 저비용 스토리지 보존: 3개월~수년 해상도: 1시간 집계 용도: 장기 용량 계획, 감사 비용: 낮음 (S3 IA / Glacier)
실제 데이터 감소 비율: 1초 원시 데이터 → 5분 집계 = 300분의 1, 1시간 집계 = 3,600분의 1. 1년치 데이터를 원시로 보관하면 500GB가 필요하지만, 1시간 집계만 보관하면 140MB면 충분하다.
계층형 보존 전략

다운샘플링(Downsampling): 해상도 교환으로 비용 절감

다운샘플링은 고해상도 시계열을 시간 버킷으로 집계해 포인트 수를 줄이는 기법이다. 1년 치 1초 데이터(31.5억 포인트)를 1시간 집계로 바꾸면 8,760 포인트가 된다.

Prometheus: Recording Rules

Prometheus 자체에는 자동 다운샘플링이 없다. Recording Rules로 사전 집계 결과를 새 메트릭으로 저장한다.

# prometheus/rules/downsample.yml
groups:
  - name: downsampled
    interval: 5m          # 5분마다 평가
    rules:
      # 원본: http_requests_total (초 단위 수집)
      # 5분 집계 새 메트릭 생성
      - record: job:http_requests_total:rate5m
        expr: rate(http_requests_total[5m])

      - record: job:http_request_duration_seconds:p99_5m
        expr: histogram_quantile(0.99,
                rate(http_request_duration_seconds_bucket[5m]))

주의점: Recording Rules는 원본 데이터를 자동으로 삭제하지 않는다. 원본과 집계 결과가 동시에 저장되므로, 원본의 보존 기간을 별도로 짧게 설정해야 비용 절감 효과가 있다.

Thanos: 자동 다운샘플링

Thanos Compactor는 오브젝트 스토리지에서 블록을 가져와 두 단계의 다운샘플링을 자동으로 수행한다.

원시 데이터 (≤ 40h 보존)
  ↓ Compactor
5분 집계 (aggrChunk: sum / count / min / max / counter)
  ↓ Compactor
1시간 집계

집계 블록에는 원본 타임스탬프 대신 sum, count, min, max, counter가 저장된다. avg()를 쿼리하면 Thanos가 sum / count를 계산해 반환한다. 덕분에 정확한 평균·합계·최소·최대를 집계 데이터에서도 구할 수 있다.

TimescaleDB: Continuous Aggregates

TimescaleDB는 CREATE MATERIALIZED VIEW ... WITH (timescaledb.continuous)증분 집계를 자동 관리한다. 원본 데이터가 추가되면 변경된 버킷만 재계산한다.

-- 원본 1분 데이터를 1시간으로 롤업
CREATE MATERIALIZED VIEW metrics_1h
WITH (timescaledb.continuous) AS
SELECT
  time_bucket('1 hour', time) AS bucket,
  host,
  avg(cpu_percent) AS avg_cpu,
  max(cpu_percent) AS max_cpu
FROM metrics GROUP BY bucket, host;

-- 자동 갱신
SELECT add_continuous_aggregate_policy('metrics_1h',
  start_offset => INTERVAL '3 hours',
  end_offset   => INTERVAL '1 hour',
  schedule_interval => INTERVAL '1 hour');

-- 원본 보존: 30일, 집계 보존: 1년
SELECT add_retention_policy('metrics',      INTERVAL '30 days');
SELECT add_retention_policy('metrics_1h',   INTERVAL '1 year');

메트릭 타입과 모델링 패턴

올바른 메트릭 타입 선택이 집계·다운샘플링의 정확성을 결정한다.

타입특성다운샘플링 주의점
Gauge현재 상태. 올라가고 내려감avg, min, max로 집계 가능
Counter단조 증가. 재시작 시 리셋rate() / increase() 후 집계. 리셋 감지 필수
Histogram버킷별 카운터 집계histogram_quantile은 집계 후 정확도 감소. sum/count 보존 필수
Summary클라이언트 측 분위수 계산분위수(0.99 등)는 집계 불가. 서버 측 집계 어려움

Counter 다운샘플링 함정: rate(http_requests_total[5m])는 원본 데이터에서만 의미 있다. 5분 집계 이후 집계 결과에서 다시 rate()를 쓰면 이중 미분이 된다. Thanos가 counter aggrChunk를 별도로 저장하는 이유가 여기에 있다.

Histogram 집계 문제: histogram_quantile(0.99, ...)는 버킷 경계 정보가 필요하다. 서로 다른 인스턴스의 히스토그램을 sum()으로 합친 후에도 histogram_quantile을 쓸 수 있지만, 다운샘플링으로 버킷 카운트가 손실되면 분위수 계산이 불가능해진다. 장기 보존 시 sum / count와 핵심 버킷만 남기는 방식을 설계 단계에서 결정해야 한다.


통합 설계: 카디널리티 × 보존 × 다운샘플링

Step 1: 메트릭 정의
무엇을 측정하는가? 타입(Gauge/Counter/Histogram)은? 수집 간격은?
Step 2: 레이블 설계
각 레이블의 고유 값 수를 추산한다. 합계 시리즈 수 = ∏ (고유 값 수). 언바운드 레이블은 제거 또는 버킷화.
Step 3: 보존 기간 결정
원시 보존(15일?), 집계 보존(1년?) 구분. 용도별 접근 빈도로 Hot/Cold 분리.
Step 4: 다운샘플링 계획
Recording Rules (5m, 1h) 또는 Thanos Compactor, Continuous Aggregates 중 선택. 타입별 집계 함수 확인.
Step 5: 검증·모니터링
카디널리티 Explorer로 상위 메트릭 주기 점검. 보존 정책 적용 확인. 집계 정확도 테스트.
데이터 모델링 의사결정 흐름

운영 체크리스트

카디널리티 점검
- [ ] 상위 10개 고카디널리티 메트릭 목록 보유
- [ ] 각 레이블의 고유 값 수가 1,000 이하
- [ ] sample_limit으로 개별 타깃 방어
- [ ] metric_relabel_configs로 불필요 레이블 정리

보존 정책
- [ ] 원시 데이터 보존 기간 설정 완료
- [ ] 집계 데이터 보존 기간 별도 설정
- [ ] 보존 만료 후 실제 삭제 확인 (디스크 사용량 추세 모니터링)

다운샘플링
- [ ] Recording Rules 또는 Continuous Aggregates 적용
- [ ] Counter 메트릭의 rate() 중복 적용 방지
- [ ] Histogram의 버킷 보존 정책 확인
- [ ] 집계 전후 결과 정확도 비교 테스트

References