Phần trước đo giá của bốn kiểu chỉ số và kết luận rằng Histogram đắt gấp chín lần Counter về số chuỗi. Phần này trả lời câu còn lại: có đáng không. Tôi sinh một triệu mẫu độ trễ có hình dạng thật, nạp vào một Histogram thật, dựng một Prometheus thật để truy vấn, rồi so ba con số mà mọi bảng điều khiển đều có sẵn: trung bình, p50, và p99.
Bảng số liệu
Một triệu mẫu độ trễ: 99% quanh 20 ms, 1% rơi vào đuôi dài quanh 500 ms — hình dạng quen thuộc của một dịch vụ có một phụ thuộc thỉnh thoảng chậm. Nạp vào prometheus_client, phục vụ qua /metrics, Prometheus 2.54.1 thu thập và truy vấn.
| Giá trị | So với p50 | |
|---|---|---|
| p50 | 20,37 ms | 1,00× |
| Trung bình | 27,45 ms | 1,35× |
| p90 | 36,95 ms | 1,81× |
| p99 | 161,33 ms | 7,92× |
| p99.9 | 776,33 ms | 38,1× |
| max | 2 133,85 ms | 104,8× |
Rồi tôi cho tỷ lệ request chậm tăng dần và xem ba chỉ số phản ứng thế nào:
| Tỷ lệ chậm | p50 | Trung bình | p99 |
|---|---|---|---|
| 0,5% (nền) | 20,28 ms | 25,01 ms | 64,84 ms |
| 1,0% | 20,38 ms | 27,43 ms (+10%) | 134,81 ms (+108%) |
| 2,0% | 20,43 ms | 32,47 ms (+30%) | 492,99 ms (+660%) |
| 3,0% | 20,57 ms | 37,62 ms (+50%) | 580,15 ms (+795%) |
| 5,0% | 20,93 ms (+3%) | 47,70 ms (+91%) | 666,17 ms (+927%) |
Điều đáng nhớ
Nói lại theo cách khác, vì đây là chỗ quyết định bảng điều khiển của bạn có dùng được hay không:
Tỷ lệ hỏng tăng mười lần thì p50 nhích 3%. Từ 20,28 lên 20,93 ms. Nếu bạn theo dõi độ trễ trung vị, bạn sẽ không thấy gì trong suốt cả sự cố.
Trung bình tăng 91% — nghe nhiều, nhưng vẫn quá ít. Nó đi từ 25 lên 48 ms. Một cảnh báo đặt ở "gấp đôi mức bình thường" sẽ nổ đúng lúc 5% người dùng đã chịu độ trễ trên nửa giây suốt một thời gian.
p99 tăng 927%. Nó nhạy gấp hơn mười lần trung bình, và nó bắt đầu kêu từ mức 1% — chỗ mà trung bình mới chỉ nhúc nhích 10%.
Và trung bình nằm sai chỗ ngay cả khi mọi thứ bình thường. Ở trạng thái nền, trung bình là 27,45 ms — chỉ cao hơn trung vị 1,35 lần, nhưng thấp hơn p99 tới 5,88 lần. Nó không mô tả trải nghiệm của đa số (p50 làm việc đó tốt hơn) và cũng không mô tả trải nghiệm của người xui (p99 làm việc đó). Nó là một con số không đại diện cho ai.
Vì sao
Trung bình là tổng chia số lượng, nên mỗi mẫu đóng góp đúng tỷ trọng của nó trong tổng thể. Khi 99% mẫu nằm quanh 20 ms, chúng ghim trung bình xuống gần đó bất kể đuôi có tệ đến đâu. Muốn kéo trung bình lên gấp đôi, bạn cần phần đuôi đóng góp bằng cả phần thân — với đuôi 500 ms và thân 20 ms, tức là cần khoảng 4% lưu lượng bị chậm. Đúng như bảng: ở 5% thì trung bình mới tăng 91%.
Phân vị thì không cộng gộp, nó đếm vị trí. p99 hỏi "mẫu chậm thứ 1% là bao nhiêu", nên nó nhảy ngay khi phần đuôi vượt qua ngưỡng 1%. Đó là lý do nó đi từ 64,84 lên 134,81 ms khi tỷ lệ chậm chỉ đi từ 0,5% lên 1% — đuôi vừa đủ rộng để chạm tới vị trí mà p99 đang đứng.
Nói ngắn: trung bình đo tổng khối lượng đau, phân vị đo ai đang đau. Với hệ thống phục vụ người dùng, câu hỏi thứ hai mới là câu hỏi đúng.
Có một chi tiết đáng tin cậy và hơi trớ trêu ở đây. Trung bình mà Prometheus tính từ lat_sum / lat_count cho 27,45 ms — khớp chính xác với trung bình tính từ một triệu mẫu thô, không sai một chữ số. Lý do là _sum và _count không đi qua bucket nào cả; chúng là hai bộ đếm thật. Trong khi đó mọi phân vị lấy từ Histogram đều chỉ là ước lượng nội suy. Vậy con số chính xác nhất mà Histogram cho bạn lại đúng là con số ít dùng nhất.
Nghĩa là gì trong thực tế
- Bỏ trung bình khỏi bảng điều khiển chính, hoặc để nó ở hàng dưới. Nó không sai, nó chỉ trả lời một câu hỏi không ai hỏi.
- Vẽ tối thiểu p50 và p99 cạnh nhau. Khoảng cách giữa hai đường mới là thứ nói lên hình dạng phân bố. Ở đây tỷ số p99/p50 đi từ 3,2 lên 31,8 trong suốt sự cố — một chỉ số cảnh báo tốt hơn cả hai con số riêng lẻ.
- Đặt cảnh báo trên phân vị, không trên trung bình. Với dữ liệu này, ngưỡng "p99 vượt 200 ms" bắt được sự cố ở mức 1,5% lưu lượng; ngưỡng "trung bình gấp đôi" phải đợi tới 5%.
- Nhớ lại phần 1 của sê-ri: p99 mù trước sự cố chạm dưới 1% lưu lượng. Cộng với bài này, quy tắc đầy đủ là: theo dõi p50, p99 và p99.9, và biết rằng mỗi cái có một vùng mù riêng.
- Nếu buộc phải chọn đúng một con số, hãy chọn p99, không phải trung bình.
Còn một cách đọc bảng nữa đáng để ý. Nhìn cột p50: nó gần như bất động qua cả năm mức, từ 20,28 tới 20,93 ms. Điều đó không phải vì trung vị là chỉ số tồi, mà vì nó đang trả lời đúng câu hỏi của nó — trải nghiệm của người dùng điển hình thật sự không đổi. Sự cố này không làm chậm mọi người; nó làm chậm một nhóm nhỏ, rất nặng. Đó là lý do vì sao đọc một mình p50 rồi kết luận "hệ thống bình thường" là một suy luận đúng về dữ liệu nhưng sai về thực tế.
Cặp p50 và p99 vẽ cạnh nhau giải quyết được chuyện đó mà không cần thêm chỉ số nào: khi hai đường tách xa nhau, bạn biết có một nhóm đang chịu thứ mà đa số không chịu, kể cả khi cả hai đường đều nằm dưới ngưỡng cảnh báo của chính chúng.
Chỗ tôi không kết luận được
Hình dạng phân bố là do tôi chọn. Tôi dựng một phân bố hai đỉnh: thân log-normal quanh 20 ms và đuôi log-normal quanh 500 ms. Đó là hình dạng rất thường gặp — một phụ thuộc chậm, một lần trượt bộ đệm, một lần thu gom rác — nhưng nó không phải hình dạng duy nhất. Với phân bố mượt hơn, khoảng cách giữa trung bình và p99 sẽ hẹp lại và các kết luận về độ nhạy sẽ yếu đi. Cái tôi tin là chiều hướng và bậc độ lớn, không phải con số 927%.
Các dòng trong bảng đuôi phình là năm phân bố sinh riêng, không phải một sự cố diễn ra theo thời gian. Mỗi dòng là 100.000 mẫu độc lập với tỷ lệ đuôi khác nhau. Điều đó đủ để so độ nhạy của ba chỉ số, nhưng nó không mô phỏng chuyện một sự cố bắt đầu — trong thực tế còn có độ trễ của cửa sổ trượt, của khoảng thu thập, và của quy tắc for trong cảnh báo.
Tôi chưa động tới sai số của chính phép ước lượng phân vị. Mọi con số p99 trong bài này là phân vị chính xác, tính bằng cách sắp xếp một triệu mẫu — trừ đúng một chỗ tôi nói rõ là lấy từ Prometheus. histogram_quantile không cho bạn con số chính xác đó, và sai số của nó không đều giữa các phân vị. Phần sau của sê-ri đo đúng chuyện này, và có một kết quả tôi không đoán trước được.
Thử ba mươi giây
docker run --rm python:3.12-slim python - <<'EOF'
import random, statistics
random.seed(7)
N = 200_000
def sinh(tail):
a = [random.lognormvariate(-3.9, 0.45) for _ in range(int(N*(1-tail)))]
a += [random.lognormvariate(-0.7, 0.35) for _ in range(N - len(a))]
a.sort(); return a
print("%-12s %10s %12s %12s" % ("ti le cham", "p50", "trung binh", "p99"))
for t in (0.005, 0.01, 0.02, 0.05):
a = sinh(t)
print("%10.1f%% %9.2f ms %9.2f ms %9.2f ms"
% (t*100, a[N//2]*1000, statistics.fmean(a)*1000, a[int(0.99*N)]*1000))
EOF
Đọc theo cột. Cột p50 gần như là một hằng số, cột trung binh bò lên chậm rãi, cột p99 thì nhảy. Ba cột đó là ba câu trả lời khác nhau cho cùng một câu hỏi "hệ thống có ổn không", và chỉ một trong ba nói thật.