P99 nói gì mà trung bình cộng giấu đi
Vì sao độ trễ trung bình đẹp trong khi người dùng vẫn kêu chậm, histogram khác percentile client-side ra sao, và cách đặt SLO đo được từ số đó.
TL;DR: Độ trễ trung bình nói dối vì phân phối lệch phải: đa số request nhanh, một đuôi nhỏ rất chậm kéo trung bình lên nhưng không lộ mức độ tệ của đuôi đó. Percentile P99 trả lời đúng câu hỏi hơn — 99% request nhanh hơn ngưỡng này, 1% tệ nhất nằm trên. Percentile không cộng được qua nhiều instance: Micrometer có percentiles (tính tại chỗ, không gộp được) và percentiles-histogram (đẩy bucket lên để backend gộp rồi mới tính, gộp được — đúng cho hệ nhiều instance). Từ số đo tới SLO là chọn ngưỡng và cửa sổ thời gian, rồi dùng error budget để quyết định có siết tính năng mới hay không.
Dashboard observability của một service báo độ trễ trung bình 120ms — ổn, dưới ngưỡng 200ms team tự đặt. Nhưng đội sản phẩm vẫn nhận khiếu nại "chậm kinh khủng" gần như mỗi ngày, đúng vào khung giờ cao điểm. Cả hai phía đều đúng: dashboard không nói dối, người dùng cũng không bịa. Vấn đề nằm ở câu hỏi bị hỏi sai — trung bình cộng trả lời "một request bất kỳ mất bao lâu", trong khi thứ người dùng thật sự cảm nhận là "request của họ mất bao lâu", và không ai trải nghiệm đúng con số trung bình cả.
Bài trước dạy Timer đã ghi lại thời lượng của từng request — count, sum, max đều nằm sẵn trong bộ nhớ. Bài này là câu hỏi tiếp theo: đọc con số đó thế nào cho đúng, để dashboard và người dùng thôi mâu thuẫn nhau.
1. Vì sao trung bình cộng nói dối với độ trễ?
Độ trễ của một endpoint hiếm khi phân phối đều quanh một giá trị trung tâm kiểu hình chuông. Nó lệch phải rất mạnh: phần lớn request chạm cache, đi qua nhánh nhanh, mất vài chục mili-giây; một số ít request gặp cache miss, lock contention, GC pause, hay một downstream chậm — mất gấp chục lần con số đó. Trung bình cộng cộng hết rồi chia đều cho tất cả, nên nó bị vài giá trị cực đoan kéo lên, nhưng đồng thời cũng bị đám đông nhanh kéo xuống — kết quả là một con số nằm lấp lửng giữa hai nhóm, không đại diện đúng cho nhóm nào.
Thử với một mẫu nhỏ tự tính tay được. Một service ghi lại 10 độ trễ liên tiếp, đơn vị mili-giây:
45, 50, 48, 52, 47, 51, 49, 53, 46, 809
Tự cộng và chia trước khi đọc tiếp: trung bình cộng của 10 số trên là bao nhiêu? Còn giá trị đứng thứ 10 khi sắp xếp tăng dần — đúng bằng P99 của mẫu 10 phần tử này — là bao nhiêu?
Đáp án: tổng 10 giá trị là 1.250, chia 10 ra trung bình 125ms. Sắp xếp tăng dần: 45, 46, 47, 48, 49, 50, 51, 52, 53, 809 — chín giá trị đầu chụm quanh 45-53ms, giá trị thứ mười nhảy vọt lên 809ms. Trung vị (trung bình của phần tử thứ 5 và thứ 6) là 49,5ms — đại diện gần đúng cho request điển hình. P99 của đúng mẫu 10 phần tử này tính theo nearest-rank (⌈0,99 × 10⌉ = 10) rơi vào đúng giá trị lớn nhất: 809ms.
Chín người thấy trang tải dưới 55ms; người thứ mười thấy 809ms — chậm hơn 16 lần trung vị. Trung bình cộng 125ms không nói với bạn điều đó: nó đã gấp 2,5 lần trung vị 49,5ms, nhưng nhiều khả năng vẫn nằm dưới ngưỡng cảnh báo mà đội vận hành tự đặt, nên không ai bị đánh thức — cái nó giấu kỹ là một request tệ tới 809ms. Không ai trong mười người trải nghiệm đúng "125ms".
Với đúng 10 quan sát, P99 trùng luôn với giá trị lớn nhất — công thức nearest-rank làm tròn lên tới phần tử cuối. Với traffic thật (hàng nghìn request mỗi giây), P99 là một ngưỡng ổn định mà 99% request thật sự nằm dưới, không phải một request tệ nhất duy nhất — khác biệt chỉ hiện rõ khi cỡ mẫu đủ lớn.
2. Percentile là gì, và vì sao đuôi ảnh hưởng nhiều người hơn bạn tưởng
Percentile (bách phân vị) thứ P của một tập số đo là ngưỡng mà đúng P% giá trị trong tập nằm dưới hoặc bằng nó. P99 của độ trễ nghĩa là: 99% request nhanh hơn (hoặc bằng) con số đó, và 1% còn lại — đuôi chậm nhất — nằm trên ngưỡng. P50 (trung vị) là ngưỡng mà một nửa nhanh hơn, một nửa chậm hơn — đại diện cho request điển hình tốt hơn trung bình cộng vì nó không bị kéo bởi giá trị cực đoan.
Điểm hay bị bỏ qua: 1% nghe có vẻ nhỏ, nhưng phần lớn một trang không chỉ gọi một API. Giả sử trang gọi 10 API nội bộ độc lập, mỗi API có đúng 1% xác suất rơi vào đuôi P99 của chính nó. Xác suất trang không dính đuôi nào cả là 0,99 nhân mười lần: 0,99^10 ≈ 0,9044. Xác suất có ít nhất một cuộc gọi rơi vào đuôi P99 là 1 - 0,9044 = 0,0956 — xấp xỉ 9,6%, gần gấp mười lần trực giác "chỉ 1% thôi". Đây là lý do một trang tổng hợp nhiều service cảm giác chậm thường xuyên hơn nhiều so với con số P99 của từng service riêng lẻ gợi ý.
flowchart TB
P["Trang goi 10 API noi bo<br/>moi API: 1% xac suat cham vao duoi P99"]
A["Xac suat KHONG dinh duoi nao<br/>0.99^10 = 0.9044"]
B["Xac suat dinh it nhat 1 duoi<br/>1 - 0.9044 = 0.0956 (~9.6%)"]
P --> A
P --> B3. Percentile không cộng được — hai chế độ khác nhau trong Micrometer
Percentile có một tính chất toán học gây phiền: nó không cộng được. Nếu 5 instance mỗi cái tự tính P99 rồi bạn lấy trung bình cộng của 5 con số P99 đó, kết quả không phải P99 của toàn hệ thống — percentile không phải đại lượng tuyến tính, trung bình của nhiều percentile không có ý nghĩa thống kê. Micrometer giải quyết bằng hai chế độ hoàn toàn khác nhau, và nhầm giữa hai chế độ là nguồn sai số phổ biến nhất khi đọc dashboard đa instance.
| Chế độ | Property | Ai tính | Gộp được qua instance? |
|---|---|---|---|
| Client-side | management.metrics.distribution.percentiles.[meter-name]=0.95,0.99 | Từng instance tự tính rồi đẩy con số lên | Không |
| Histogram bucket | management.metrics.distribution.percentiles-histogram.[meter-name]=true | Instance chỉ đẩy đếm-theo-bucket, backend gộp rồi mới tính | Có |
# Che do 1: client-side percentiles - tung instance tu tinh
management.metrics.distribution.percentiles.http.server.requests=0.95,0.99
# Che do 2: histogram bucket - day bucket len, backend gop roi moi tinh
management.metrics.distribution.percentiles-histogram.http.server.requests=true
Ở chế độ 1, mỗi instance tự ước lượng P99 cục bộ rồi chỉ đẩy lên đúng một con số — nhẹ, nhưng chỉ đúng cho riêng instance sinh ra nó; Prometheus không có cách nào gộp nhiều "P99 đã tính sẵn" thành một P99 đúng cho toàn hệ thống.
Ở chế độ 2, mỗi instance không tự tính percentile — nó chỉ đếm request rơi vào từng khoảng giá trị (bucket), rồi đẩy bộ đếm lên dưới dạng metric mang nhãn le (less-or-equal, quy ước Prometheus histogram). Bộ đếm cộng dồn được — Prometheus gộp bộ đếm của mọi instance trước, rồi mới dùng histogram_quantile() để nội suy percentile từ tổng đã gộp. Tính percentile luôn xảy ra SAU bước gộp, nên kết quả là percentile thật của toàn hệ thống.
flowchart TB
subgraph M1["Che do percentiles (client-side)"]
C1["Instance 1 tu tinh P99 = 120ms"] --> CX["Trung binh 3 P99 = 170ms<br/>KHONG PHAI P99 he thong"]
C2["Instance 2 tu tinh P99 = 300ms"] --> CX
C3["Instance 3 tu tinh P99 = 90ms"] --> CX
end
M1 -. "khac han" .-> M2
subgraph M2["Che do percentiles-histogram"]
H1["Instance 1: dem bucket"] --> SUM["Prometheus gop bucket ca 3 instance"]
H2["Instance 2: dem bucket"] --> SUM
H3["Instance 3: dem bucket"] --> SUM
SUM --> Q["histogram_quantile 0.99 = P99 dung cua he thong"]
end4. Chi phí: histogram sinh thêm time series — nối lại cardinality
Chế độ histogram gộp được, nhưng không miễn phí. Mỗi bucket boundary tự nó là một time series riêng — nhãn le cộng thêm vào mọi tổ hợp tag đã có trên Timer đó. Cardinality của một metric là tích số giá trị của mọi tag (bài trước); bật percentiles-histogram nhân thêm một chiều cardinality mới — số bucket — lên trên tích đó. Timer nào đã cardinality cao vì tag uri/outcome mà bật thêm histogram thì cardinality nhân lên gấp nhiều lần nữa. Muốn biết đúng bao nhiêu bucket, cứ gọi /actuator/prometheus rồi đếm dòng cùng tên metric khác giá trị le= — con số phụ thuộc range mặc định của Micrometer cho loại meter đó.
Cách rẻ hơn: thay vì bật full histogram (bucket phủ toàn range), khai đúng ngưỡng bạn quan tâm bằng slo. Micrometer chỉ tạo bucket tại đúng các ngưỡng đó — ít time series hơn nhiều mà vẫn cộng dồn được, vì bản chất vẫn là bộ đếm cumulative. Giả sử team đã chốt SLO 99% request dưới 300ms (giải thích kỹ ở mục 5) — tự điền danh sách ngưỡng trước khi xem đáp án:
# TODO: dien danh sach nguong (vd 100ms,300ms,500ms,1s)
# it nhat mot nguong phai bam sat SLO 300ms da chot
management.metrics.distribution.slo.http.server.requests=
Chọn danh sách ngưỡng cho slo: một mốc bám sát đúng ngưỡng SLO 300ms, cộng thêm vài mốc thấp hơn và cao hơn để thấy phân bố quanh nó — vì sao khớp đúng ngưỡng SLO lại quan trọng hơn phủ đều toàn range?
Đáp án:
# Chi tao bucket dung tai cac nguong quan tam - re hon full histogram, van gop duoc
management.metrics.distribution.slo.http.server.requests=100ms,300ms,500ms,1s
Bucket 300ms khớp thẳng ngưỡng SLO: Prometheus đếm ngay được số request nằm dưới nó (le="300") mà không cần nội suy histogram_quantile(). Các mốc 100ms/500ms/1s cho thấy phân bố quanh ngưỡng — bao nhiêu request nhanh hơn nhiều, bao nhiêu chậm hơn nhiều so với cam kết.
5. Từ số đo tới SLO: ngưỡng, cửa sổ thời gian, và error budget
SLO (Service Level Objective — mục tiêu mức dịch vụ) là cam kết đo được, gồm đúng ba phần: một metric (P99 độ trễ), một ngưỡng (300ms), một cửa sổ thời gian (30 ngày). Viết đầy đủ: "99% request dưới 300ms trong 30 ngày" — thiếu cửa sổ thời gian thì SLO vô nghĩa, vì 99% trong 1 giờ cao điểm nghiêm ngặt hơn hẳn 99% gộp cả tháng.
1% còn lại là error budget (ngân sách lỗi): số request được phép chậm hơn ngưỡng, tính ra con số cụ thể. Ví dụ tự tính: 2.000.000 request/30 ngày, SLO 99% dưới 300ms → error budget = 1% × 2.000.000 = 20.000 request được phép chậm hơn 300ms cả tháng. Còn dư ngân sách, đội release nhanh hơn; cạn ngân sách trước hạn — ví dụ hết 20.000 ngay ngày thứ 20 — là tín hiệu dừng: ưu tiên fix độ tin cậy, hoãn tính năng mới thay vì đổ thêm rủi ro vào hệ thống đã vượt cam kết.
Pitfall thường gặp
❌ Nhầm 1 — đo P99 gộp cả request lỗi trả về nhanh: request lỗi (validation fail, 4xx) thường trả lời gần như ngay lập tức, nhanh hơn cả request thành công. Gộp chung vào phân phối latency kéo percentile xuống — dashboard đẹp giả trong khi tỷ lệ lỗi thật sự đang tăng.
✅ Tách latency theo outcome/status (bài trước) — chỉ tính P99 trên request thành công, đo tỷ lệ lỗi bằng metric riêng. "Nhanh hay chậm" và "thành công hay thất bại" là hai câu hỏi khác nhau, cần hai con số riêng.
❌ Nhầm 2 — đo P99 chỉ ở tầng server, coi đó là trải nghiệm người dùng: http.server.requests chỉ đo từ lúc request chạm server tới lúc trả response, không thấy DNS lookup, TLS handshake, hay thời gian trên mạng — phần có thể chiếm phần lớn thời gian người dùng thật sự chờ, nhất là với mạng yếu.
✅ P99 server-side đủ để chẩn đoán sự cố backend, nhưng không thay thế đo lường phía client (real user monitoring) khi câu hỏi là "người dùng cảm nhận app chậm ra sao". Ghi rõ dashboard đo latency ở tầng nào để tránh kết luận sai phạm vi sự cố.
Đào sâu
Spec / reference chính thức:
- Micrometer — Histograms and percentiles — chi tiết property
percentiles,percentiles-histogram,slovà vì sao percentile client-side không gộp được. - Prometheus — Histograms and summaries — cơ chế
histogram_quantile(), vì sao trung bình của nhiều quantile "statistically nonsensical", và nhãnletrên bucket counter. - Google SRE Workbook — Implementing SLOs — định nghĩa SLO/error budget và cách dùng error budget để quyết định gate release.
Ghi chú: Micrometer cho đúng property cần config; Prometheus giải thích vì sao histogram gộp được về mặt toán học; SRE Workbook cho cách áp error budget vào quy trình release thật.
Liên hệ các bài khác
- Bài 03 — Micrometer, metric và tag —
Timersinh ra các con số percentile mà bài này đọc lại; cardinality của tag học ở đó là lý do phải cân nhắcpercentiles-histogramso vớislo. - Bài 05 — Observation API — Observation API tự động sinh
Timercho một block code, cùng cơ chế percentile ở bài này áp dụng thẳng lên số liệu nó tạo ra. - Bài 06 — Tracing OTLP — khi P99 chỉ ra "có sự cố" nhưng không nói "sự cố ở đâu", trace theo request cụ thể (bài 06) là bước điều tra tiếp theo.
Tóm tắt
- Trung bình cộng bị kéo bởi đuôi phân phối nhưng không lộ mức độ tệ của đuôi — dùng mẫu 10 số để tự kiểm tra chênh lệch giữa trung bình, trung vị và P99.
- Muốn biết dashboard có đang giấu một sự cố hay không: so song song P50 với P99 trên cùng biểu đồ — hai đường càng xa nhau, đuôi phân phối càng phình.
- Nhiều đường P99 lệch nhau rõ giữa các pod trên cùng dashboard là dấu hiệu đang dùng nhầm
percentilesclient-side thay vìpercentiles-histogram. slotạo bucket đúng tại ngưỡng cần quan tâm — rẻ hơn full histogram mà vẫn gộp được qua nhiều instance.- Error budget là đồng hồ đếm ngược chạy suốt cả cửa sổ thời gian, không phải con số tổng kết đọc một lần cuối kỳ — cạn sớm là tín hiệu hành động ngay, không phải chờ báo cáo cuối tháng.
Tự kiểm tra
Q1Vì sao độ trễ trung bình có thể trông ổn ngay cả khi nhiều người dùng đang trải nghiệm chậm?▸
Q2Một trang gọi 10 API nội bộ độc lập, mỗi API có 1% xác suất rơi vào đuôi P99 riêng. Vì sao xác suất người dùng dính ít nhất một request chậm lại cao hơn nhiều so với cảm giác '1% thôi'?▸
Q3Vì sao management.metrics.distribution.percentiles không gộp được giữa nhiều instance, còn percentiles-histogram thì gộp được?▸
Q4Tại sao bật percentiles-histogram cho một Timer có tag cardinality cao (ví dụ tag uri hàng trăm giá trị) lại tốn kém hơn nhiều so với Timer cardinality thấp?▸
Q5SLO là '99% request dưới 300ms trong 30 ngày', hệ thống nhận 2.000.000 request trong cửa sổ đó. Error budget là bao nhiêu request? Nếu ngân sách đã dùng hết ở ngày thứ 20, đội nên làm gì?▸
Q6Nêu một pitfall khiến số đo P99 trông 'đẹp giả' dù hệ thống thật sự đang có vấn đề.▸
Bài tiếp theo: Observation API
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên