Ba mươi bảy phần trước đều ngầm giả định rằng hệ giám sát đang chạy. Phần này bỏ giả định đó và hỏi một câu khó chịu: nếu chính hệ giám sát chết, bạn biết bằng cách nào?

Câu hỏi ấy không trừu tượng. Một hệ giám sát chết là hệ giám sát im lặng, mà im lặng thì trông y hệt như "mọi thứ đều ổn". Đây là kiểu hỏng duy nhất mà công cụ càng hỏng nặng lại càng trông yên bình.

Bon kieu hong cua he giam sat va cach phat hien nao thuc su keu

Bảng số liệu

Hệ đo: exporter phơi chỉ số dich_vu_hong, Prometheus thu thập và đánh giá luật mỗi 10 giây, Alertmanager với group_wait: 0srepeat_interval: 1m, một máy nhận webhook ghi mốc thời gian. Bốn luật cảnh báo chạy song song trên cùng hệ đó:

DichVuHong    expr: dich_vu_hong > 0        <- luat ban that su viet ra
MucTieuChet   expr: up{job="app"} == 0      <- Prometheus tu sinh chi so "up"
ChiSoBienMat  expr: absent(dich_vu_hong)
NhipTim       expr: vector(1)               <- luon luon keu (dead man's switch)

Rồi giết lần lượt từng thành phần. Ba lần chạy độc lập.

Hỏng ở đâu DichVuHong MucTieuChet ChiSoBienMat NhipTim
Dịch vụ hỏng thật kêu, 0,1–13,5 s đến
Exporter biến mất im lặng, 3/3 lần kêu, 3,5–17,6 s kêu, 3,5–37,6 s đến
Prometheus chết im im im vẫn đến
Alertmanager chết im im im im

Điều đáng nhớ

Luật bạn viết ra để bắt sự cố là luật im lặng đúng lúc cần nhất. Khi exporter biến mất, dich_vu_hong > 0 không kêu — cả ba lần. Lý do đơn giản đến mức dễ bỏ qua: biểu thức ấy cần chuỗi dich_vu_hong để so sánh. Không có chuỗi thì không có gì để so, và PromQL trả về tập rỗng. Tập rỗng không lớn hơn 0. Nó cũng không nhỏ hơn 0. Nó chỉ đơn giản là không có gì.

Đây là khác biệt giữa "tôi đo được và nó ổn" với "tôi không đo được gì cả". Một luật ngưỡng thuần tuý gộp cả hai vào cùng một trạng thái: yên lặng.

Hai luật bắt được nó — up == 0absent() — đều không nhắc tới điều kiện hỏng. Chúng nói về sự tồn tại của dữ liệu, không phải về giá trị của dữ liệu.

Đáng chú ý là hai luật ấy không kêu cùng lúc. up == 0 bắt được ở giây 3,5, 10,9 và 17,6; absent() ở giây 3,5, 10,9 và 37,6. Lần thứ ba chênh nhau 20 giây, vì up chuyển sang 0 ngay ở lần thu thập hỏng đầu tiên, còn absent() phải đợi tới khi chuỗi cũ thật sự rời khỏi cửa sổ tra cứu của Prometheus. Cả hai đều đủ nhanh, nhưng up nhanh hơn và ổn định hơn — thêm một lý do để nó là luật đầu tiên bạn dựng.

Nhịp tim vẫn đập sau khi tim đã ngừng. Đây là con số làm tôi phải đo riêng. Giết Prometheus ở mốc 0, rồi theo dõi 420 giây:

Lần Nhịp tim vẫn đến ở Nhịp cuối
1 70 s, 130 s, 190 s 190 s
2 7 s, 67 s, 127 s, 187 s 187 s
3 44 s, 104 s, 164 s 164 s

Ba đến bốn nhịp nữa, kéo dài 164–190 giây sau khi Prometheus đã chết hẳn.

Nguyên nhân: nhịp tim đó không do Prometheus phát trực tiếp tới máy nhận. Prometheus gửi cảnh báo sang Alertmanager kèm một hạn dùng đặt trước vài phút, rồi chính Alertmanager mới là bên phát lại đều đặn theo repeat_interval. Prometheus chết đi, Alertmanager không hay biết — nó vẫn còn một cảnh báo chưa hết hạn trong tay và vẫn phát lại đúng lịch, cho tới khi hạn ấy trôi qua.

Nghĩa là một dead man's switch đi qua Alertmanager có khoảng mù xấp xỉ ba phút. Bộ theo dõi bên ngoài đặt ngưỡng "không thấy nhịp trong 2 phút thì báo động" sẽ không bao giờ kêu, vì trong hai phút đó nhịp vẫn về đều.

Vì sao

Bốn thành phần, mỗi cái chết đi thì phần còn lại thấy khác nhau:

  • Exporter chết — Prometheus vẫn sống và tự sinh chỉ số up{job="app"} = 0. Đây là chỉ số Prometheus tạo ra cho mỗi lần thu thập, không phải do ứng dụng phơi ra, nên nó tồn tại kể cả khi ứng dụng đã biến mất. Đó là lý do up == 0 là tấm lưới rẻ nhất và đáng dựng nhất.
  • Prometheus chết — không còn ai đánh giá luật. Mọi luật đều im, kể cả up == 0, vì up cũng do chính Prometheus sinh ra.
  • Alertmanager chết — Prometheus vẫn đánh giá luật và vẫn cố gửi, nhưng không còn ai chuyển tiếp. Đây là kiểu hỏng khó chịu nhất: mọi thứ trong Prometheus trông hoàn hảo, bảng điều khiển xanh, luật đang FIRING — mà không ai nhận được gì.

Chuỗi phụ thuộc chỉ đi một chiều: mỗi lớp chỉ giám sát được lớp dưới nó. Không lớp nào tự giám sát được chính mình, và không lớp nào giám sát được lớp trên. Muốn bịt đầu trên thì phải có một thứ nằm ngoài hẳn — và thứ đó không được đi qua bất kỳ thành phần nào nó đang canh chừng.

Nghĩa là gì trong thực tế

Ba việc, xếp theo tỷ lệ lợi ích trên công sức:

  • Dựng up == 0 cho mọi job. Một luật, bắt được kiểu hỏng phổ biến nhất, và bắt trong 3,5–17,6 giây. Đây là thứ rẻ nhất trong cả bài.
  • Với chỉ số quan trọng, thêm absent(). up chỉ biết mục tiêu còn trả lời hay không; nó không biết mục tiêu có còn phơi ra đúng chỉ số bạn cần hay không. Một lần đổi tên chỉ số khi nâng cấp là đủ để up vẫn xanh mà luật ngưỡng thì mù.
  • Dead man's switch phải đi đường riêng. Nếu nhịp tim của bạn đi qua chính Alertmanager mà nó đang canh, khoảng mù là ba phút. Cách đúng là để một dịch vụ bên ngoài — không dùng chung hạ tầng — chờ tín hiệu; và ngưỡng chờ phải lớn hơn khoảng mù đó, nếu không bạn sẽ có báo động giả mỗi lần Alertmanager khởi động lại.

Một cách kiểm nhanh: tắt exporter của một dịch vụ thật trong giờ hành chính và bấm giờ. Nếu sau năm phút không ai nhận được gì, bạn vừa biết chính xác mình đang mù ở đâu — với chi phí bằng đúng năm phút.

Chỗ tôi không kết luận được

Con số 164–190 giây gắn với repeat_interval: 1m và hạn dùng mặc định của Prometheus. Đổi hai tham số ấy thì khoảng mù đổi theo, và tôi không quét hết các tổ hợp — chỉ đo đúng một cấu hình, ba lần.

Chu kỳ thu thập ở đây là 10 giây, ngắn hơn mặc định 15 giây, nên mọi thời gian phát hiện trong bài đều lạc quan hơn hệ thật một chút.

Và tôi giết container, tức là hỏng dứt khoát. Kiểu hỏng khó hơn nhiều là hỏng một nửa: Prometheus còn sống nhưng hết đĩa, hoặc Alertmanager nhận được nhưng gửi Slack thất bại. Những kiểu đó không hiện ra trong bảng này, và chúng cần một bài khác.

Thử ba mươi giây

Mở tệp luật cảnh báo của bạn ra và đếm hai con số: bao nhiêu luật nói về giá trị (> 0, > 0.9, < 100), và bao nhiêu luật nói về sự tồn tại (up ==, absent().

Nếu con số thứ hai bằng không thì toàn bộ hệ cảnh báo của bạn chỉ bắt được đúng một kiểu hỏng: kiểu mà mọi thứ vẫn chạy và báo cáo trung thực về việc mình đang hỏng.