Phần trước đo khoảng cách giữa span db.query ở ứng dụng và con số PostgreSQL tự báo, và thấy 88% thời gian nằm ngoài cơ sở dữ liệu. Bài này làm đúng phép đo đó ở tầng ngoài cùng: máy chủ web tự báo bao nhiêu, người dùng thật chờ bao nhiêu, và khoảng cách giữa hai con số lớn tới đâu khi có độ trễ mạng.
Bảng số liệu
Một máy chủ HTTP tự đo thời gian xử lý của chính nó và báo qua header, cộng một client đo tổng thời gian. Độ trễ mạng tạo bằng tc netem, mỗi con số là trung vị của 25–60 lượt.
| Hoàn cảnh | Máy chủ tự báo | Client, giữ kết nối | Client, kết nối mới | Máy chủ mù |
|---|---|---|---|---|
| 1 KB, mạng không trễ | 0,115 ms | 0,206 ms | 0,409 ms | 71,9% |
| 100 KB, mạng không trễ | 0,117 ms | 0,219 ms | 0,418 ms | 72,1% |
| 1 KB, trễ 50 ms khứ hồi | 0,661 ms | 33,103 ms | 66,237 ms | 99,0% |
| 100 KB, trễ 50 ms khứ hồi | 0,673 ms | 33,135 ms | 151,255 ms | 99,6% |
Điều đáng nhớ
Với độ trễ mạng thật, máy chủ mù 99,6%. Nó báo 0,673 ms trong khi người dùng chờ 151,255 ms — gấp 225 lần. Mọi bảng điều khiển dựng trên thời gian xử lý phía máy chủ sẽ xanh mượt suốt thời gian một trang mất một phần bảy giây để tải.
Ngay cả khi mạng không trễ, máy chủ vẫn chỉ thấy 28% thời gian thật: 0,115 so với 0,409 ms. Khoảng cách đó không đến từ mạng chậm mà từ bắt tay TCP và việc truyền byte — những thứ luôn có.
Kích thước phản hồi chỉ lộ ra khi mạng có độ trễ. Trên mạng nhanh, 1 KB và 100 KB gần như bằng nhau: 0,409 so với 0,418 ms, chênh 2,2%. Thêm 50 ms độ trễ vào thì cùng hai con số đó thành 66,2 và 151,3 ms — chênh 128%.
Đó là lý do việc thử nghiệm trên máy của lập trình viên không bao giờ phát hiện được phản hồi quá lớn. Trên máy đó, mọi kích thước đều nhanh như nhau.
Và giữ kết nối sẵn tiết kiệm đúng 33,134 ms — chính xác một vòng khứ hồi. Đó là toàn bộ giá trị của keep-alive, đo được thành một con số.
Vì sao
Máy chủ đếm từ lúc nó đọc xong dòng đầu của yêu cầu tới lúc nó ghi xong byte cuối vào socket. Mọi thứ trước và sau khoảng đó nằm ngoài tầm nhìn của nó: phân giải DNS, bắt tay TCP, bắt tay TLS, gói tin đi trên đường, và bộ đệm gửi của hệ điều hành xả dần ra mạng.
Với độ trễ 50 ms khứ hồi, một yêu cầu HTTP đơn giản cần hai vòng: một cho bắt tay TCP, một cho yêu cầu và phản hồi. Đo được 66,237 ms — khớp với hai vòng cộng chi phí xử lý.
Chuyện 100 KB đắt gấp đôi 1 KB thì đến từ TCP khởi động chậm. Kết nối mới không gửi ngay toàn bộ dữ liệu; nó bắt đầu với một cửa sổ nhỏ, khoảng mười gói tin, rồi tăng dần sau mỗi vòng xác nhận. 1 KB nằm gọn trong cửa sổ đầu tiên nên đi trong một vòng. 100 KB cần vài vòng nữa, và mỗi vòng tốn 50 ms.
Trên mạng không trễ, mỗi vòng gần như miễn phí nên số vòng không quan trọng. Đó chính là điều làm cái bẫy này nguy hiểm: nó chỉ xuất hiện ở nơi bạn không đo.
Nghĩa là gì trong thực tế
- Đừng đặt SLO dựa trên thời gian xử lý phía máy chủ. Nó trả lời câu "mã của tôi chạy hết bao lâu", không trả lời câu "người dùng chờ bao lâu". Phần trước của sê-ri đã đo hậu quả của việc lẫn lộn hai câu hỏi.
- Nếu chưa có đo từ trình duyệt, hãy dùng kiểm tra tổng hợp từ nhiều vùng địa lý làm thay tạm. Nó không cho bạn trải nghiệm thật của người dùng, nhưng nó thấy được mạng — thứ mà máy chủ hoàn toàn mù.
- Giảm kích thước phản hồi là tối ưu bị đánh giá thấp nhất. Nó không hiện ra ở bất kỳ phép đo nào trên máy phát triển, nhưng ở người dùng thật thì nó là 85 mili giây.
- Đo với độ trễ nhân tạo trước khi phát hành. Một dòng
tc qdisc add dev eth0 root netem delay 25msbiến máy phát triển thành thứ gần với thực tế hơn nhiều, và nó bắt được đúng loại lỗi mà không phép thử nào khác bắt được. - Kiểm tra keep-alive thật sự đang bật. 33 ms mỗi lời gọi là con số lớn, và nó biến mất im lặng nếu proxy hay cân bằng tải đóng kết nối sau mỗi yêu cầu.
Có một cách nhìn khác làm rõ vì sao khoảng cách này khó chịu đến vậy. Ba con số trong bảng không phải ba mức chính xác khác nhau của cùng một đại lượng — chúng là ba đại lượng khác nhau. Máy chủ trả lời "mã của tôi chạy hết bao lâu". Client giữ kết nối trả lời "lần gọi thứ hai trở đi mất bao lâu". Client mở kết nối mới trả lời "người dùng mở trang lần đầu mất bao lâu".
Cả ba đều là câu hỏi hợp lệ, và mỗi đội thường chỉ có sẵn một trong ba. Đội hạ tầng nhìn con số thứ nhất, đội API nhìn con số thứ hai, còn người dùng sống với con số thứ ba. Khi có người báo "trang chậm" và biểu đồ ghi 0,7 ms, không ai nói dối cả — họ chỉ đang đọc ba câu trả lời cho ba câu hỏi khác nhau và tưởng là một.
Chỗ tôi không kết luận được
Tôi không đo TLS. Bắt tay TLS thêm một tới hai vòng khứ hồi nữa tuỳ phiên bản và tuỳ có nối lại phiên hay không. Với độ trễ 50 ms, đó là thêm 50–100 ms cho lần kết nối đầu tiên. Con số 151 ms của tôi là cận dưới cho một trang HTTPS thật.
Tôi cũng không đo DNS, chuyển hướng, hay việc tải các tài nguyên phụ. Một trang web thật tải hàng chục tệp, nhiều tệp trong số đó từ tên miền khác. Bài này chỉ đo một yêu cầu duy nhất.
Độ trễ 50 ms là tôi đặt vào, và tôi đặt nó ở một chiều. tc netem delay 25ms ở phía client làm gói đi ra trễ 25 ms; tôi gọi kết quả là "50 ms khứ hồi" vì mỗi vòng đi và về chịu độ trễ đó, nhưng nó không hoàn toàn giống một đường truyền thật có độ trễ đối xứng và có mất gói. Đây là mô hình đơn giản hoá.
Con số "máy chủ mù 99,6%" phụ thuộc vào việc máy chủ của tôi xử lý rất nhanh — 0,673 ms. Một dịch vụ tốn 200 ms xử lý sẽ có tỷ lệ mù thấp hơn nhiều. Máy chủ càng nhanh thì nó càng mù, và đó là một điều trớ trêu đáng nhớ: tối ưu mã tới mức xuất sắc chỉ làm phần bạn nhìn thấy nhỏ đi, không làm phần người dùng chờ nhỏ đi.
Một chi tiết tôi kiểm lại vì nó trông sai. Ở mạng không trễ, 100 KB chỉ chậm hơn 1 KB có 2,2% — tôi ngờ rằng thân phản hồi không thực sự được gửi. Kiểm bằng cách in kích thước dữ liệu client nhận được: đúng 100,0 KB. Vậy con số đúng, và lời giải thích là băng thông mạng ảo trong cùng một máy đủ lớn để 100 KB đi trong thời gian không đáng kể.
Thử ba mươi giây
# Bien may cua ban thanh may cua nguoi dung o xa, trong 30 giay
docker run --rm --cap-add NET_ADMIN python:3.12-slim sh -c '
apt-get -qq update >/dev/null && apt-get -qq install -y iproute2 curl >/dev/null
D=https://example.com
echo "khong do tre:"
curl -s -o /dev/null -w " tong %{time_total}s | ket noi %{time_connect}s | TLS %{time_appconnect}s\n" $D
tc qdisc add dev eth0 root netem delay 50ms
echo "them 50 ms moi chieu:"
curl -s -o /dev/null -w " tong %{time_total}s | ket noi %{time_connect}s | TLS %{time_appconnect}s\n" $D
'
So hai dòng. Phần tăng thêm không nằm ở máy chủ — máy chủ làm đúng một lượng việc như nhau ở cả hai lần. Đó là phần mà mọi biểu đồ phía máy chủ của bạn không bao giờ vẽ.