TL;DR
- 시계열 데이터의 특성: 쓰기 압도적, 시간 순서, 거의 변경 없음, 시간 범위 쿼리, 집계가 핵심
- 5대 옵션: TimescaleDB(PG 확장), InfluxDB, Prometheus, ClickHouse, VictoriaMetrics
- 카디널리티가 모든 것: 레이블 폭증 시 성능이 무너짐. 설계 단계에서 신중히
- 압축이 핵심: Gorilla(Facebook)가 표준. 12배 압축으로 디스크 비용 절감
- 다운샘플링과 보존: 오래된 데이터는 낮은 해상도로 + 자동 삭제
1. 시계열 데이터의 본질
1.1 시계열 데이터란?
2025-04-15 10:00:00, sensor_1, temperature, 23.5
2025-04-15 10:00:01, sensor_1, temperature, 23.6
2025-04-15 10:00:02, sensor_1, temperature, 23.5
...
핵심 특성:
- 타임스탬프 포함 — 각 데이터가 시간을 가짐
- append-only — 거의 변경 안 됨
- 순차적 쓰기 — 보통 최신 데이터 순으로
- 시간 범위 쿼리 — "지난 1시간", "어제 vs 오늘"
- 집계 위주 —
avg,max,percentile - 대용량 — 초당 수백만 데이터 포인트
1.2 사용 사례
| 분야 | 데이터 |
|---|---|
| 모니터링 | CPU, 메모리, 응답 시간 |
| IoT | 센서 (온도, 습도, 진동) |
| 금융 | 주가, 거래량, 환율 |
| DevOps | 로그, 메트릭, 트레이스 |
| 에너지 | 전력 소비, 태양광 발전 |
| 자동차 | OBD, 텔레메트리 |
| 물류 | GPS, 차량 추적 |
1.3 RDB로는 안 되나?
가능하지만 비효율적:
CREATE TABLE metrics (
time TIMESTAMP,
device_id INT,
metric_name VARCHAR(50),
value FLOAT
);
CREATE INDEX idx_time ON metrics(time);
문제점:
- B-Tree가 시간순 삽입에 최적이 아님 — 항상 끝에 추가 = 인덱스 비효율
- 압축 없음 — 비슷한 값이 연속이지만 이를 활용 못 함
- 자동 파티셔닝 없음 — 1년치 1억 행이 단일 테이블
- 자동 다운샘플링 없음 — 오래된 데이터를 압축 못 함
→ 시계열 전용 DB가 필요합니다.
2. 5대 시계열 DB 비교
2.1 TimescaleDB
기본: PostgreSQL 확장.
CREATE EXTENSION timescaledb;
CREATE TABLE metrics (
time TIMESTAMPTZ NOT NULL,
device_id INT,
temperature DOUBLE PRECISION
);
-- 하이퍼테이블로 변환
SELECT create_hypertable('metrics', 'time', chunk_time_interval => INTERVAL '1 day');
장점:
- PostgreSQL 호환 — SQL 그대로 사용
- JOIN 지원 — 메타데이터와 시계열 결합 가능
- 자동 파티셔닝 (chunk)
- Continuous Aggregates — 자동 다운샘플링
- 압축 — 11배 압축률
단점:
- 다른 PG 부담 (동일 인스턴스)
- 카디널리티 폭증에 민감
언제: PostgreSQL 사용 중이고 시계열 추가 필요 시.
2.2 InfluxDB
전용 시계열 DB. v2와 v3가 매우 다름.
InfluxDB v2 (Flux 언어):
from(bucket: "metrics")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "cpu")
|> aggregateWindow(every: 1m, fn: mean)
InfluxDB v3 (2024년):
- Apache Arrow + DataFusion + Parquet 기반
- SQL 지원
- 컬럼 저장
- Object storage 지원
장점:
- 시계열 전용 → 최적화
- TICK 스택 (Telegraf, InfluxDB, Chronograf, Kapacitor)
단점:
- v1 → v2 → v3 마이그레이션 어려움
- 카디널리티 한계 (v1/v2)
2.3 Prometheus
모니터링 전용 TSDB. Pull 모델.
scrape_configs:
- job_name: 'app'
static_configs:
- targets: ['localhost:8080']
# 지난 5분 평균 응답 시간
rate(http_request_duration_seconds_sum[5m]) / rate(http_request_duration_seconds_count[5m])
# CPU 사용률
100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)
장점:
- CNCF 졸업 프로젝트 — Kubernetes 표준
- Pull 모델 — 서비스 디스커버리
- PromQL — 강력한 쿼리 언어
- AlertManager 통합
단점:
- 로컬 저장만 — 장기 보존 어려움 (Thanos, Cortex 필요)
- 수평 확장 X — 단일 노드
- 카디널리티 폭증에 매우 민감
언제: 모니터링 (특히 K8s)에 적합.
2.4 ClickHouse
OLAP 컬럼 DB지만 시계열에 매우 강함.
CREATE TABLE metrics (
time DateTime,
device_id UInt32,
temperature Float32
) ENGINE = MergeTree()
ORDER BY (device_id, time);
장점:
- 압도적 쿼리 성능 — 컬럼 저장 + 벡터화
- 거대한 데이터 — 페타바이트
- 실시간 INSERT — 초당 수백만 행
- SQL + 강력한 함수
단점:
- 실시간 단일 행 update 약함
- 메모리 많이 사용
- 운영 복잡
언제: 대규모 분석 + 시계열 동시 필요.
2.5 VictoriaMetrics
Prometheus 호환 + 수평 확장.
장점:
- PromQL 호환 + 추가 함수
- 수평 확장 — 클러스터 모드
- 압도적 압축 (Gorilla + 추가 최적화)
- InfluxDB, Graphite, OpenTSDB 호환
- 메모리 효율 — Prometheus 대비 7배
단점:
- 상대적으로 신생
- 일부 PromQL 동작 차이
언제: Prometheus 확장이 필요할 때.
2.6 비교표
| TimescaleDB | InfluxDB | Prometheus | ClickHouse | VictoriaMetrics | |
|---|---|---|---|---|---|
| 유형 | PG 확장 | 전용 TSDB | 모니터링 TSDB | OLAP DB | 모니터링 TSDB |
| 언어 | SQL | Flux/SQL | PromQL | SQL | PromQL+ |
| 모델 | Push | Push | Pull | Push | Push/Pull |
| 압축 | 11x | 좋음 | 보통 | 매우 좋음 | 최고 |
| 수평 확장 | 제한적 | v3 | ❌ (외부 도구) | ✅ | ✅ |
| 카디널리티 | 보통 | 약함 | 약함 | 강함 | 강함 |
| JOIN | ✅ | 제한적 | ❌ | ✅ | ❌ |
| 사용 사례 | 일반 | IoT/일반 | 모니터링 | 분석+TS | 모니터링 |
3. 카디널리티 — 시계열 DB의 가장 큰 적
3.1 카디널리티란?
시계열 = 메트릭 이름 + 레이블 조합:
http_requests_total{method="GET", path="/api/users", status="200"}
http_requests_total{method="POST", path="/api/users", status="200"}
http_requests_total{method="GET", path="/api/users", status="500"}
...
카디널리티 = 고유 시계열의 수.
3.2 폭증 시나리오
http_requests_total{
method="GET", # 5개
path="/api/users", # 100개
status="200", # 10개
user_id="12345", # 1000000개 ← 위험!
}
→ 5 × 100 × 10 × 1,000,000 = 50억 시계열.
메모리 폭증, 인덱스 폭증, 쿼리 느려짐. 카디널리티 1억 넘으면 대부분의 TSDB가 죽습니다.
3.3 절대 레이블에 넣지 말 것
❌ 사용자 ID (수백만+) ❌ 요청 ID (무한) ❌ 이메일 (이메일마다 새 시계열) ❌ 타임스탬프 (값으로 사용해야 함) ❌ 자유 텍스트 (URL 파라미터 등) ❌ 세션 ID
✅ HTTP 메서드 (5개) ✅ 상태 코드 (10개) ✅ 인스턴스 이름 (수백 개) ✅ 환경 (3개: dev/staging/prod) ✅ 리전 (10개)
3.4 카디널리티 방지 전략
1. 레이블 정규화:
path="/api/users/12345" → path="/api/users/{id}"
2. 사전 집계:
사용자별 → 사용자 그룹별로 집계
3. 별도 테이블/필드:
사용자 ID는 레이블 X, 이벤트 데이터로
4. 카디널리티 모니터링:
# Prometheus에서 가장 큰 메트릭
topk(10, count by (__name__)({__name__=~".+"}))
4. 압축 — Gorilla와 그 후예
4.1 시계열 데이터의 특성
관찰:
- 타임스탬프는 균일 간격 (예: 매 10초)
- 값은 점진적 변화 (예: 23.5 → 23.6 → 23.5)
- 같은 metric은 같은 타입
이를 활용하면 극단적 압축 가능.
4.2 Gorilla — Facebook 2015
페이스북이 발표한 시계열 압축 알고리즘.
타임스탬프 압축:
- 첫 타임스탬프는 그대로
- 다음 타임스탬프는 델타 저장 (이전과의 차이)
- 그 다음은 delta-of-delta (변화의 변화)
T1=1000, T2=1010, T3=1020, T4=1030
↓
1000, +10, +0, +0 (10은 한 번만, 나머지는 0)
값 압축:
- 첫 값 그대로 (8 bytes)
- 다음 값은 XOR 이전 값
- XOR 결과의 leading zero + 의미 있는 비트만 저장
결과: 16 bytes/포인트 → 평균 1.37 bytes/포인트. 12배 압축.
4.3 압축 효과
1000개 메트릭 × 초당 1개 × 1년 = 31,536,000,000 포인트
압축 전: 16 bytes × 31B = 504 GB
Gorilla: 1.37 bytes × 31B = 43 GB
11배 절약. 디스크 비용, 백업, 네트워크 전송 모두 절감.
4.4 후속 알고리즘
- Chimp — Gorilla 후속 (2022)
- TSXor — 더 단순한 변형
- Zstd 추가 — 블록 단위 추가 압축
5. 다운샘플링 (Downsampling)
5.1 왜 필요한가?
1초 해상도 데이터를 1년 보관 = 매우 큰 데이터.
현실:
- 최근 7일: 1초 해상도 (디버깅용)
- 7일 ~ 30일: 1분 해상도
- 30일 ~ 1년: 1시간 해상도
- 1년 +: 1일 해상도
오래된 데이터는 자세한 정보 불필요. 요약으로 충분.
5.2 TimescaleDB Continuous Aggregates
-- 매 1시간마다 평균/최대/최소 미리 계산
CREATE MATERIALIZED VIEW metrics_hourly
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 hour', time) AS hour,
device_id,
AVG(temperature) AS avg_temp,
MAX(temperature) AS max_temp,
MIN(temperature) AS min_temp,
COUNT(*) AS sample_count
FROM metrics
GROUP BY hour, device_id;
-- 자동 갱신 정책
SELECT add_continuous_aggregate_policy('metrics_hourly',
start_offset => INTERVAL '3 hours',
end_offset => INTERVAL '1 hour',
schedule_interval => INTERVAL '1 hour');
5.3 보존 정책 (Retention)
-- 30일 이상 raw 데이터 자동 삭제
SELECT add_retention_policy('metrics', INTERVAL '30 days');
-- 1년 이상 hourly 데이터 자동 삭제
SELECT add_retention_policy('metrics_hourly', INTERVAL '1 year');
5.4 Prometheus 다운샘플링
기본 Prometheus는 다운샘플링 없음. 외부 도구 필요:
- Thanos — 장기 저장 + 다운샘플링
- Cortex — 멀티테넌트
- VictoriaMetrics — 내장
6. PromQL — Prometheus 쿼리 언어
6.1 기본 쿼리
# 인스턴스의 현재 CPU 사용률
node_cpu_seconds_total
# 특정 레이블 필터
node_cpu_seconds_total{instance="server-1", mode="user"}
# 여러 레이블
node_cpu_seconds_total{instance=~"server-.*", mode!="idle"}
6.2 Range Vector
# 지난 5분간의 데이터
node_cpu_seconds_total[5m]
6.3 함수
# 지난 5분 평균 변화율
rate(http_requests_total[5m])
# 지난 5분 최대값 - 최소값
max_over_time(temperature[5m]) - min_over_time(temperature[5m])
# 백분위수
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))
6.4 집계
# 인스턴스별 합산
sum by (instance) (rate(http_requests_total[5m]))
# 상위 10개
topk(10, rate(http_requests_total[5m]))
# 평균
avg by (job) (cpu_usage_percent)
6.5 알림 쿼리
# 5분간 CPU 사용률 80% 초과
avg(cpu_usage_percent) > 80
# 1분간 5xx 비율 1% 초과
sum(rate(http_requests_total{status=~"5.."}[1m]))
/ sum(rate(http_requests_total[1m])) > 0.01
7. 실전 — Prometheus + Grafana 스택
7.1 아키텍처
[Apps] → /metrics endpoint
↓ (scrape)
[Prometheus]
↓ (long-term)
[Thanos/Cortex]
↓ (query)
[Grafana]
↓
[User/Alert]
7.2 메트릭 노출 (Node.js)
const express = require('express')
const promClient = require('prom-client')
const register = new promClient.Registry()
promClient.collectDefaultMetrics({ register })
const httpRequestDuration = new promClient.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status'],
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
})
register.registerMetric(httpRequestDuration)
const app = express()
app.use((req, res, next) => {
const end = httpRequestDuration.startTimer()
res.on('finish', () => {
end({ method: req.method, route: req.route?.path || 'unknown', status: res.statusCode })
})
next()
})
app.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType)
res.end(await register.metrics())
})
app.listen(8080)
7.3 Prometheus 설정
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'my-app'
static_configs:
- targets: ['app:8080']
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
7.4 Grafana 대시보드
핵심 메트릭:
- RED Method: Rate, Errors, Duration
- USE Method: Utilization, Saturation, Errors
# Rate
sum(rate(http_requests_total[5m])) by (route)
# Errors
sum(rate(http_requests_total{status=~"5.."}[5m])) by (route)
# Duration (p99)
histogram_quantile(0.99,
sum(rate(http_request_duration_seconds_bucket[5m])) by (le, route)
)
7.5 알림
groups:
- name: app
rules:
- alert: HighErrorRate
expr: sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) > 0.01
for: 5m
annotations:
summary: "High error rate on {{ $labels.instance }}"
8. IoT 시나리오 — 100만 디바이스
8.1 요구사항
- 100만 IoT 디바이스
- 디바이스당 초당 1개 데이터 포인트
- 1년 보관
- 실시간 대시보드
- 알림
8.2 데이터량 계산
1,000,000 디바이스 × 1 point/s × 86,400 s/day = 86.4B/day
86.4B/day × 365 days = 31.5 trillion points/year
압축 (1.37 bytes): 43 TB
8.3 아키텍처
[100만 디바이스]
↓ MQTT
[Mosquitto / EMQX]
↓
[Telegraf / Vector]
↓
[TimescaleDB Cluster] or [VictoriaMetrics Cluster]
↓
[Grafana]
8.4 데이터 모델
CREATE TABLE sensors (
time TIMESTAMPTZ NOT NULL,
device_id BIGINT,
metric_type SMALLINT, -- enum (1=temp, 2=humidity, ...)
value DOUBLE PRECISION
);
SELECT create_hypertable('sensors', 'time', chunk_time_interval => INTERVAL '1 day');
-- 인덱스
CREATE INDEX ON sensors(device_id, time DESC);
-- 압축
ALTER TABLE sensors SET (
timescaledb.compress,
timescaledb.compress_segmentby = 'device_id'
);
SELECT add_compression_policy('sensors', INTERVAL '7 days');
8.5 카디널리티 계산
device_id: 1,000,000 (높음 — 각 디바이스가 별도 시계열이면 죽음)metric_type: 10
해결: device_id를 컬럼으로, 레이블/태그로 X.
TimescaleDB는 SQL 컬럼이라 OK. Prometheus였다면 죽음.
9. 금융 시나리오 — 주가 데이터
9.1 특성
- 마이크로초 정확도
- 미세한 가격 변화 추적
- 주문장(order book) 깊이
- 빠른 시간 범위 쿼리
- 백테스팅 (과거 데이터 분석)
9.2 ClickHouse 적합
CREATE TABLE trades (
symbol String,
trade_time DateTime64(6, 'UTC'),
price Float64,
volume UInt32,
side Enum8('buy' = 1, 'sell' = 2)
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(trade_time)
ORDER BY (symbol, trade_time);
-- 1분봉 집계
SELECT
symbol,
toStartOfMinute(trade_time) AS minute,
argMin(price, trade_time) AS open,
max(price) AS high,
min(price) AS low,
argMax(price, trade_time) AS close,
sum(volume) AS volume
FROM trades
WHERE symbol = 'AAPL' AND trade_time >= now() - INTERVAL 1 DAY
GROUP BY symbol, minute
ORDER BY minute;
ClickHouse는 조ㅡㄴ단 행도 초 단위에 처리. 백테스팅에 이상적.
10. 선택 가이드
10.1 의사결정 트리
PostgreSQL을 이미 사용 중인가?
├─ YES → TimescaleDB (시작점)
└─ NO
├─ 모니터링/메트릭 전용?
│ ├─ 작은 규모 → Prometheus
│ └─ 큰 규모 → VictoriaMetrics
├─ IoT/일반?
│ └─ InfluxDB v3 또는 TimescaleDB
└─ 분석 + 시계열?
└─ ClickHouse
10.2 일반적 권장
- 시작: TimescaleDB (PG 익숙) or Prometheus (모니터링)
- 확장: VictoriaMetrics or ClickHouse
- IoT: InfluxDB v3 or TimescaleDB
- 금융/분석: ClickHouse
퀴즈
1. 시계열 DB가 일반 RDB보다 빠른 이유는?
답: (1) 시간 기반 자동 파티셔닝 — chunk 단위로 분할 → 쿼리 시 필요한 chunk만 스캔, (2) 컬럼 저장 — 메트릭 값들이 연속 저장 → 압축 효율, 캐시 친화적, (3) 시계열 특화 압축 (Gorilla) — 12배 압축, (4) 시간 인덱스 최적화 — append-only 패턴에 최적, (5) 다운샘플링 — 오래된 데이터 자동 요약. RDB는 일반 목적 → 시계열에 비효율.
2. 카디널리티 폭증이란 무엇이고 어떻게 방지하나요?
답: 메트릭의 고유 레이블 조합 수가 너무 많아져 메모리/인덱스가 폭발하는 현상. 예: user_id를 레이블로 하면 사용자 수만큼 시계열 생성 → 100만+. 방지: (1) 사용자 ID, 요청 ID, 이메일 같은 고유 식별자를 레이블에 넣지 말 것, (2) URL 경로를 정규화 (/api/users/12345 → /api/users/{id}), (3) 카디널리티 모니터링, (4) 사전 집계, (5) 별도 이벤트 저장소 사용.
3. Gorilla 압축이 어떻게 12배 압축을 달성하나요?
답: 시계열 데이터의 두 가지 패턴을 활용합니다. (1) 타임스탬프: 균일 간격 → delta-of-delta 인코딩 (변화의 변화). 1초 간격이면 대부분 0. (2) 값: 비슷한 값이 연속 → XOR 이전 값 → leading zero + 의미 있는 비트만 저장. CPU 사용률 23% → 24%면 거의 같은 비트, 차이만 작게 저장. 결과: 16 bytes/포인트 → 평균 1.37 bytes/포인트.
4. Prometheus의 Pull 모델 vs InfluxDB의 Push 모델?
답: Pull (Prometheus): Prometheus가 주기적으로 /metrics 엔드포인트를 긁어옴. 장점: 서비스 디스커버리, 동적 환경 적합, "건강한가" 자동 확인. 단점: 방화벽/NAT 어려움, 짧은 작업(cron) 캡처 어려움 (Pushgateway 필요). Push (InfluxDB): 클라이언트가 데이터를 보냄. 장점: 방화벽 통과, 짧은 작업 OK. 단점: 클라이언트 관리 복잡. 일반적으로 K8s 모니터링은 Pull, IoT는 Push가 적합.
5. TimescaleDB와 Prometheus의 차이는?
답: TimescaleDB는 PostgreSQL 확장 → SQL 사용, JOIN 가능, 일반 데이터와 결합 가능. 카디널리티에 보다 강함. 장기 저장 쉬움. Prometheus는 모니터링 전용 → PromQL (강력하지만 학습 필요), 단일 노드, K8s 통합 표준, 장기 저장 어려움 (외부 도구 필요). 선택: 일반 시계열 + SQL 사용자는 TimescaleDB, 모니터링 + K8s는 Prometheus. 두 가지를 함께 쓰는 경우도 많음.
참고 자료
- TimescaleDB — 공식 문서
- Prometheus — 공식
- InfluxDB v3
- ClickHouse
- VictoriaMetrics
- Gorilla 논문 — Facebook 2015
- Chimp 논문 — Gorilla 후속
- Time Series Database Lectures — CMU 강의
- Awesome Time Series
- Telegraf — 메트릭 수집
- Grafana — 시각화
현재 단락 (1/404)
- **시계열 데이터의 특성**: 쓰기 압도적, 시간 순서, 거의 변경 없음, 시간 범위 쿼리, 집계가 핵심