Skip to content

Tối Ưu Hóa PromQL: Query Nào Đắt, Vì Sao

Nhắc lại cơ chế fan-out: nền tảng để hiểu mọi tối ưu ở phần này

File 01 đã thiết lập mental model cốt lõi: mỗi query PromQL là một request fan-out qua ba tầng (root mixer → zone mixer → leaf), và Monarch cố gắng đẩy phép tính xuống tầng thấp nhất có thể trước khi truyền kết quả lên trên. Toàn bộ phần tối ưu PromQL trong file này chỉ là hệ quả áp dụng nguyên lý đó vào các pattern PromQL cụ thể — không có "mẹo" nào ở đây tách rời khỏi cơ chế kiến trúc đã nói.

Nguyên tắc tổng quát duy nhất cần nhớ: một query rẻ là query mà phần lớn phép tính (rate, sum, filter theo label) có thể hoàn thành ở zone mixer/leaf trước khi dữ liệu di chuyển lên root mixer; một query đắt là query buộc root mixer phải nhìn thấy tập dữ liệu lớn trước khi có thể tính ra kết quả cuối.

Query timeout thực tế: 120 giây, không phải 30 giây mặc định của Grafana

Trước khi bàn tối ưu, cần một mốc tham chiếu cụ thể: theo tài liệu chính thức, GMP cho phép query chạy tối đa khoảng 120 giây trước khi bị hủy, trong khi Grafana mặc định timeout ở 30 giây (Query using the Prometheus API or UI). Rất nhiều báo cáo "GMP chậm" hoặc "GMP timeout" thực chất là lỗi cấu hình phía client — Grafana hủy request ở giây thứ 30 dù Monarch vẫn đang xử lý bình thường và có thể trả kết quả trong 120 giây. Bước đầu tiên trước khi tối ưu bất kỳ query nào là xác nhận đúng phía nào đang timeout — chỉnh TimeoutQuery timeout của data source Grafana lên 120/2m trước khi kết luận query có vấn đề thật sự.

Query rẻ: aggregation có thể đẩy xuống, filter theo label sớm

promql
sum(rate(http_requests_total{cluster="prod-us1"}[5m])) by (namespace)

Query này rẻ vì hai lý do cộng hưởng: (1) filter cluster="prod-us1" giới hạn fan-out chỉ đến một zone, không cần hỏi mọi zone mixer trên toàn cầu; (2) rate() và bước đầu của sum by (namespace) là phép tính có thể thực hiện cục bộ tại từng leaf/zone mixer trước khi gộp — mỗi zone chỉ cần trả về kết quả đã aggregate theo namespace, không phải toàn bộ raw sample.

Query đắt loại 1: thiếu filter định danh zone/project, buộc fan-out toàn cục

promql
sum(rate(http_requests_total[5m])) by (pod)

Không có filter cluster/project_id, root mixer buộc phải fan-out đến mọi zone thuộc metrics scope hiện tại. Tệ hơn, by (pod) giữ lại một label cardinality cao (số lượng Pod có thể lên đến hàng chục nghìn trong tổ chức lớn) — nghĩa là kết quả trả về từ mỗi zone mixer vẫn còn cardinality cao, không được "nén" đáng kể qua bước aggregate. Kết quả: một lượng dữ liệu lớn phải di chuyển từ nhiều zone mixer lên root mixer để gộp lần cuối, đúng pattern gây tốn kém đã nói ở file 01.

Cách sửa: luôn filter theo project_id/cluster khi biết phạm vi cần hỏi, và cân nhắc có thực sự cần by (pod) (cardinality cao) hay by (namespace)/by (deployment) (cardinality thấp hơn nhiều bậc) đã đủ trả lời câu hỏi vận hành.

Query đắt loại 2: ratio query khuếch đại lỗi transient và khó tối ưu execution plan

promql
sum(rate(http_requests_total{status=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))

Tài liệu troubleshooting chính thức chỉ ra rõ ràng: nên tách ratio query thành hai query độc lập (numerator, denominator) thay vì viết trong cùng một expression, đây là "Prometheus best practice" chứ không riêng gì GMP — vì "dùng tỷ lệ có thể khuếch đại tác động của sự cố thoáng qua" (transient issue). Cơ chế cụ thể: nếu tại một thời điểm dữ liệu đến trễ (được ghi nhận thực tế có độ trễ 3-7 giây trong tài liệu troubleshooting) chỉ ảnh hưởng đến một trong hai vế (ví dụ denominator có dữ liệu đầy đủ nhưng numerator bị thiếu 1 sample do trễ), tỷ lệ tính ra có thể lệch rất xa giá trị thật trong một khoảng ngắn — gây ra false positive alert dù hệ thống hoàn toàn khỏe mạnh.

Về mặt execution trên Monarch, tách numerator/denominator còn có lợi ích thứ hai ít được nhắc đến: mỗi vế trở thành một sub-expression độc lập có thể được tối ưu execution plan riêng, thay vì Monarch phải giữ cả hai luồng dữ liệu đồng bộ trong cùng một cây tính toán trước khi chia.

Query đắt loại 3: long-range lookback chạy quá thường xuyên

promql
# Rule group evaluate mỗi phút, nhưng lookback window 4 tuần
increase(job_completed_total[4w])

Đây là pattern gây ra lỗi "Cancelled due to number of queries... 501 ≥ 500" — một giới hạn nội bộ về số lượng query đồng thời cho phép trên mỗi project. Nguyên nhân không nằm ở bản thân query 4 tuần (Monarch xử lý được), mà ở tần suất evaluate: nếu một Rules/ClusterRules group đặt interval: 1m cho một expression lookback 4 tuần, mỗi phút Monarch phải quét lại toàn bộ 4 tuần dữ liệu — trong khi giá trị kết quả gần như không đổi giữa hai lần evaluate liên tiếp cách nhau một phút. Khuyến nghị chính thức: các rule long-lookback (từ vài ngày trở lên) nên evaluate theo giờ, không phải theo phút — tần suất evaluate nên tỷ lệ nghịch với độ dài lookback window, không phải cố định một giá trị interval cho mọi rule.

Query đắt loại 4: query trên dữ liệu thưa (sparse) với rate window quá ngắn

Nếu một metric chỉ được scrape mỗi 60 giây nhưng query dùng rate(metric[1m]), kết quả rất dễ bị NaN hoặc nhiễu vì cửa sổ 1 phút chỉ chứa đúng 1-2 sample — không đủ để rate() nội suy chính xác. Nguyên tắc Prometheus chuẩn (áp dụng nguyên vẹn cho GMP): cửa sổ rate() nên tối thiểu gấp 4 lần scrape interval để đảm bảo luôn có đủ điểm dữ liệu ngay cả khi một lần scrape bị miss. Với interval: 60s, cửa sổ hợp lý tối thiểu là [4m], không phải [1m].

Kỹ thuật offset: phòng vệ trước dữ liệu đến trễ

Vì Monarch là hệ thống phân tán ghi qua gRPC streaming từ hàng triệu collector, dữ liệu không đảm bảo đến đúng thứ tự thời gian tuyệt đối — tài liệu troubleshooting ghi nhận độ trễ thực tế khoảng 3-7 giây là bình thường. Với alerting rule hoặc dashboard cần độ chính xác cao ở các điểm dữ liệu gần nhất (now()), áp dụng modifier offset (tối thiểu gấp đôi khoảng lookback dài nhất trong cùng biểu thức) giúp tránh việc query chạm vào vùng dữ liệu chưa ổn định:

promql
rate(http_requests_total[5m] offset 30s)

Kết hợp với việc đặt for: trong alerting rule tối thiểu bằng hai lần evaluation interval, đây là hai kỹ thuật chính thức được khuyến nghị để giảm nhiễu do độ trễ ghi dữ liệu — không phải để "che giấu" vấn đề mà để phản ánh đúng thực tế rằng một hệ thống phân tán planet-scale không thể có consistency tức thời tuyệt đối, và query cần được viết với giả định đó.

Recording rule như công cụ tối ưu execution plan, không chỉ tối ưu độ trễ dashboard

File 03 đã nói recording rule giảm chi phí tính toán tại thời điểm query. Ở góc độ tối ưu PromQL, cần nhấn mạnh thêm: khi một expression phức tạp (nhiều tầng rate() + sum by() + phép chia) được pre-compute thành một metric mới, mọi dashboard/alerting policy dùng lại metric đó chỉ cần một phép đọc time series đơn giản — loại bỏ hoàn toàn nhu cầu fan-out phức tạp mỗi lần load. Đây là lý do vì sao tổ chức vận hành GMP ở quy mô lớn nên coi việc "có bao nhiêu recording rule" như một chỉ số sức khỏe hệ thống — quá ít recording rule nghĩa là dashboard đang query trực tiếp expression nặng mỗi lần refresh; quá nhiều recording rule (đặc biệt nếu chỉ dùng một lần) nghĩa là đang trả chi phí cardinality không tương xứng với lợi ích.

Bảng tổng hợp: nhận diện nhanh query đắt trước khi chạy

Dấu hiệu trong PromQLVì sao đắtCách sửa
Không filter cluster/project_idFan-out toàn bộ metrics scopeLuôn thêm filter định danh zone sớm nhất có thể
by (label_cardinality_cao)Kết quả không được nén trước khi lên root mixerAggregate theo label cardinality thấp hơn nếu đủ dùng
Ratio trong cùng expressionKhuếch đại lỗi transient, khó tối ưu planTách numerator/denominator thành hai query
Lookback dài + evaluate thường xuyênQuét lại dữ liệu không đổi nhiều lầnGiảm tần suất evaluate tỷ lệ nghịch với độ dài lookback
rate() window quá ngắn so với scrape intervalKhông đủ điểm dữ liệu, kết quả nhiễu/NaNWindow tối thiểu gấp 4 lần scrape interval
Query gần now() không có offsetChạm vùng dữ liệu chưa ổn định (trễ 3-7s)Thêm offset, tăng for: tối thiểu 2× interval

Tham khảo chính thức