Suốt sê-ri ta đo hiệu năng bằng nhiều con số — ns/op, op/giây, tỉ lệ tăng tốc. Giờ lùi lại một bước để hỏi: đo hiệu năng thế nào cho đúng? Có hai chỉ số cơ bản mà người ta hay lẫn lộn hoặc dùng sai: throughput (làm được bao nhiêu việc mỗi giây) và latency (một việc mất bao lâu). Chúng khác nhau, đôi khi trái chiều, và — quan trọng nhất — latency không phải một con số mà là một phân phối. Tôi đo một workload có tranh chấp và thu latency của từng thao tác, và con số phơi bày hai sai lầm phổ biến về đo hiệu năng.

Throughput và latency: hai thứ, đo cái đuôi

Hai chỉ số, không thay thế nhau

Throughput = op/giây: dung lượng của hệ, bao nhiêu việc trôi qua mỗi đơn vị thời gian. Latency = thời gian một op: một request mất bao lâu từ lúc bắt đầu tới lúc xong. Chúng không là một: một hệ có throughput cao vẫn có thể có latency tệ (nhiều việc trôi qua, nhưng mỗi việc chờ lâu trong hàng đợi), và ngược lại. Ta đã thấy chúng trái chiềubatching: gom lô tăng throughput nhưng tăng latency (item chờ đầy lô).

Tôi đo trong container gcc:13: nhiều luồng cùng thao tác trên một biến chung được bảo vệ bởi một mutex, mỗi thao tác có một vùng tới hạn nhỏ. Với mỗi op tôi bấm giờ latency (thời gian từ lúc xin khóa tới lúc xong), gom toàn bộ vào mảng, sắp xếp, và tính các phân vị: trung vị (p50), phân vị 99 (p99 — 1% chậm nhất bắt đầu từ đây), p99,9, và max. Đo ở 1, 4, 8 luồng.

Đo (a): throughput giảm, latency tăng — cùng lúc

        throughput | mean lat | p50   | p99      | p99.9
N=1     14,0 M/s   |  54 ns   |  42   |  84      |  84
N=4      8,4 M/s   | 456 ns   | 125   | 6.875    | 13.375
N=8      5,5 M/s   | 1.420 ns | 125   | 32.292   | 78.917

Nhìn cột throughput: nó giảm khi thêm luồng (14 → 8,4 → 5,5 triệu op/giây). Vì tất cả tranh một mutex, thêm luồng chỉ tăng tranh chấp — đúng scale âm. Nhìn cột latency trung bình: nó tăng (54 → 456 → 1.420 ns). Cả hai chỉ số cùng xấu đi — thêm luồng ở đây vừa giảm throughput vừa tăng latency. Đây là bằng chứng trực tiếp rằng "nhiều luồng" không đồng nghĩa "nhanh hơn", và rằng throughput và latency phải nhìn cùng lúc — tối ưu một chỉ số mà không nhìn cái kia dễ dẫn tới kết luận sai. (Ngược lại, batching sẽ cho throughput tăng mà latency cũng tăng — trái chiều; ở đây tranh chấp cho cả hai cùng xấu.)

Đo (b): trung bình giấu cái đuôi

Đây là điểm quan trọng nhất, và là sai lầm phổ biến nhất trong đo latency. Nhìn hàng N=8 kỹ hơn:

p50 (trung vị) : 125 ns      <- một nửa số op nhanh cỡ này
mean (trung bình): 1.420 ns   <- gấp 11 lần trung vị
p99            : 32.292 ns    <- 1% chậm nhất: gấp 258 lần trung vị
p99,9          : 78.917 ns    <- 0,1% chậm nhất
max            : 336.417 ns

Trung vị chỉ 125 nsmột nửa số thao tác nhanh như vậy. Nhưng p99 = 32.292 ns — nghĩa là 1% số thao tác chậm gấp 258 lần trung vị. Và trung bình (1.420 ns) nằm đâu đó ở giữa, bị kéo lên bởi cái đuôi nhưng vẫn giấu mức độ tệ của nó (p99 gấp 23 lần chính mean). Nếu bạn chỉ báo cáo "latency trung bình 1,4 µs", bạn đang che giấu sự thật rằng cứ 100 request thì có 1 request mất 32 µs, và cứ 1000 thì có 1 mất 79 µs.

Vì sao điều này chết người: người dùng gặp cái đuôi, không gặp trung bình. Một trang web tải 100 tài nguyên; nếu p99 latency tệ, gần như mọi lần tải trang sẽ đụng ít nhất một tài nguyên chậm (1 - 0,99^100 ≈ 63%). Một dịch vụ gọi 10 microservice; p99 của mỗi cái dồn lại thành p99 tệ hơn nhiều của toàn request. Trung bình đẹp không cứu được bạn nếu đuôi xấu. Đây là lý do các hệ thống nghiêm túc đo và cam kết theo phân vị (p99, p99,9), không theo trung bình.

Một lần tôi đo hớ: mean nói dối về latency

Tôi vào đo với hai niềm tin phổ biến. Thứ nhất: "throughput cao nghĩa là hệ nhanh". Sai — throughput và latency là hai chiều khác nhau; một hệ có thể đẩy nhiều op/giây mà mỗi op vẫn chậm (xếp hàng), và như đo được, dưới tranh chấp cả hai cùng tệ. Thứ hai, nguy hiểm hơn: "đo latency bằng con số trung bình là đủ". Sai — latency là một phân phối lệch (skewed) với cái đuôi dài; trung bình bị đuôi kéo lệch đồng thời giấu mức độ của đuôi. Ở 8 luồng, mean là 1.420 ns nhưng p99 là 32.292 ns — nếu tôi chỉ nhìn mean, tôi đã bỏ lỡ hoàn toàn việc 1% request chậm gấp hàng chục lần.

Bài học đo lường, và là ý cốt của cả sê-ri áp cho chính việc đo: một con số đơn (trung bình) không mô tả nổi hiệu năng đồng thời — phải đo phân phối, và đặc biệt là cái đuôi. Trung bình phù hợp cho đại lượng đối xứng, nhưng latency dưới tải là bất đối xứng nặng: đa số nhanh, một thiểu số chậm khủng khiếp (do xếp hàng, tranh khóa, convoy, GC, chuyển ngữ cảnh). Chính cái thiểu số đó quyết định trải nghiệm. Đúng tinh thần đo lường vi mô đúng cách: đo đúng đại lượng quan trọng như đo chính xác — và với latency, đại lượng đúng là phân vị đuôi dưới tải thực, không phải trung bình lúc rảnh.

Vì sao điều này quan trọng khi lập trình

Hệ quả đầu tiên: đo và cam kết latency theo phân vị, không theo trung bình. Khi đánh giá hay đặt mục tiêu (SLO) cho một dịch vụ, dùng p99/p99,9 ("99% request dưới X ms"), không dùng "trung bình dưới X ms". Trung bình có thể đẹp trong khi đuôi thảm họa. Ghi lại phân phối (histogram), không chỉ một con số — nhiều thư viện đo (Prometheus histogram, HdrHistogram) sinh ra để làm đúng việc này.

Hệ quả thứ hai: nhìn throughput và latency cùng lúc, và biết bạn đang tối ưu cho cái nào. Một hệ thiên throughput (xử lý theo mẻ, ETL) chấp nhận latency cao để đẩy nhiều việc; một hệ thiên latency (giao dịch tương tác, game) hy sinh throughput để mỗi request nhanh. Batching, buffer lớn, hàng đợi sâu — tất cả tăng throughput mà tăng latency. Chọn có ý thức, và đo cả hai đường cong khi tăng tải.

Hệ quả thứ ba là tinh thần đo lường: đo đúng đại lượng — phân phối và đuôi, không chỉ trung bình. Con số mang theo: throughput (op/giây) và latency (thời gian 1 op) là HAI chỉ số khác nhau, có thể trái chiều (batch tăng throughput+latency; tranh chấp giảm throughput 14->5,5 M/s MÀ tăng latency mean 54->1420 ns). Và latency là PHÂN PHỐI, không phải một số: ở 8 luồng p50 chỉ 125 ns nhưng p99 = 32.292 ns (258x p50), p99,9 = 78.917 ns — trung bình (1.420) GIẤU cái đuôi. Người dùng gặp đuôi (p99), không gặp mean. Đo p99/p99,9 dưới TẢI, đừng chỉ đo mean và throughput.

Thử ba mươi giây

Nhìn cách bạn (hay nhóm bạn) đang báo cáo hiệu năng một dịch vụ hoặc hàm nóng, và hỏi: chúng ta đo latency bằng gì — trung bình, hay phân vị? Nếu chỉ có "latency trung bình", bạn đang mù trước cái đuôi: cứ đo thêm p99 và p99,9, gần như chắc chắn bạn sẽ thấy chúng tệ hơn trung bình nhiều lần, và đó mới là cái người dùng cảm nhận. Rồi hỏi câu thứ hai: khi tôi tăng tải (nhiều luồng, nhiều request), throughput và latency đổi thế nào? Vẽ cả hai đường: bạn sẽ thấy "đầu gối latency" — điểm mà latency đang phẳng bỗng vọt lên khi hệ gần bão hòa, dù throughput chỉ nhích thêm chút. Ba mươi giây thêm hai phân vị và một trục tải đó biến một bức tranh hiệu năng dối trá (trung bình đẹp) thành bức tranh thật — và nhắc rằng đo đúng đại lượng quan trọng ngang đo chính xác: với latency, đại lượng đúng là cái đuôi dưới tải.