Ai đó khoe "API của mình có latency trung bình 0.1 mili-giây" và mọi người gật gù. Nhưng con số đó nói dối về trải nghiệm thật của người dùng — không phải vì nó sai, mà vì nó là trung bình. Trung bình cộng dồn mọi request rồi chia đều, nên một nhúm request cực chậm bị "pha loãng" trong biển request nhanh. Người dùng thì không trải nghiệm cái trung bình đó — họ trải nghiệm từng request một, và người xui xẻo rơi vào 1% chậm nhất chính là người viết đánh giá một sao. Bài này (phần 11 loạt Debug) đo thật để thấy khoảng cách giữa mean và p99, và vì sao percentile mới là thứ đáng đo.
Vì sao mean giấu đuôi
Vấn đề của trung bình với dữ liệu latency: phân phối latency gần như luôn lệch phải (right-skewed). Đa số request nhanh, nhưng luôn có một cái đuôi dài các request chậm — do GC pause, lock contention, cache miss, một truy vấn DB xui, một lần chờ I/O. Trung bình bị cái đuôi này kéo lên một chút, nhưng không phản ánh được rằng cái đuôi tồn tại và tệ đến mức nào.
Percentile giữ lại thông tin đó. p99 = 6ms nghĩa là "99% request nhanh hơn 6ms, 1% còn lại chậm hơn". Nó nói thẳng về nhóm tệ nhất — chính nhóm hay phàn nàn. Các mốc thường dùng:
- p50 (trung vị): request "ở giữa" — một nửa nhanh hơn, một nửa chậm hơn. Đây mới là cái "điển hình", không phải mean.
- p95 / p99: ngưỡng mà 95% / 99% request nằm dưới — đo chất lượng đuôi.
- p99.9: 1 phần nghìn tệ nhất — quan trọng khi lưu lượng lớn (1 triệu request/ngày thì p99.9 là 1000 người mỗi ngày).

Hình 1: Mean cộng dồn rồi chia đều nên đuôi chậm bị pha loãng; đo latency thật của từng thao tác bằng time.Since, cộng mean rồi sort mảng để lấy percentile bằng nearest-rank (p99 = phần tử thứ ~99000 trên 100.000 mẫu đã sort).
Đo thật: 100.000 thao tác, phân phối lệch
Mình viết một chương trình Go chạy 100.000 thao tác. Mỗi thao tác làm một chút việc số học (nhanh, cỡ micro-giây); nhưng 2% trong số đó rơi vào "đuôi chậm" — chèn một time.Sleep(2-8ms) mô phỏng một GC pause hay một lần chờ I/O thật. Rồi đo latency thật của từng cái bằng time.Since, sort, và lấy percentile:

Hình 2: Chạy thật — mean=0.133ms nhưng p50=0.003ms; p50/p90/p95 đều 0.003ms (95% request cực nhanh), rồi p99=6.121ms bùng lên; p99/mean=46.1x, p99/p50=2128.9x, và 97950/100000 (98%) request nhanh hơn mean.
- p50 tới p95 phẳng lì ở 0.003ms: 95% request nhanh như nhau (~3 micro-giây). Đây là phần "khỏe mạnh".
- p99 nổ lên 6.121ms: chỉ ở mốc 99% cái đuôi mới lộ ra — gấp ~2000 lần p50. Đây chính là 2% thao tác chậm mình cố tình tạo, và chúng dồn hết vào 1-2% cuối của phân phối.
- mean = 0.133ms là con số vô nghĩa để mô tả trải nghiệm: nó cao gấp 44 lần p50 (vì bị đuôi kéo lên) nhưng vẫn thấp gấp 46 lần p99 (vì đuôi bị pha loãng). Nó không mô tả ai cả — không phải người nhanh (p50), cũng không phải người chậm (p99).
- Cú chốt: 98% request nhanh hơn mean. Chương trình đếm thật:
97950/100000request có latency thấp hơn con số trung bình. Nói cách khác, "trung bình" ở đây còn chậm hơn trải nghiệm của gần như mọi người dùng. Trung bình không phải "điển hình" khi phân phối lệch.
Vì sao điều này định hình cách đặt SLO
Đây là lý do các SLO (Service Level Objective) nghiêm túc luôn viết theo percentile: "p99 latency < 200ms", không bao giờ "latency trung bình < 200ms". Một hệ thống có thể đạt "mean 50ms" trong khi p99 là 2 giây — nghĩa là cứ 100 khách thì 1 người chờ 2 giây, đủ để họ bỏ đi. Mean giấu chuyện đó; p99 phơi bày nó. Khi bạn tối ưu, bạn phải biết mình đang tối ưu cho ai: kéo p50 từ 3ms xuống 2ms làm hài lòng đám đông đã hài lòng; kéo p99 từ 6ms xuống 3ms cứu 1% đang khổ.
Đánh đổi cần cân nhắc
Percentile không cộng/trung bình được qua các máy chủ. Một sai lầm phổ biến: lấy p99 của 10 server rồi tính trung bình 10 con số đó — kết quả sai. Percentile của tập hợp không bằng trung bình các percentile con. Muốn p99 toàn cục đúng, phải gộp toàn bộ mẫu (hoặc dùng cấu trúc gộp được như HDR histogram / t-digest) rồi mới tính percentile trên tập gộp. Đây là lý do các hệ đo lường (Prometheus, v.v.) dùng histogram bucket thay vì lưu p99 rời rạc.
Sort toàn bộ mẫu tốn bộ nhớ; production dùng histogram. Bài này sort cả 100.000 mẫu — đơn giản và chính xác, tốt cho phân tích offline. Nhưng một service xử lý hàng triệu request/giây không thể giữ mọi mẫu trong RAM. Giải pháp thực chiến là histogram (đếm số mẫu rơi vào mỗi khoảng giá trị) — tốn bộ nhớ cố định, cho percentile xấp xỉ nhưng đủ tốt, và gộp được qua nhiều máy. Đổi độ chính xác tuyệt đối lấy khả năng đo liên tục ở quy mô lớn.
Coordinated omission làm p99 đo được đẹp hơn thực tế. Cạm bẫy tinh vi: nếu công cụ đo gửi request tuần tự và chờ request chậm xong mới gửi cái tiếp theo, nó vô tình bỏ sót những request lẽ ra đã bị xếp hàng trong lúc hệ thống đơ — làm p99 trông tốt hơn thực tế người dùng chịu. Các công cụ tốt (wrk2, và HDR histogram của Gil Tene) hiệu chỉnh điều này bằng cách đo theo lịch gửi cố định, không theo thời điểm phản hồi.
Ba ý mang về
- Mean giấu đuôi, percentile phơi bày nó: đo thật 100.000 thao tác cho
mean=0.133msnhưngp99=6.121ms— mean thấp hơn p99 tới 46 lần, và 98% request nhanh hơn cả mean. Trung bình không mô tả trải nghiệm của bất kỳ ai khi phân phối lệch. - p50/p95/p99 nói những chuyện khác nhau: đo thật p50=p95=
0.003ms(đám đông khỏe mạnh) nhưng p99=6.121ms(1% khổ sở, gấp ~2000 lần p50). Tính đơn giản bằngsortmảng latency rồi lấy phần tử theo thứ hạng (nearest-rank). - SLO và tối ưu phải dựa trên percentile, không phải mean: đặt mục tiêu kiểu "p99 < 200ms"; nhớ percentile không trung bình được qua nhiều máy (gộp mẫu hoặc dùng HDR histogram), và cảnh giác coordinated omission làm p99 đo được đẹp hơn thực tế.
Nguồn
- Gil Tene — How NOT to Measure Latency (coordinated omission): https://www.youtube.com/watch?v=lJ8ydIuPFeU
- Brendan Gregg — Linux Performance: https://www.brendangregg.com/linuxperf.html
- HdrHistogram — A High Dynamic Range Histogram: http://hdrhistogram.org/
Phần sau — bài cuối loạt Debug — ta đào tới tận hàm nóng: dùng profiling để tìm chính xác dòng code nào đang ngốn CPU, thay vì đoán mò.