Time-series database
Time-series database lưu dữ liệu đo theo thời gian như metric server, giá cổ phiếu, vị trí GPS. Bài này giải thích đặc điểm của loại dữ liệu này, cách nén, downsampling và retention, so sánh TimescaleDB, InfluxDB, Prometheus, ClickHouse, và bẫy cardinality.
Time-series database là gì và mô hình cốt lõi
Dữ liệu time-series là chuỗi các giá trị đo gắn với thời điểm: CPU của server mỗi 15 giây, nhiệt độ container lạnh mỗi phút, số request/s của một API, giá BTC mỗi giây. Grokking ch.13 nhắc tới time-series database ở bước lưu metric của hệ thống giám sát: các mẫu có timestamp được lưu theo thứ tự thời gian để vẽ được cả dòng lịch sử của một metric, và lịch sử là điều kiện để hệ thống giám sát có ích.1 Ch.14 cũng chọn time-series database làm nơi lưu metric trong thiết kế hệ thống giám sát.2
Loại dữ liệu này có đặc điểm rất khác dữ liệu nghiệp vụ:
- Gần như chỉ ghi thêm. Hiếm khi sửa một điểm đo đã ghi. Dữ liệu tới theo thứ tự thời gian, chỉ lệch vài giây.
- Ghi rất nhiều, đều đặn. 10.000 server, mỗi server 500 metric, 15 giây một lần (giả định) là 333.000 điểm/s, suốt ngày đêm.
- Đọc theo khoảng thời gian và gộp. "p95 latency của service checkout trong 6 giờ qua, mỗi phút một điểm." Hiếm khi đọc một điểm lẻ.
- Dữ liệu mới nóng, dữ liệu cũ lạnh. Dashboard và alert đọc 1 giờ gần nhất. Dữ liệu 6 tháng trước chỉ dùng cho báo cáo xu hướng.
- Xóa theo khối thời gian. Hết hạn lưu thì bỏ nguyên một ngày, không xóa từng dòng.
Bộ đặc điểm này là mô tả chung của khoá học.3
Mô hình dữ liệu
Hầu hết TSDB dùng cùng một mô hình:
http_requests_total{service="checkout", method="POST", status="500"} @1730534400 1027
└─ metric name ──┘ └──────────── labels (tags) ─────────────────┘ timestamp value
Mỗi tổ hợp duy nhất của tên metric và bộ label tạo thành một series. Series là đơn vị lưu trữ: các điểm của một series nằm liền nhau trên đĩa, được nén cùng nhau. Số series khác nhau gọi là cardinality, con số quan trọng nhất khi định cỡ một TSDB.3
Kiến trúc bên trong
Các TSDB khác nhau về chi tiết nhưng phần lớn có chung khung (mô tả khái niệm, không gắn một sản phẩm cụ thể):3 WAL là kỹ thuật ghi nhật ký trước khi sửa dữ liệu để chống mất khi crash, như Seven Databases mô tả ở HBase.4
- Ghi vào bộ đệm trong RAM + write-ahead log. Điểm mới được thêm vào chunk đang mở của series tương ứng. WAL đảm bảo không mất dữ liệu khi crash.
- Đóng khối theo thời gian. Sau một khoảng thời gian (mỗi hệ gọi là block, chunk hay partition), khối được nén và ghi xuống đĩa thành file bất biến.
- Index đảo ngược cho label. Từ
service="checkout"tìm ra danh sách series id. Truy vấn lọc theo label là phép giao các danh sách này. - Compaction. Gộp các khối nhỏ thành khối lớn để giảm số file và tăng tỷ lệ nén.
- Retention. Xóa nguyên khối cũ hơn ngưỡng. Rẻ vì chỉ là xóa file hoặc drop partition.
Nén
Nén là lý do TSDB rẻ hơn nhiều so với lưu metric vào bảng Postgres thường. Kỹ thuật phổ biến xuất phát từ bài báo Gorilla của Facebook:5
- Timestamp: delta-of-delta. Điểm đo cách nhau đều 15 giây, nên hiệu giữa hai khoảng cách liên tiếp gần như luôn bằng 0, mã hóa bằng 1 bit.
- Giá trị: XOR với giá trị trước. Giá trị CPU 42,1 rồi 42,3 có rất nhiều bit giống nhau. XOR cho ra phần lớn là bit 0, chỉ cần lưu phần khác.
Kết quả: mỗi điểm (timestamp 8 B + float 8 B = 16 B) nén xuống chỉ còn vài byte (ước lượng; ví dụ tính toán bên dưới giả định 1,5 B). Các database cột nén theo cột (delta, dictionary), tỷ lệ nén lớn là bình thường.5
Downsampling và retention
Không ai cần độ phân giải 15 giây cho dữ liệu năm ngoái. Redis in Action làm đúng ý này ở quy mô nhỏ: counter theo thời gian được giữ ở nhiều độ chính xác (1 giây, 5 giây, 1 phút...), mỗi độ chính xác chỉ giữ một số mẫu gần nhất, và một tiến trình nền dọn mẫu cũ.6 Grokking ch.14 cũng nói khi bộ nhớ cục bộ có hạn thì phải có chính sách quyết định giữ hay xóa dữ liệu nào.7 Một chính sách ví dụ (giả định của khoá học):
| Độ phân giải | Giữ trong | Dùng cho |
|---|---|---|
| Gốc 15 s | 15 đến 30 ngày | Debug sự cố, alert |
| 1 phút (min, max, avg, count) | 90 ngày | Dashboard theo tuần, tháng |
| 1 giờ | 1 đến 2 năm | Báo cáo dung lượng, xu hướng |
Downsampling phải lưu nhiều phép gộp. Chỉ lưu avg sẽ làm mất các đỉnh ngắn, mà đỉnh chính là thứ bạn cần thấy. Lưu thêm max và count; Redis in Action lưu min, max, count, sum và tổng bình phương để tính trung bình và độ lệch chuẩn vì "nếu không ghi thì sau này chắc chắn sẽ cần".8 Với percentile, không thể lấy trung bình của các p95; cần lưu histogram hoặc sketch để gộp đúng.3
Data model và API của các lựa chọn
Prometheus
Prometheus kéo (pull) metric từ endpoint /metrics của từng service theo chu kỳ.9 Grokking ch.13 phân tích pull và push: với pull, server chỉ cần giữ metric trong bộ nhớ và mở một endpoint, hệ thống giám sát tự quyết lịch lấy nên server không làm quá tải nó; push hợp khi firewall chặn hệ thống giám sát truy cập server.10 Ch.14 còn chọn mô hình lai: pull trong một data center rồi push lên tầng trên.11
import client from "prom-client";
const latency = new client.Histogram({
name: "http_request_duration_seconds",
help: "Request latency",
labelNames: ["route", "method", "status"],
buckets: [0.05, 0.1, 0.25, 0.5, 1, 2.5],
});
app.use((req, res, next) => {
const end = latency.startTimer({ route: req.route?.path ?? "unknown", method: req.method });
res.on("finish", () => end({ status: String(res.statusCode) }));
next();
});
Truy vấn bằng PromQL:
histogram_quantile(0.95,
sum by (le, route) (rate(http_request_duration_seconds_bucket[5m])))
TimescaleDB
TimescaleDB là extension của PostgreSQL. Bảng thường được chuyển thành hypertable, tự chia thành chunk theo thời gian. Bạn giữ nguyên SQL, join được với bảng nghiệp vụ.12
CREATE TABLE container_temp (
time timestamptz NOT NULL,
container_id text NOT NULL,
celsius double precision
);
SELECT create_hypertable('container_temp', 'time', chunk_time_interval => interval '1 day');
-- Bảng gộp 1 phút, tự cập nhật
CREATE MATERIALIZED VIEW temp_1m WITH (timescaledb.continuous) AS
SELECT time_bucket('1 minute', time) AS bucket, container_id,
avg(celsius) AS avg_c, max(celsius) AS max_c
FROM container_temp GROUP BY bucket, container_id;
SELECT add_retention_policy('container_temp', interval '30 days');
SELECT add_compression_policy('container_temp', interval '7 days');
InfluxDB và ClickHouse
InfluxDB nhận dữ liệu bằng push qua line protocol: cpu,host=web-1,region=sg usage=42.1 1730534400000000000. Các phiên bản mới thay đổi nhiều về kiến trúc lưu trữ nên cần kiểm tra tài liệu hiện hành.12
ClickHouse là database cột tổng quát, không chuyên cho time-series nhưng làm rất tốt: bảng MergeTree sắp theo (service, timestamp), nén mạnh, quét nhanh. Hợp với event và log có nhiều chiều, nơi mô hình series của Prometheus bị vỡ vì cardinality.
Con số và sizing
Ví dụ: hệ thống giám sát 10.000 server, mỗi server 500 series, chu kỳ 15 giây. Các con số là ước tính và giả định của khoá học, cần đo lại.13
| Đại lượng | Tính | Kết quả |
|---|---|---|
| Số series active | 10.000 × 500 | 5 triệu |
| Mẫu mỗi giây | 5M / 15 | ~333.000 |
| Byte mỗi giây (1,5 B/mẫu) | 333.000 × 1,5 | ~0,5 MB/s |
| Mỗi ngày | 0,5 MB × 86.400 | ~43 GB |
| Giữ 15 ngày dữ liệu gốc | 43 × 15 | ~650 GB |
| RAM cho head block (~4 KB/series) | 5M × 4 KB | ~20 GB |
Nhận xét: đĩa không đáng lo, RAM theo số series mới là giới hạn (ước lượng 4 KB mỗi series là giả định, cần đo). Một instance đơn lẻ có ngưỡng series riêng; vượt ngưỡng đó thì chia theo nhóm service (functional sharding) hoặc dùng hệ phân tán. Grokking ch.14 cũng nói cách mở rộng bằng cách thêm tầng giám sát theo phân cấp.11
Một ví dụ khác: theo dõi vị trí 50.000 container lạnh, mỗi container gửi nhiệt độ và GPS mỗi phút. 50.000 / 60 ≈ 830 điểm/s. Một node TimescaleDB vừa phải có thể đủ vì tải chỉ cỡ trăm điểm mỗi giây (ước lượng, cần load test).
Bẫy cardinality
Cardinality nổ khi đưa giá trị có miền lớn vào label:
// Tồi: mỗi user tạo series riêng
http_requests_total{user_id="8812731", route="/checkout"}
// Tồi: path chứa id
http_requests_total{path="/orders/99812/items"}
// Tốt: route template, số mã lỗi có giới hạn
http_requests_total{route="/orders/:id/items", status="500"}
1 triệu user × 20 route × 5 status = 100 triệu series, vượt xa khả năng của một instance thông thường.14 Quy tắc: label chỉ chứa giá trị có tập hữu hạn và nhỏ (service, region, status, method). Dữ liệu theo user hay theo request thuộc về log, trace, hoặc một database cột như ClickHouse.
Sai lầm hay gặp: Gắn pod_name hoặc container_id làm label rồi deploy 20 lần mỗi ngày. Mỗi lần deploy tạo bộ series mới, series cũ vẫn chiếm index tới khi hết retention. Cardinality tăng âm thầm cho tới khi hết RAM.14
Khi nào dùng và khi nào không
| Lựa chọn | Mạnh ở | Yếu ở | Hợp với |
|---|---|---|---|
| Prometheus (+ Thanos/Mimir) | Hệ sinh thái giám sát, PromQL, alert | Cardinality thấp, không hợp dữ liệu nghiệp vụ, lưu dài hạn cần thêm thành phần | Metric hạ tầng và ứng dụng |
| TimescaleDB | SQL đầy đủ, join, transaction, continuous aggregate | Scale ngang hạn chế hơn hệ phân tán thuần | IoT, dữ liệu nghiệp vụ theo thời gian, team đã quen Postgres |
| InfluxDB | Ingest push đơn giản, nhiều client IoT | Thay đổi lớn giữa các phiên bản, ngôn ngữ truy vấn đổi qua lại | IoT, sensor |
| ClickHouse | Quét và gộp cực nhanh, cardinality cao | Không hợp cập nhật từng dòng, vận hành cluster khó | Analytics, event, log, ad click |
| Cassandra / DynamoDB | Ghi rất nhiều, scale ngang | Không có hàm gộp theo thời gian, tự xây downsampling | Lịch sử vị trí, timeline theo key |
| Postgres thường | Đơn giản | Bảng phình, xóa dữ liệu cũ tốn vacuum | Lượng điểm mỗi ngày còn nhỏ (ước lượng) |
Không cần TSDB khi lượng dữ liệu nhỏ (vài nghìn điểm mỗi giờ, ước lượng) hoặc khi truy vấn chủ yếu theo id thay vì theo khoảng thời gian. Bảng so sánh trên là nhận định tổng hợp của khoá học.15
Vận hành ở quy mô nhỏ và lớn
Nhỏ. Dùng dịch vụ managed: Grafana Cloud, Amazon Managed Prometheus, Timescale Cloud, hoặc một Prometheus + Grafana tự host trên một VM vừa phải. Giữ vài tuần dữ liệu gốc. Chi phí tính theo ước lượng, cần kiểm tra giá hiện hành, phần lớn là theo số series.16
Vừa. Nhiều Prometheus, mỗi cái cho một nhóm service hoặc một cluster Kubernetes, giống cách Grokking ch.14 dùng nhiều secondary monitoring server cho các nhóm server rồi gom lên tầng trên.11 Dữ liệu dài hạn đẩy lên object storage (Grokking ch.14 dùng blob storage cho dữ liệu dư).11 Recording rule tính trước các truy vấn dashboard nặng. Đặt giới hạn số series cho mỗi target để một service lỗi không kéo sập cả hệ thống.
Lớn. Hệ phân tán (Mimir, VictoriaMetrics cluster, M3) với ingest qua Kafka hoặc remote write, shard theo hash của series, nhân bản cho ingest. Dữ liệu cũ nằm trên object storage, truy vấn qua lớp query có cache. Có đội riêng quản lý ngân sách cardinality cho từng team.16
Điểm nói trong phỏng vấn
- Nêu đặc điểm dữ liệu trước khi chọn công cụ: ghi thêm theo thời gian, đọc theo khoảng, gộp, xóa theo khối.
- Tính số series, không chỉ số điểm. Series quyết định RAM và index. Điểm mỗi giây quyết định đĩa và ingest.
- Nói về nén. Delta-of-delta và XOR đưa mỗi mẫu xuống vài byte (ước lượng), nên lưu vài tuần của 5 triệu series chỉ cỡ vài trăm GB.5
- Đề xuất downsampling nhiều tầng và giải thích vì sao cần lưu cả max và count, không chỉ avg.8
- Cảnh báo cardinality và nói rõ dữ liệu theo user thuộc về log hoặc database cột.
- Phân biệt pull và push theo Grokking ch.13, và nêu khi nào chọn cái nào.10
Trong phỏng vấn: Bài Metrics monitoring xoay quanh TSDB: ingest, nén, downsampling, alert. Ad click aggregator thường dùng ClickHouse hoặc Druid sau một tầng stream như Flink. Stock trading lưu dữ liệu nến (OHLC) theo phút. Container tracking và Strava lưu chuỗi vị trí và cảm biến theo thời gian.
Bài liên quan: Cấu trúc dữ liệu cho big data cho các sketch dùng để tính percentile và đếm unique, Cassandra cho mô hình ghi nhiều theo partition key.
Nguồn trích dẫn
-
Grokking Modern System Design · ch.13 Distributed Monitoring · mục "Persist the data" (Prerequisites of a Monitoring System). ↩
-
Grokking Modern System Design · ch.14 Monitor Server-side Errors (Design of a Monitoring System) · mục "Storage". ↩
-
Chưa có nguồn trong thư viện: thư viện không có sách về time-series database; phần này chỉ nêu khái niệm cơ bản và đặc điểm chung của dữ liệu theo thời gian. ↩ ↩2 ↩3 ↩4
-
Seven Databases in Seven Weeks (file .zip, tải về giải nén) · ch.4 HBase · Day 2 · đoạn về write-ahead log (WAL), tr. 112–113. ↩
-
Chưa có nguồn trong thư viện: thư viện không có sách về nén time-series (Gorilla, delta-of-delta, XOR); chỉ nêu ý tưởng, không đưa tỷ lệ nén của sản phẩm. ↩ ↩2 ↩3
-
Redis in Action · ch.5 · §5.2.1 Storing counters in Redis (counter theo nhiều độ chính xác, giữ 120 mẫu gần nhất, dọn mẫu cũ; tr. 94–98). ↩
-
Grokking Modern System Design · ch.14 · mục "Improving our design" và câu hỏi "What happens if a local or global monitoring system is down?" (giới hạn lưu cục bộ, cần chính sách giữ hay xóa dữ liệu). ↩
-
Redis in Action · ch.5 · §5.2.2 Storing statistics in Redis (min, max, count, sum, sumsq; tr. 98–99). ↩ ↩2
-
Chưa có nguồn trong thư viện: thư viện không có sách về Prometheus; chỉ nêu mô hình pull ở mức khái niệm (khớp với Grokking ch.13). ↩
-
Grokking Modern System Design · ch.13 Distributed Monitoring · mục "Populate the metrics" (pull và push). ↩ ↩2
-
Grokking Modern System Design · ch.14 · mục "Improving our design" (mô hình lai pull và push, secondary monitoring server, blob storage, mở rộng bằng phân cấp). ↩ ↩2 ↩3 ↩4
-
Chưa có nguồn trong thư viện: thư viện không có sách về TimescaleDB, InfluxDB hay ClickHouse; chỉ nêu khái niệm cơ bản. ↩ ↩2
-
Chưa có nguồn trong thư viện: các con số sizing (byte mỗi mẫu, RAM mỗi series) là giả định của khoá học, không phải số liệu từ sách. ↩
-
Chưa có nguồn trong thư viện: thư viện không có sách về cardinality của time-series; phần này là kinh nghiệm và phép tính của khoá học. ↩ ↩2
-
Chưa có nguồn trong thư viện: so sánh các lựa chọn TSDB là nhận định tổng hợp của khoá học. ↩
-
Chưa có nguồn trong thư viện: gợi ý vận hành và chi phí theo quy mô là nhận định của khoá học. ↩ ↩2