High Cardinality & Label Explosion: Cơ Chế Vỡ Ở Tầng Nào
Định nghĩa lại "cardinality" cho đúng với mô hình Monarch
Trong Prometheus tự host, "high cardinality" thường được hiểu đơn giản: quá nhiều time series làm Prometheus Pod tốn RAM, cuối cùng OOM. Trên GMP, hậu quả vật lý y hệt vẫn tồn tại (nhiều time series hơn = nhiều bộ nhớ hơn ở tầng leaf), nhưng vì bộ nhớ đó không phải của bạn mà là tài nguyên dùng chung trên Monarch, cardinality explosion biểu hiện thành ít nhất bốn triệu chứng khác nhau tùy vào nó vỡ ở tầng nào trong pipeline — và biết phân biệt bốn tầng này là kỹ năng chẩn đoán quan trọng nhất của chương này.
Bốn tầng, theo đúng thứ tự dữ liệu đi qua:
Exporter (/metrics)
↓ (1) Metric descriptor được tạo/cập nhật
Cloud Monitoring API — metricDescriptors
↓ (2) Time series được tạo cho mỗi tổ hợp label unique
Monarch leaf shard — lưu theo key range
↓ (3) Một "monitored resource" gom nhiều time series
Monarch Quarantiner — cơ chế bảo vệ per-tenant
↓ (4) Billing — active time series được tính phíTầng 1 — Metric descriptor quota: 25.000 mỗi project
Mỗi metric type độc nhất (tên metric + tập hợp label key, không tính label value) tạo ra một metric descriptor. Theo Cloud Monitoring quotas and limits, giới hạn cứng là 25.000 metric descriptor cho Prometheus/workload/external metric mỗi project. Đây là giới hạn dễ bị vi phạm nhất bởi một lỗi thiết kế cụ thể: nhúng dữ liệu định danh (dimensional data) vào tên metric thay vì vào label.
Tài liệu troubleshooting chính thức liệt kê các ví dụ thực tế về metric name bị lỗi kiểu này:
request_path_____path_to_a_resource____istio_request_duration_milliseconds
envoy_cluster_grpc_method_name_failure
vault_rollback_attempt_path_name_1700683024Mỗi request path khác nhau, mỗi method khác nhau, thậm chí mỗi timestamp khác nhau (!) trở thành một metric descriptor riêng biệt — không phải một label value mới trên cùng một descriptor. Đây là khác biệt bản chất so với label explosion (tầng 2): label explosion tạo nhiều time series trên cùng một descriptor; lỗi này tạo ra hàng nghìn descriptor riêng biệt, đốt quota descriptor nhanh hơn nhiều so với đốt quota time series.
Cách chẩn đoán: dùng metricDescriptors.list API hoặc trang Metrics Management, kiểm tra nếu số lượng metric inactive (không còn nhận dữ liệu mới nhưng vẫn tính vào quota) vượt xa số lượng đang active — đây là dấu hiệu rõ ràng của một exporter đang sinh descriptor theo pattern động. Cách sửa tận gốc là cấu hình lại exporter để dùng label thay vì nhúng giá trị vào tên (ví dụ StatsD exporter, Vault exporter, Istio Envoy đều có tùy chọn cấu hình tường minh cho việc này), sau đó xóa các descriptor rác bằng script chính thức delete_metric_descriptors — luôn chạy dry-run trước, vì thao tác xóa descriptor không thể hoàn tác và mất toàn bộ lịch sử dữ liệu của metric đó.
Tầng 2 — Time series explosion: một label unique = một time series
Đây là dạng cardinality quen thuộc nhất: metric http_requests_total{path="/api/v1/users/<user_id>"} với một triệu user_id khác nhau tạo ra một triệu time series trên cùng một descriptor. Giới hạn liên quan ở tầng này là 1.000.000 active time series cho mỗi monitored resource đối với Prometheus (Cloud Monitoring quotas and limits) — cao hơn hẳn giới hạn 200.000 của custom/workload/external metric, phản ánh thực tế rằng Prometheus ecosystem vốn tạo cardinality cao hơn theo bản chất thiết kế của nó.
Nhưng con số một triệu này dễ gây ảo tưởng an toàn giả. Vấn đề thực sự không phải là "một metric có bao nhiêu time series" mà là tốc độ thay đổi tập hợp time series đó. Có một giới hạn riêng, ít được chú ý hơn nhưng nguy hiểm hơn trong thực tế: tốc độ tạo metric/label definition mới — 6.000 lần mỗi phút mỗi project. Một exporter sinh label động dựa trên giá trị runtime thay đổi liên tục (ví dụ timestamp, request ID, session token) không chỉ tạo nhiều time series mà còn tạo chúng nhanh hơn giới hạn tạo mới cho phép, dẫn đến lỗi rõ ràng: "You have hit per-project limit on metric definition or label definition changes per minute."
Theo tài liệu troubleshooting, đây không phải là giới hạn ingest dữ liệu (dữ liệu của time series đã tồn tại vẫn ghi bình thường) — nó chỉ chặn việc tạo thêm time series/label mới trong cùng phút đó, và tự phục hồi khi tốc độ tạo giảm xuống dưới ngưỡng. Điều này giải thích một triệu chứng khó hiểu: một metric "bị mất dữ liệu không đều đặn" thay vì mất hoàn toàn — vì chỉ những tổ hợp label hoàn toàn mới trong phút đó bị từ chối, còn tổ hợp label đã tồn tại từ trước vẫn ghi bình thường.
Giới hạn label per-descriptor: 200 cho Prometheus
Một descriptor Prometheus được phép tối đa 200 label — cao hơn nhiều so với 30 label của custom metric thường, nhưng vẫn hữu hạn. Lỗi liên quan "metric would have over PER_PROJECT_LIMIT labels" xảy ra khi metric định nghĩa lại quá nhanh với tập hợp label key khác nhau (không phải label value khác nhau — đây là điểm dễ nhầm: label value đóng góp vào time series cardinality ở tầng 2, còn label key không cố định đóng góp vào cardinality của chính descriptor). Nguồn phổ biến nhất theo tài liệu troubleshooting: cAdvisor tự triển khai (self-deployed) đính kèm container annotation làm label động, hoặc relabeling rule gắn thêm label runtime không cố định.
Tầng 3 — Monarch Quarantiner: khi một "monitored resource" phình quá lớn
Đây là cơ chế ít được biết đến nhất nhưng quan trọng bậc nhất để hiểu failure mode thực sự nguy hiểm ở production scale. Một monitored resource trong mô hình GCP là đơn vị gom nhóm time series theo resource type (ví dụ k8s_container, prometheus_target) — về bản chất là một shard logic trên Monarch. Khi số lượng time series thuộc về cùng một monitored resource instance tăng quá lớn, Monarch áp dụng cơ chế bảo vệ gọi là quarantine đối với resource đó, biểu hiện qua lỗi "Monitored resource has too many time series" kèm HTTP 503 khi ghi.
Nguyên nhân gốc rễ hầu như luôn giống nhau: một exporter ghi metric thay mặt cho nhiều resource khác nhau nhưng không phân biệt chúng đúng cách qua label instance. Ví dụ điển hình theo tài liệu troubleshooting chính thức:
statsd_exporternhận metric từ hàng trăm ứng dụng khác nhau nhưng tất cả đều được ghi dưới cùng mộtinstancelà IP:port của chính statsd_exporter — Monarch nhìn thấy một "resource" (chính exporter) nhưng phải gánh cardinality của hàng trăm ứng dụng phía sau nó.kube-state-metricskhông sharding trên cluster rất lớn — một Pod duy nhất báo cáo metric cho toàn bộ object trong cluster, khiến monitored resource của chính Pod đó phình to theo số lượng object cluster.- Envoy/Istio sidecar proxy báo cáo traffic metric với
instancecố định thay vì phản ánh đúng namespace/workload nguồn.
Thứ tự khắc phục theo hiệu quả, từ tài liệu chính thức
- Chạy exporter riêng mỗi VM/node như node agent hoặc DaemonSet — cách triệt để nhất, đảm bảo mỗi monitored resource instance chỉ gánh cardinality của một node.
- Chạy nhiều exporter mỗi VM, chia metric theo từng exporter với job name khác nhau — giảm tải nếu không thể tách theo node.
- Dùng cơ chế horizontal sharding tích hợp sẵn của kube-state-metrics — công cụ đã được thiết kế sẵn cho chính vấn đề này.
- Relabel
instancevề đúng resource gốc (IP:port thực sự sinh ra metric), kèmhonor_labels: true— sửa tận gốc vấn đề định danh sai resource. - Dùng hàm
hashmodtrong relabeling để chia một scrape job lớn thành nhiều job nhỏ hơn — giải pháp cơ học khi không thể sửa kiến trúc exporter.
Nếu nghi ngờ một resource đang bị quarantine, tài liệu troubleshooting khuyến nghị liên hệ Cloud Customer Care để kiểm tra "Monarch Quarantiner hospitalization logs" — đây là bằng chứng rõ ràng cho thấy cơ chế bảo vệ multi-tenant này tồn tại thật sự ở tầng hạ tầng, không phải một khái niệm trừu tượng.
Tầng 4 — Billing: "active" nghĩa là gì
Chi tiết billing đầy đủ được khai triển ở file 05, nhưng điểm cần nắm ở đây: một time series được tính là "active" — tức tính phí — khi nó có sample mới được ghi trong một khoảng thời gian nhất định, không phải khi nó tồn tại về mặt định nghĩa. Điều này có nghĩa cardinality explosion không tự "tắt cost" khi label value cũ ngừng xuất hiện — chỉ khi time series đó ngừng nhận sample mới (ví dụ user_id không còn hoạt động), nó mới ngừng được tính là active. Nhưng nếu label value liên tục xoay vòng (ví dụ request ID mới mỗi giây), tổng số active time series tại bất kỳ thời điểm nào vẫn cao bất kể tổng lịch sử đã tích lũy bao nhiêu.
Danh sách label không nên bao giờ đưa vào metric (bài học rút ra từ bốn tầng trên)
Tổng hợp lại từ cơ chế bốn tầng, các loại giá trị sau đây về bản chất không có giới hạn trên và không nên xuất hiện như một label value trên bất kỳ metric nào được export ra GMP:
- Định danh người dùng/khách hàng (
user_id,session_id,email) — cardinality tỷ lệ thuận số người dùng, không có trần. - Request ID / trace ID / timestamp nhúng vào label — cardinality tỷ lệ thuận traffic, thực chất là dữ liệu log, không phải dữ liệu metric.
- Full URL path chưa được template hóa (
/api/v1/users/12345thay vì/api/v1/users/{id}) — mỗi resource ID khác nhau nhân bản số time series. - IP nguồn của client trong môi trường có traffic công khai — cardinality tỷ lệ thuận số client duy nhất.
Nguyên tắc phân định đúng: nếu một giá trị có bản chất định danh một sự kiện hoặc một thực thể cụ thể (thay vì định danh một loại sự kiện hay một nhóm thực thể có số lượng giới hạn), nó thuộc về Cloud Logging (structured log), không thuộc về metric. Đây là ranh giới mental model quan trọng nhất của toàn bộ observability stack: metric trả lời "bao nhiêu/tỷ lệ nào", log trả lời "cái gì cụ thể đã xảy ra" — trộn lẫn hai vai trò này vào cùng một hệ thống luôn dẫn đến cardinality explosion.