Phần trước ta đã instrument một app với bốn loại metric. Nhưng khi mở /metrics ra, bạn thấy http_requests_total 1055 và... rồi sao? Con số cộng dồn đó gần như vô nghĩa khi đọc trực tiếp — nó chỉ cho biết tổng từ lúc app khởi động, không cho biết bây giờ service đang nhận bao nhiêu request, endpoint nào nóng, hay tỉ lệ lỗi là bao nhiêu. Toàn bộ giá trị của metric được mở khoá bởi PromQL — ngôn ngữ truy vấn biến đống chuỗi thời gian thô thành câu trả lời. Bài này (phần 3 loạt Observability) dựng một app có counter gắn nhãn, bắn tải đều, rồi chạy các query PromQL thật trên obs-prom để thấy chính xác mỗi hàm làm gì.

Nhãn, và vì sao counter thô vô dụng

Metric của Prometheus hiếm khi là một con số đơn — nó gắn nhãn (label) để tách chiều. Một CounterVec với nhãn path và status tạo một chuỗi thời gian riêng cho mỗi tổ hợp:

var reqs = promauto.NewCounterVec(
    prometheus.CounterOpts{Name: "http_requests_total"},
    []string{"path", "status"})

// mỗi tổ hợp nhãn là một chuỗi độc lập
reqs.WithLabelValues("/api/users", "200").Inc()
reqs.WithLabelValues("/api/login", "500").Inc()

Counter chỉ tăng. Đọc giá trị thô (1055) cho bạn tổng tích luỹ từ lúc process chạy — một con số phụ thuộc uptime, không so sánh được giữa các instance, không phản ánh tải hiện tại. Điều bạn thật sự muốn biết là tốc độ: nó tăng bao nhanh mỗi giây. Đó là việc của rate().

Ảnh chụp đoạn mã Go nền tối khai báo CounterVec http_requests_total với nhãn path và status, bộ tải nền tăng counter theo tỉ lệ biết trước users 10 mỗi giây orders 5 mỗi giây login 500 nửa mỗi giây lỗi, và danh sách câu PromQL rate http_requests_total 1m tốc độ mỗi chuỗi, sum by path rate gộp theo path, sum by status rate gộp theo status, increase path api users 1m đếm trong 1 phút, sum rate status 500 chia sum rate tất cả tỉ lệ lỗi toàn hệ thống

Hình 1: Counter http_requests_total gắn nhãn path và status; bộ tải nền tăng nó theo tỉ lệ biết trước để output PromQL dễ kiểm chứng; bên dưới là các câu PromQL ta sẽ chạy — rate, sum by, increase và một biểu thức tính tỉ lệ lỗi.

Đo thật: chạy query trên obs-prom

Mình chạy app trong go-lab, để obs-prom scrape mỗi 5 giây. Bộ tải nền tăng counter theo tỉ lệ cố định: /api/users 10/s, /api/orders 5/s, /api/login 2/s, cộng ít request lỗi (status="500"). Sau khi app chạy đủ một phút (để cửa sổ [1m] có dữ liệu), mình gọi /api/v1/query trên obs-prom:

Ảnh chụp kết quả PromQL thật nền tối từ obs-prom, counter thô users 200 bằng 1055 orders 200 bằng 527 login 200 bằng 211 login 500 bằng 52 orders 500 bằng 29, rate 1m users 200 bằng 10 orders 200 bằng 5 login 200 bằng 2 login 500 bằng 0.509 orders 500 bằng 0.273, sum by path rate users 10 orders 5.273 login 2.509, sum by status rate 200 bằng 17 và 500 bằng 0.782, increase users 1m bằng 600 đúng 10 mỗi giây nhân 60 giây, tỉ lệ lỗi sum rate 500 chia sum rate tất cả bằng 0.782 chia 17.78 bằng 0.0440 khoảng 4.4 phần trăm request lỗi

Hình 2: Kết quả thật. Counter thô (1055, 527…) vô nghĩa khi đọc trực tiếp; rate(...[1m]) trả về đúng tốc độ đã đặt (10, 5, 2 req/s); sum by (path) và sum by (status) gộp theo chiều; increase(users[1m]) ra đúng 600; tỉ lệ lỗi tính được 0.0440 (~4.4%).

Đọc từng query:

  • rate(http_requests_total[1m]) — với mỗi chuỗi, tính tốc độ tăng trung bình mỗi giây trong cửa sổ 1 phút. Kết quả: users/200 = 10, orders/200 = 5, login/200 = 2 — đúng tỉ lệ mình đặt. Đây là con số bạn thật sự quan tâm, không phải counter thô. rate() cũng tự xử lý counter reset (app restart) nên không bao giờ ra giá trị âm.
  • sum by (path) (rate(...)) — gộp các chuỗi, giữ nhãn path, bỏ status. Nên /api/login = 2.509 (gộp cả 200 lẫn 500: 2 + 0.509). Đây là cách trả lời "endpoint nào nóng nhất" bất kể status.
  • sum by (status) (rate(...)) — gộp theo chiều kia: tổng mọi request 200 = 17/s, mọi request 500 = 0.782/s, không quan tâm path.
  • increase(http_requests_total{path="/api/users"}[1m]) — khác rate, increase cho số lượng tăng trong cửa sổ (không chia cho giây). Kết quả 600 — đúng bằng 10/s × 60s. Dùng khi bạn muốn "có bao nhiêu request trong 1 phút qua" thay vì tốc độ.
  • Tỉ lệ lỗi — sum(rate(...{status="500"}[1m])) / sum(rate(...[1m])) = 0.782 / 17.78 = 0.0440, tức ~4.4% request bị lỗi. Một dòng PromQL cho ra chỉ số sức khoẻ cốt lõi của service.

Điểm mấu chốt: cùng một bộ counter thô, PromQL bóc ra được tốc độ, phân rã theo bất kỳ nhãn nào, và chỉ số tổng hợp — tất cả sau khi thu thập, không cần đổi code app.

Vì sao luôn bọc counter trong rate() trước khi gộp

Một lỗi kinh điển của người mới: sum(http_requests_total) — cộng thẳng các counter thô. Nó cho một con số tăng vô hạn, nhảy giật mỗi khi một instance restart (counter về 0 kéo tổng tụt xuống), và hoàn toàn không phản ánh tải. Thứ tự đúng luôn là: rate() trước (biến counter thô thành tốc độ, xử lý reset), rồi sum() sau (gộp các tốc độ lại). sum(rate(...)) có nghĩa; rate(sum(...)) thì không (bạn không thể rate một đại lượng đã mất thông tin per-series về reset). Quy tắc: rate trong, sum ngoài.

Đánh đổi cần cân nhắc

Cửa sổ [1m] phải chứa đủ điểm dữ liệu. rate() cần ít nhất 2 mẫu trong cửa sổ để tính. Với scrape 5s, [1m] có ~12 mẫu — thừa. Nhưng nếu scrape interval là 1 phút mà bạn dùng rate(...[1m]), cửa sổ chỉ chứa 1 điểm và query trả về rỗng. Quy tắc an toàn: cửa sổ range nên ít nhất gấp 4 lần scrape interval. Query cảnh báo thường dùng [5m] để mượt và bền với một lần scrape lỗi.

rate làm mượt, nên che gai nhọn ngắn. rate(...[5m]) trung bình hoá 5 phút — một đợt lỗi bùng 10 giây sẽ bị pha loãng và có thể không chạm ngưỡng cảnh báo. Cửa sổ ngắn ([1m]) nhạy hơn nhưng nhiễu hơn. Chọn cửa sổ là đánh đổi giữa độ nhạy và độ ổn định; không có giá trị đúng cho mọi trường hợp.

rate nội suy nên con số có thể hơi "lẻ". Ở đây kết quả ra tròn (10, 5, 600) vì tải cực đều, nhưng trong thực tế rate ngoại suy ở biên cửa sổ và căn mẫu không trùng ranh giới, nên bạn thường thấy 9.98 thay vì 10. Đó là bản chất của phép tính trên chuỗi thời gian rời rạc, không phải lỗi — đừng mong con số tuyệt đối chính xác từ rate.

Ba ý mang về

  1. Counter thô vô dụng, rate() mới có nghĩa: đo thật counter http_requests_total=1055 chẳng nói gì, nhưng rate(...[1m]) ra đúng 10/5/2 req/s — tốc độ hiện tại là thứ bạn thật sự cần, và rate tự xử lý counter reset.
  2. sum by phân rã theo nhãn, increase đếm số lượng: đo thật sum by (path) tách endpoint nóng, sum by (status) tách lỗi, increase(users[1m])=600 cho số request trong cửa sổ — cùng bộ metric, nhiều góc nhìn.
  3. Rate trong, sum ngoài; chọn cửa sổ cẩn thận: sum(rate(...)) đúng còn sum counter thô thì sai; cửa sổ range nên ≥4× scrape interval, và cửa sổ ngắn nhạy nhưng nhiễu — ví dụ một dòng tính tỉ lệ lỗi ra 4.4% cho chỉ số sức khoẻ tức thì.

Nguồn

Phần sau ta mổ xẻ percentile: vì sao trung bình độ trễ nói dối, p99 thật sự nghĩa là gì, và histogram_quantile tính nó từ bucket ra sao — chạy thật để thấy average che giấu đuôi chậm thế nào.