bài trước tôi cố định cửa sổ TCP rồi tăng RTT, và thấy thông lượng tụt theo tỉ lệ nghịch của RTT. Nhưng đó mới là một nửa câu chuyện. Lần này tôi làm ngược lại: giữ RTT cố định và tăng dần kích thước cửa sổ, để trả lời câu hỏi thực tế hơn — cửa sổ càng lớn thì càng nhanh, nhưng lớn tới đâu thì hết tác dụng? Câu trả lời, đo bằng số thật trong container, vẽ ra một đường cong có điểm gãy rất rõ.

Cửa sổ TCP

Hai cái trần, thông lượng lấy cái thấp hơn

Cửa sổ TCP là lượng dữ liệu tối đa được phép "đang bay" mà chưa nhận xác nhận. Nó cùng với RTT quyết định thông lượng theo công thức cửa sổ / RTT. Nhưng có một trần thứ hai luôn tồn tại: băng thông vật lý của đường truyền. Không cửa sổ nào đẩy dữ liệu nhanh hơn tốc độ đường ống cho phép. Ghép lại, thông lượng thật là giá trị nhỏ hơn của hai cái trần:

thông lượng = min( cửa sổ / RTT , băng thông )

Công thức này dự đoán hai chế độ. Khi cửa sổ nhỏ, cửa sổ / RTT là số nhỏ hơn, nên nó là nút thắt — tăng cửa sổ thì thông lượng tăng theo. Khi cửa sổ đủ lớn để cửa sổ / RTT vượt băng thông, băng thông trở thành nút thắt — tăng cửa sổ thêm nữa vô ích. Điểm chuyển giữa hai chế độ là chỗ đáng đo.

Đo: cố định RTT, quét kích thước cửa sổ

Tôi dựng một đường mạng thật nhưng lần này ép nó giống đường Internet chứ không phải dây trong nhà: dùng netem vừa thêm 10 ms mỗi chiều (RTT ~32 ms), vừa giới hạn băng thông xuống 100 Mbit/s (tức ~12,5 MB/s). Rồi tôi đặt cửa sổ nhận bằng SO_RCVBUF ở nhiều cỡ và đo thông lượng của một luồng tải liên tục, lấy trung vị ba lần:

tc qdisc add dev veth0 root netem delay 10ms rate 100mbit
# máy khách đặt SO_RCVBUF = 32KB, 64KB, ... 2MB rồi đo bytes/thời gian

Kết quả (cửa sổ hiệu dụng là giá trị nhân thật sự dùng, thường gấp đôi giá trị yêu cầu):

Cửa sổ hiệu dụng Thông lượng Chế độ
32 KB 0,9 MB/s cửa sổ chặn (~cửa sổ/RTT)
64 KB 1,8 MB/s cửa sổ chặn
128 KB 3,8 MB/s cửa sổ chặn
256 KB 7,9 MB/s cửa sổ chặn
512 KB 11,7 MB/s băng thông chặn
1024 KB 11,7 MB/s băng thông chặn
2048 KB 11,7 MB/s băng thông chặn

Đường cong gãy đúng như công thức dự đoán. Ở nửa trên, mỗi lần cửa sổ gấp đôi thì thông lượng gấp đôi: 32→64 KB cho 0,9→1,8; 128→256 KB cho 3,8→7,9. Trong vùng này, thông lượng ≈ cửa sổ / RTT (256 KB chia 32 ms ≈ 8 MB/s, khớp với 7,9 đo được). Rồi tới cửa sổ 512 KB, thông lượng chạm 11,7 MB/s và đứng lại — mọi cửa sổ lớn hơn đều cho đúng con số đó. Băng thông 100 Mbit đã trở thành nút thắt (11,7 thay vì 12,5 là vì header IP/TCP/Ethernet ăn mất vài phần trăm).

Điểm gãy chính là tích băng thông–độ trễ

Điểm gãy nằm giữa 256 KB và 512 KB không phải ngẫu nhiên. Đó là tích băng thông–độ trễ (bandwidth-delay product, BDP) — kích thước cửa sổ vừa đủ để lấp đầy đường ống:

BDP = băng thông × RTT = 12,5 MB/s × 0,032 s ≈ 400 KB

Đúng 400 KB, rơi ngay giữa hai dòng bảng đổi chế độ. BDP là con số vàng khi tinh chỉnh: cửa sổ nhỏ hơn BDP thì bạn để băng thông nằm không (đường ống chưa đầy đã phải dừng chờ ACK); cửa sổ lớn hơn BDP thì phần dư chẳng làm tăng tốc, chỉ ngốn thêm RAM cho bộ đệm và, tệ hơn, có thể gây bufferbloat — dữ liệu xếp hàng trong các bộ đệm quá lớn dọc đường làm độ trễ phình ra. Cửa sổ lý tưởng là vừa đúng BDP.

Đây cũng là chỗ tùy chọn window scaling bước vào. Trường cửa sổ trong header TCP gốc chỉ có 16 bit, tối đa 65535 byte — với đường của tôi, 64 KB chỉ cho 2 MB/s, khóa cứng thông lượng thấp hơn nhiều so với băng thông. Mọi cửa sổ vượt 64 KB trong bảng trên chỉ đạt được nhờ tùy chọn window scaling (nhân đôi cửa sổ theo một hệ số lũy thừa 2), thứ được bật mặc định trên Linux hiện đại. Không có nó, "đường ống to" và "đường ống dài" đồng nghĩa với thông lượng kẹt ở đáy.

Một lần tôi đo hớ vì đường thí nghiệm quá nhanh

Trước khi giới hạn băng thông, tôi chạy đúng phép quét này trên veth trần — không đặt rate. Kết quả làm tôi bối rối theo hướng ngược lại: thông lượng cứ tăng mãi theo cửa sổ, không hề bằng phẳng. Cửa sổ 1 MB cho 29 MB/s, 2 MB cho 45 MB/s, 4 MB cho 87 MB/s, và vẫn còn dốc lên. Tôi suýt kết luận "cửa sổ càng to càng nhanh, chẳng có trần nào cả".

Kết luận đó sai, và cái sai nằm ở dụng cụ đo. veth là giao diện ảo trong bộ nhớ; nó chuyển byte ở tốc độ RAM, tức thực tế không có trần băng thông để thông lượng đụng vào. Trong công thức min(cửa sổ/RTT, băng thông), số hạng băng thông gần như vô hạn, nên cửa sổ/RTT luôn là nút thắt — và tôi chỉ nhìn thấy đúng một nửa của đường cong. Không có chế độ thứ hai để mà thấy điểm gãy.

Cách sửa là thêm rate 100mbit để netem mô phỏng một đường truyền thật có băng thông hữu hạn. Ngay khi làm vậy, khúc bằng phẳng hiện ra và công thức đầy đủ mới lộ diện. Bài học đo lường quan trọng: đo mạng trên loopback hay veth trần sẽ giấu mất nửa sự thật — bạn thấy được ảnh hưởng của cửa sổ và RTT, nhưng không bao giờ thấy giới hạn băng thông, vì môi trường ảo không có nó. Muốn kết quả áp dụng được cho đường thật, phải mô phỏng cả băng thông hữu hạn, nếu không bạn đang đo một thế giới nơi mọi đường ống đều rộng vô tận.

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

Hệ quả trực tiếp nhất là chuyện tinh chỉnh bộ đệm. Trên một đường dài và rộng (long fat network — RTT cao, băng thông lớn, ví dụ truyền dữ liệu giữa hai trung tâm dữ liệu xuyên lục địa), BDP rất lớn — hàng megabyte. Bộ đệm TCP mặc định của hệ điều hành thường được đặt cho mạng nội bộ RTT thấp, nên trên đường này chúng nhỏ hơn BDP và bóp nghẹt thông lượng dù băng thông thừa thãi. Đây là lý do các tham số tcp_rmem/tcp_wmem tồn tại, và là việc đầu tiên phải xem khi một luồng đơn qua đường xa chạy chậm hơn nhiều so với băng thông thuê bao. Công thức để chỉnh rất cụ thể: đặt cửa sổ tối đa ≥ băng thông × RTT của đường xấu nhất bạn cần phục vụ.

Hệ quả thứ hai là hiểu đúng vì sao một luồng đơn thường không lấp nổi một đường băng thông lớn. Nếu cửa sổ tối đa của bạn là 4 MB và RTT là 100 ms, trần thông lượng của một luồng là 40 MB/s — dù đường là 10 Gbit. Muốn lấp đường đó phải mở nhiều luồng song song (điều mà các công cụ tải như aria2 hay giao thức như BitTorrent làm), hoặc nới cửa sổ lên đúng BDP. Cùng một lý do, các hệ thống sao lưu và nhân bản dữ liệu qua khoảng cách xa luôn phải chỉnh cửa sổ, không thì băng thông đắt tiền nằm không.

Con số mang theo từ bài này: thông lượng bằng giá trị nhỏ hơn giữa cửa sổ/RTT và băng thông; nó tăng tuyến tính theo cửa sổ cho tới điểm gãy đúng bằng BDP, rồi bằng phẳng. Chỉnh cửa sổ dưới BDP là phí băng thông; trên BDP là phí bộ nhớ. Điểm ngọt nằm ngay tại tích băng thông–độ trễ.

Thử ba mươi giây

Xem cửa sổ tối đa mà máy bạn cho phép: sysctl net.ipv4.tcp_rmem in ra ba số (tối thiểu, mặc định, tối đa) tính bằng byte. Lấy số cuối chia cho RTT tới một máy chủ ở xa (đo bằng ping) để ra trần thông lượng của một luồng đơn tới đó. Nếu con số đó thấp hơn băng thông đường truyền của bạn, thì chính cửa sổ — không phải đường truyền — đang là thứ giới hạn tốc độ, và nới tcp_rmem lên cỡ BDP sẽ gỡ nút thắt.