Bốn tín hiệu vàng — thông lượng, độ trễ, lỗi, độ bão hoà — là danh sách được nhắc nhiều nhất trong giám sát, và cũng là danh sách hiếm khi được đo cạnh nhau trên cùng một sự cố. Bài này dựng một dịch vụ HTTP thật có tám worker, quét tải từ 1 tới 64 client đồng thời, và đo cả bốn ở mỗi mức. Thứ tự chúng phản ứng mới là điều đáng nhớ.

Bốn tín hiệu vàng trên một dịch vụ thật

Bảng số liệu

Dịch vụ HTTP viết bằng Python, giới hạn 8 worker, mỗi request tốn 20 ms chờ (mô phỏng một lời gọi cơ sở dữ liệu). Client đóng vòng, hết hạn sau 500 ms. Trung vị của ba lần chạy.

Client Traffic p50 p99 Errors Saturation Hàng đợi
1 38 req/s 26,9 ms 31,3 ms 0,0% 12% 1
2 76 req/s 26,9 ms 31,2 ms 0,0% 25% 1
4 156 req/s 25,7 ms 31,0 ms 0,0% 50% 1
8 325 req/s 24,0 ms 31,0 ms 0,0% 100% 1
12 347 req/s 29,6 ms 54,6 ms 0,0% 100% 5
16 350 req/s 45,1 ms 89,9 ms 0,0% 100% 9
32 349 req/s 90,9 ms 230,6 ms 0,0% 100% 24
64 352 req/s 179,7 ms 484,1 ms 0,7% 100% 60

Điều đáng nhớ

Nói lại theo cách khác, vì thứ tự mới là thứ dùng được:

Saturation chạm trần ở C = 8, khi ba tín hiệu kia còn đẹp hoàn hảo. Độ trễ p50 là 24,0 ms — thấp hơn cả lúc rảnh. p99 là 31,0 ms, y hệt mức nền. Lỗi bằng 0. Nếu bảng điều khiển của bạn chỉ có độ trễ và lỗi, dịch vụ này trông khoẻ mạnh đúng vào lúc nó vừa hết công suất.

Traffic một mình đánh lừa người đọc. Ở C = 8 thông lượng là 325 req/s; ở C = 64 nó là 352 req/s. Hai con số gần như giống nhau, nhưng một bên phục vụ 8 client với hàng đợi rỗng, còn bên kia có 60 request đang xếp hàng và p99 gần nửa giây. Thông lượng phẳng có thể là "vừa đủ" và cũng có thể là "đã hết".

Từ C = 8 lên C = 64, client tăng 8 lần thì thông lượng chỉ tăng 8%, còn p99 tăng 15,6 lần. Toàn bộ tải thêm vào biến thành thời gian xếp hàng, không thành công việc.

Errors là tín hiệu đến cuối cùng. Nó chỉ nhúc nhích ở C = 64, và chỉ 0,7%. Lúc đó người dùng đã chịu p99 gần 500 ms từ rất lâu. Cảnh báo dựa trên tỷ lệ lỗi là cảnh báo dựa trên thứ xảy ra sau cùng.

Vì sao

Dịch vụ có tám worker và mỗi request giữ một worker trong 20 ms, nên công suất trần là 8 / 0,020 = 400 req/s. Đo được 350 req/s — phần chênh là chi phí của Python và tầng mạng.

Dưới trần, thêm client là thêm thông lượng: mỗi client mới lấp một worker đang rảnh, và độ trễ không đổi vì không ai phải chờ. Đó là đoạn C = 1 tới 8, nơi thông lượng tăng gần như tuyến tính từ 38 lên 325 req/s.

Từ C = 8 trở đi không còn worker rảnh. Client thứ chín phải xếp hàng, và định luật Little mô tả chính xác chuyện xảy ra sau đó: thời gian chờ bằng số khách chia cho tốc độ phục vụ. Kiểm lại trên số đo:

Dự đoán C / thông lượng Đo được Lệch
C = 16 45,7 ms 45,1 ms 1,3%
C = 32 91,7 ms 90,9 ms 0,9%
C = 64 181,8 ms 179,7 ms 1,2%

Khớp trong khoảng 1,3%. Đó là lý do độ trễ tăng tuyến tính theo số client sau điểm bão hoà, và cũng là lý do saturation biết trước: nó đo cái đang cạn, còn độ trễ đo hậu quả của việc đã cạn.

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

  • Đo saturation cho tài nguyên thật sự khan hiếm của bạn, không phải cho CPU theo mặc định. Ở đây tài nguyên là worker, không phải CPU — dịch vụ chờ I/O nên CPU gần như rảnh suốt. Với ứng dụng khác thì đó có thể là kết nối cơ sở dữ liệu, kích thước hàng đợi luồng, hay số file descriptor.
  • Cảnh báo trên saturation với ngưỡng dưới 100%. Ở đây trần là 8; một ngưỡng "trên 6 worker bận trong 5 phút" cho bạn thời gian phản ứng trước khi độ trễ bắt đầu leo. Đợi tới 100% là đợi tới lúc hàng đợi đã bắt đầu dài ra.
  • Đừng bao giờ đọc thông lượng một mình. Luôn ghép nó với saturation hoặc độ dài hàng đợi. Cặp "thông lượng phẳng + saturation 100%" là chữ ký của bão hoà; cặp "thông lượng phẳng + saturation 40%" nghĩa là tải thật sự đang thấp.
  • Nếu chỉ đo được một tín hiệu, hãy đo saturation. Nếu đo được hai, thêm p99. Errors gần như luôn là tín hiệu thừa cho mục đích phát hiện sớm — nó hữu ích để biết mức độ thiệt hại, không phải để biết có chuyện.

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

Đây là mô hình đóng vòng, không phải lưu lượng thật. Client của tôi gửi request tiếp theo ngay khi nhận được câu trả lời, nên số client đồng thời là hằng số và hệ thống tự điều tiết. Lưu lượng thật là mở vòng: người dùng đến theo tốc độ riêng bất kể dịch vụ có kịp hay không, và khi đó hàng đợi có thể phình vô hạn thay vì dừng ở 60 như bảng trên. Hình dạng đường cong sẽ dốc hơn, không thoải như ở đây.

Ngưỡng hết hạn 500 ms của tôi quyết định cột Errors. Với ngưỡng 200 ms, lỗi sẽ xuất hiện ngay từ C = 32; với ngưỡng 2 giây, cột đó sẽ toàn số 0. Thứ tự "errors đến cuối cùng" là đúng về bản chất, nhưng khoảng cách giữa nó và ba tín hiệu kia là do tôi chọn.

Một phép đo phải vứt đi hoàn toàn, và cách nó lộ ra. Lần quét đầu tiên cho p50 = 70,9 ms ở C = 1, trong khi mỗi request chỉ có 20 ms công việc. Thừa 50 ms, và nó không chiếm worker — bằng chứng là thông lượng tối đa vẫn đạt 366 req/s chứ không tụt xuống 114 req/s như khi 70 ms kia nằm trong worker.

Nguyên nhân là thuật toán Nagle gặp delayed ACK. Server gửi header rồi gửi body thành hai đoạn; Nagle giữ đoạn thứ hai lại chờ ACK của đoạn đầu, còn client thì trì hoãn ACK theo mặc định của Linux. Hai cơ chế tối ưu gặp nhau thành một khoảng chờ chết.

Cách kiểm rất dứt khoát: tắt Nagle ở phía server thì p50 rơi từ 70,9 xuống 26,9 ms. Đặt TCP_NODELAY ở phía client thì không đổi gì (71,3 ms) — vì client không phải bên gửi hai đoạn.

Điều đáng suy nghĩ: bốn tín hiệu vàng không hề bắt được lỗi này. Độ trễ 70 ms trông hoàn toàn bình thường cho một dịch vụ web; thông lượng, lỗi và độ bão hoà đều đẹp. Nó chỉ lộ ra vì tôi biết trước công việc thật là 20 ms và thấy con số không khớp. Bốn tín hiệu vàng phát hiện thay đổi; chúng không phát hiện được một khoản phí cố định đã nằm đó từ ngày đầu.

Thử ba mươi giây

docker run --rm python:3.12-slim python - <<'EOF'
# Dinh luat Little tren so lieu do duoc: W = C / thong luong
do_duoc = [(8,325,24.0), (12,347,29.6), (16,350,45.1), (32,349,90.9), (64,352,179.7)]
print("%4s %10s %14s %12s %8s" % ("C","tps","du doan","do duoc","lech"))
for C, tps, p50 in do_duoc:
    du = 1000*C/tps
    print("%4d %10d %11.1f ms %9.1f ms %7.1f%%" % (C, tps, du, p50, 100*(p50-du)/du))
EOF

Cột cuối cho biết dịch vụ của bạn có đang hành xử như một hàng đợi hay không. Nếu độ trễ đo được khớp với số client / thông lượng, bạn đang bão hoà và mọi tối ưu vi mô đều vô ích — thứ duy nhất giúp được là thêm worker hoặc làm mỗi request nhanh hơn.