Metric cardinality cao: một label có thể tạo hàng nghìn time series Trung cấp
Thời gian đọc ước tính: 6 phút
Triệu chứng
Phần tiêu đề “Triệu chứng”Sau một lần deploy, bộ nhớ Prometheus tăng, scrape chậm, dashboard query lâu hơn, trong khi traffic không đổi. Trong diff có một thay đổi tưởng vô hại: thêm label user_id vào counter http_requests_total để “dễ lọc theo khách hàng”.
Cardinality là số tổ hợp giá trị label khác nhau của một metric; mỗi tổ hợp là một time series riêng. Label có giá trị không giới hạn (user ID, email, request ID, URL chứa ID) biến metric thành kho tra cứu sự kiện, việc mà metric không được thiết kế để làm.
Tái hiện: các chiều label nhân với nhau
Phần tiêu đề “Tái hiện: các chiều label nhân với nhau”Ba route template, hai nhóm status, 100 user giả: 3 × 2 = 6 series; thêm user_id thành 3 × 2 × 100 = 600.
Đang tải diagram…
Con số là trần tổ hợp xuất hiện, không phải số byte RAM. Bảng dưới cho thấy chỗ nào bắt đầu đáng lo:
| Số series | Ví dụ | Mức rủi ro |
|---|---|---|
| 6 | route × status | Không đáng kể |
| 600 | thêm 100 user | Vẫn nhỏ, nhưng là mô hình sai; user thật là hàng triệu |
| 400.000 | 50 route × 5 method × 10 status × 8 region × 20 instance | Phải có series budget và theo dõi |
| 10 triệu | thêm label không giới hạn | Sự cố; không dựa vào quy tắc “bao nhiêu KB mỗi series” vì phụ thuộc phiên bản và workload |
Histogram còn nhân thêm số bucket (cộng _count, _sum); label không giới hạn trên histogram càng đắt. instance do Prometheus gắn lúc scrape, cũng là một hệ số nhân theo số replica.
Chạy mô phỏng tổ hợp (không cần Prometheus):
npx --prefix examples ts-node examples/src/metric-cardinality/cardinality-demo.tsĐổi 100 thành 10_000 để thấy số tổ hợp tăng. Chỉ tổ hợp thực sự được ghi mới tạo series; giá trị động liên tục thì tạo churn (series cũ ngừng, series mới xuất hiện), nên chi phí còn phụ thuộc tốc độ thay đổi.
Cách phát hiện
Phần tiêu đề “Cách phát hiện”Câu hỏi và công cụ tương ứng:
| Muốn biết | Dùng |
|---|---|
| Tổng active series và xu hướng | prometheus_tsdb_head_series |
| Tốc độ tạo series mới (churn) | rate(prometheus_tsdb_head_series_created_total[5m]) |
| Metric nào nhiều series nhất | topk(10, count by (__name__) ({__name__=~".+"})) (query nặng, chạy có kiểm soát) |
| Label nào nhiều giá trị nhất | GET /api/v1/status/tsdb, đọc seriesCountByMetricName, labelValueCountByLabelName, memoryInBytesByLabelName, seriesCountByLabelValuePair |
| Phân tích sâu một block | promtool tsdb analyze <db path> |
Endpoint /api/v1/status/tsdb nhận tham số limit (mặc định 10, tối đa 10000) và là endpoint vận hành, không nên public. So sánh trước và sau deploy để tìm metric hoặc label mới; tổng memory một mình chưa đủ kết luận vì retention, ingest và query load đều ảnh hưởng.
Trong code, review mọi chỗ gọi .labels(...): tập giá trị tối đa là bao nhiêu, client có điều khiển được giá trị không? Kiểm tra cả middleware tự xuất metric, nó hay dùng URL thô nếu chưa cấu hình route normalization.
Khắc phục ngay
Phần tiêu đề “Khắc phục ngay”Guardrail: cho scrape fail thay vì để Prometheus OOM
Phần tiêu đề “Guardrail: cho scrape fail thay vì để Prometheus OOM”Mặc định các giới hạn này là 0 (không giới hạn). Khi vượt sample_limit, label_limit, label_name_length_limit hoặc label_value_length_limit, cả lần scrape bị coi là thất bại, nên bạn thấy up == 0 thay vì mất bộ nhớ.
scrape_configs: - job_name: orders-api sample_limit: 20000 # số sample tối đa mỗi lần scrape (sau metric relabeling) label_limit: 30 # số label tối đa trên một sample label_value_length_limit: 200 # byte target_limit: 50 # số target tối đa của job (tính năng experimental)Chọn số theo quan sát thực tế của từng job, cộng margin. Giới hạn chỉ chặn ở ranh giới scrape; label từ input vẫn phải được chuẩn hóa trong code.
Phòng ngừa lâu dài
Phần tiêu đề “Phòng ngừa lâu dài”Series budget: ước tính tích số label × số replica cho từng metric family trước khi merge, và alert trên prometheus_tsdb_head_series cùng churn.
Review label bằng bảng:
| Label | Tối đa bao nhiêu giá trị | Bị chặn trên chưa | Owner |
|---|---|---|---|
route |
số route template | Có (dùng template, route lạ thành unmatched) |
Team API |
status_class |
5 | Có | Team API |
tier |
3 (free, paid, enterprise) |
Có | Team Billing |
user_id |
hàng triệu | Không, cấm |
Thay thế khi cần tra cá thể:
- Exemplar (metric trỏ tới
trace_id) và log hoặc trace có retention và phân quyền, xem logs, metrics, traces. - Chuẩn hóa label từ input thành tập hữu hạn; test rằng input lạ rơi vào nhóm fallback.
- Native histogram giảm số series do bucket so với classic histogram.
- Cần phân tích theo dimension cao: dùng event store hoặc hệ thống analytics, không dùng metric.
Checklist
Phần tiêu đề “Checklist”- Mỗi label mới có số giá trị tối đa, đã bị chặn trên và có owner (bảng review label ở trên).
- Không dùng
user_id,request_id, email hay raw path chứa ID làm label; dùng route template, route lạ thànhunmatched. - Ước tính series budget (
tích số label × số replica, nhân thêm bucket với histogram) trước khi merge. - Đặt
sample_limitvàlabel_limitcho từng job theo quan sát thực tế cộng margin. - Alert trên
prometheus_tsdb_head_seriesvà churnrate(prometheus_tsdb_head_series_created_total[5m]). - Sau deploy, so
/api/v1/status/tsdbtrước và sau để tìm metric hoặc label mới. - Khi Prometheus quá tải: chặn nguồn (rollback hoặc bỏ label) trước,
metric_relabel_configsđể giảm áp lực, không xóa TSDB bằng tay.
Tóm tắt
Phần tiêu đề “Tóm tắt”Tự kiểm tra: (1) Vì sao thêm 100 user ID biến 6 tổ hợp thành tối đa 600? (2) Khi vượt sample_limit, điều gì xảy ra với lần scrape? (3) Cần tra cứu một request cụ thể thì dùng metric, log hay trace?