"Mạng nhà tôi 100 Mbps mà web vẫn tải chậm" — câu than quen thuộc, và nó chạm đúng một hiểu lầm phổ biến: nghĩ rằng tốc độ tải trang tỉ lệ thuận với con số băng thông nhà mạng quảng cáo. Thực ra tải một trang web bị giới hạn bởi hai thứ rất khác nhau — độ trễ (một gói đi và về mất bao lâu) và băng thông (mỗi giây đẩy được bao nhiêu byte) — và cái nào là nút thắt lại tùy vào việc bạn đang tải kiểu gì. Bài này dựng một đường mạng giả lập trong container, đổi từng biến một, và đo trực tiếp cái nào giới hạn — rồi vấp đúng cái bẫy của một câu châm ngôn nghe rất kêu.
Hai thứ hay bị lẫn
Độ trễ (latency, đo bằng RTT — round-trip time) là thời gian một gói tin đi từ bạn tới máy chủ rồi quay về, tính bằng mili giây. Nó do khoảng cách vật lý và số chặng trên đường quyết định, và gần như không cải thiện được bằng tiền — ánh sáng trong cáp quang có tốc độ hữu hạn.
Băng thông (bandwidth, đo bằng mbit/giây) là lượng dữ liệu tối đa đường truyền đẩy được mỗi giây. Đây mới là con số nhà mạng bán cho bạn, và là thứ tăng được bằng cách trả thêm tiền.
Chúng độc lập nhau. Một đường vệ tinh có thể có băng thông lớn (đẩy nhiều byte/giây) nhưng độ trễ khủng khiếp (600 ms mỗi vòng vì tín hiệu phải lên quỹ đạo và về). Một đường cáp ngắn có thể độ trễ tí xíu nhưng băng thông hẹp. Câu hỏi thật là: khi tải một trang, thời gian bạn chờ bị cái nào trong hai thứ này quyết định?
Đo: cái nào giới hạn tùy dạng tải
Tôi dùng tc netem để điều khiển đường mạng trong container: delay đặt RTT, rate đặt băng thông. Rồi đổi từng biến một, giữ biến kia cố định, và đo thời gian tải trên hai dạng tải khác nhau — một trang web điển hình (40 tài nguyên nhỏ, tổng 120 KB, lấy qua tối đa 6 kết nối song song như trình duyệt thật), và một file lớn 5 MB tải một mạch.
Trang 40 tài nguyên nhỏ cho kết quả này:
| Giữ RTT=50ms, đổi băng thông | Thời gian | Giữ BW=10mbit, đổi RTT | Thời gian |
|---|---|---|---|
| 2 mbit | 697 ms | 10 ms | 149 ms |
| 10 mbit | 521 ms | 100 ms | 915 ms |
| 50 mbit | 538 ms | 200 ms | 1749 ms |
Cột trái chững lại: qua 10 mbit là thời gian tải gần như không giảm nữa (521 rồi 538 ms — chênh lệch nằm trong nhiễu). Cột phải thì tăng tuyến tính theo RTT: RTT gấp đôi, thời gian gần gấp đôi. Với dạng tải này, băng thông sau một ngưỡng thấp là vô nghĩa, còn độ trễ quyết định tất cả.
Một file lớn 5 MB cho kết quả ngược hẳn:
| Giữ RTT=50ms, đổi băng thông | Thời gian | Giữ BW=25mbit, đổi RTT | Thời gian |
|---|---|---|---|
| 5 mbit | 9035 ms | 10 ms | 1811 ms |
| 10 mbit | 4624 ms | 100 ms | 2399 ms |
| 50 mbit | 1207 ms | 200 ms | 3165 ms |
Ở đây gấp đôi băng thông là gần nửa thời gian (5→10 mbit: 9035 xuống 4624), đúng kiểu bị băng thông giới hạn. Còn RTT gấp 20 lần (10→200 ms) chỉ làm thời gian tăng chưa tới hai lần. Với một file lớn, băng thông mới là nút thắt.
Một lần tôi đo hớ: "độ trễ luôn thắng" chỉ đúng một nửa
Tôi vào bài này với một mục tiêu định sẵn: chứng minh câu châm ngôn nổi tiếng trong giới web — "với tải trang, độ trễ quan trọng hơn băng thông; thêm mbit gần như không làm trang nhanh hơn". Phép đo trang xác nhận nó đẹp đến mức tôi đã gõ xong câu kết: băng thông chững ở 10 mbit, độ trễ kéo thời gian tuyến tính — vậy "băng thông không quan trọng, chỉ độ trễ" là luật, chấm hết.
May là tôi đo thêm dạng tải thứ hai trước khi chốt. File lớn 5 MB bác bỏ câu luật đó ngay lập tức: ở đó gấp đôi băng thông cắt gần nửa thời gian, còn độ trễ gần như không đụng tới. Cùng một đường mạng, cùng hai biến — chỉ khác hình dạng dữ liệu — mà kết luận lật ngược 180 độ.
Vấn đề của tôi là đã suýt biến một câu đúng-trong-ngữ-cảnh thành một luật phổ quát. "Độ trễ quan trọng hơn băng thông" đúng cho trang web điển hình, vì một trang là hàng chục tài nguyên nhỏ, mỗi cái tốn vài vòng khứ hồi (phân giải DNS, bắt tay TCP, xin và nhận) nhưng chỉ vài KB dữ liệu — nên tổng thời gian bị số vòng khứ hồi thống trị, tức bị RTT thống trị. Nhưng nó sai cho tải khối lớn: một video, một bản cập nhật phần mềm, một lần đồng bộ tệp — ở đó có rất ít vòng khứ hồi nhưng rất nhiều byte phải chảy qua, nên băng thông mới là thứ quyết định.
Bài học đo lường: một câu châm ngôn kỹ thuật hầu như luôn kèm một ngữ cảnh ẩn; bê nó ra khỏi ngữ cảnh đó là biến nó thành sai. Câu trả lời đúng cho "độ trễ hay băng thông giới hạn?" không phải chọn một, mà là "tùy dạng tải: nhiều-nhỏ thì độ trễ, ít-lớn thì băng thông". Tôi suýt viết một luật bắt tai mà sai đúng một nửa số trường hợp — và cái cứu tôi chỉ là chịu khó đo thêm một dạng tải nữa thay vì dừng ở dạng khớp với định kiến.
Vì sao độ trễ thống trị trang web
Đáng hiểu vì sao trang web lại nghiêng về phía độ trễ đến vậy, vì nó giải thích luôn cách tối ưu. Một trang tải xong cần một chuỗi các bước, mỗi bước là ít nhất một vòng khứ hồi: phân giải DNS, bắt tay TCP, bắt tay TLS, rồi xin từng tài nguyên. Tệ hơn, TCP không dùng ngay toàn bộ băng thông — nó khởi động chậm (cửa sổ tắc nghẽn tăng dần qua vài vòng khứ hồi), nên cả một tài nguyên nhỏ cũng bị đo bằng số vòng khứ hồi chứ không phải bằng byte. Cộng tất cả lại, thời gian tải một trang xấp xỉ (số vòng khứ hồi) × RTT + (số byte) ÷ băng thông. Với 120 KB, cái số hạng byte÷băng thông chỉ cỡ 100 ms ở 10 mbit — nhỏ xíu so với hàng chục vòng khứ hồi. Đó là lý do RTT thống trị, và cũng là lý do file lớn (nơi số hạng byte phình to) thì ngược lại.
Vì sao điều này quan trọng khi lập trình
Hệ quả đầu tiên là tối ưu trang web bằng cách giảm số vòng khứ hồi, không phải bằng cách mua thêm băng thông. Vì thời gian bị RTT × (số vòng) thống trị, đòn bẩy lớn nhất là cắt số vòng: gộp tài nguyên để bớt số request, tái dùng kết nối keep-alive (né bắt tay lặp lại, đúng cái đã đo ở bài keep-alive), dùng HTTP/2 để ghép nhiều request trên một kết nối, và — quan trọng nhất — đặt máy chủ/CDN gần người dùng về mặt địa lý để RTT nhỏ đi. Một CDN không cho bạn thêm băng thông; nó cho bạn ít mili giây RTT hơn, và với trang web đó mới là vàng.
Hệ quả thứ hai là nhận ra mình đang ở dạng tải nào trước khi tối ưu. Nếu sản phẩm của bạn là phục vụ trang, API nhỏ, tin realtime — bạn ở vùng độ-trễ-thống-trị, hãy dồn sức giảm vòng khứ hồi và khoảng cách. Nếu sản phẩm của bạn là phục vụ video, tải tệp lớn, sao lưu, đồng bộ dữ liệu — bạn ở vùng băng-thông-thống-trị, hãy dồn sức vào thông lượng (nén, băng thông, cửa sổ TCP đủ lớn để lấp đường ống). Tối ưu nhầm trục là đổ công vào thứ không phải nút thắt: mua thêm băng thông cho một API nhỏ, hay tối ưu RTT cho một dịch vụ tải phim.
Hệ quả thứ ba là một bài học đo lường chung, đúng cho cả sê-ri: khi một quy tắc nghe hợp lý, hãy đo nó ở một ngữ cảnh thứ hai trước khi tin. Con số mang theo: cái giới hạn tải trang phụ thuộc dạng tải — trang nhiều tài nguyên nhỏ bị độ trễ thống trị (băng thông chững sau ngưỡng thấp), tải khối lớn bị băng thông thống trị (RTT gần như vô can). Đừng hỏi "độ trễ hay băng thông quan trọng hơn?" như thể có một đáp án; hãy hỏi "tải của tôi là nhiều-nhỏ hay ít-lớn?", vì chính hình dạng đó chọn nút thắt cho bạn.
Thử ba mươi giây
Mở DevTools tab Network trên một trang bất kỳ, xem cột Waterfall. Bạn sẽ thấy phần lớn mỗi thanh không phải là tải nội dung (đoạn màu đậm cuối cùng) mà là chờ — chờ kết nối, chờ TLS, chờ máy chủ trả byte đầu tiên (TTFB). Những khoảng chờ đó chính là độ trễ, là các vòng khứ hồi, và với một trang nhiều tài nguyên nhỏ chúng cộng dồn thành phần lớn thời gian. Rồi thử tải một file lớn (một video, một bản cài đặt) và nhìn con số tốc độ: giờ nó bám sát băng thông đường truyền của bạn, còn độ trễ gần như biến mất khỏi bức tranh. Hai lần quan sát đó, trên cùng một máy cùng một mạng, là toàn bộ điều bài này đo — cái giới hạn không cố định, nó đổi theo thứ bạn đang tải.