Hai container, một mạng Docker, một luồng gửi và một luồng nhận. Bài này đo hai nút mà người ta hay chỉnh: kích thước mỗi lần send, và SO_SNDBUF.

Thông lượng TCP theo chunk và theo bộ đệm

Kích thước mỗi lần send: 36 lần

Gửi 256 MB, bộ đệm để mặc định, chỉ đổi số byte mỗi lần gọi send. Ba lần đo mỗi mức:

Mỗi lần send Ba lần đo (MB/s)
512 B 323,5 / 341,4 / 342,8
1 KB 410,8 / 430,2 / 420,7
4 KB 1.024,6 / 1.067,7 / 1.037,8
16 KB 4.594,8 / 4.300,3 / 4.145,4
64 KB 9.217,8 / 10.492,8 / 9.595,7
256 KB 10.354,8 / 10.358,2 / 11.338,9
1 MB 11.426,2 / 12.196,9 / 12.277,1

36 lần giữa 512 byte và 1 MB, và đường cong gãy rõ ở 64 KB.

Đây là cùng một hiện tượng phần 21 đã đo với ổ đĩa: mỗi lời gọi hệ thống có chi phí cố định, và với gói nhỏ thì chi phí đó áp đảo. Phần 24 đo được giá của một lời gọi hệ thống là khoảng 1.000 ns; gửi 512 byte mỗi lần nghĩa là trả 1.000 ns cho 512 byte.

Trên 64 KB thì gần như không thêm được gì. Đó là điểm dừng đáng nhớ.

Trong ứng dụng thật, việc này thường không nằm ở lời gọi send mà nằm ở tầng trên:

w := bufio.NewWriterSize(conn, 64*1024)   // Go
f = sock.makefile("wb", buffering=65536)  # Python

Một dịch vụ ghi từng dòng JSON thẳng ra socket đang ở dòng đầu của bảng trên.

Rồi SO_SNDBUF đặt tay làm chậm đi

Cùng phép đo, chunk cố định 64 KB, chỉ đổi bộ đệm gửi:

Xin Nhận cấp thực tế Ba lần đo (MB/s)
mặc định 46.080 10.420,0 / 11.457,6 / 10.737,0
4 KB 8.192 2.089,6 / 2.226,6 / 2.073,1
16 KB 32.768 2.336,2 / 2.230,5 / 1.807,8
64 KB 131.072 3.760,1 / 3.919,5 / 2.943,5
256 KB 425.984 11.483,0 / 9.496,9 / 8.718,4
1 MB 425.984 11.240,7 / 11.263,6 / 9.405,7
4 MB 425.984 8.992,2 / 9.723,4 / 11.056,4

Bộ đệm mặc định chỉ 46 KB — nhỏ hơn mọi giá trị đặt tay từ 64 KB trở lên — mà nhanh bằng hoặc hơn tất cả.

Và đặt một giá trị "an toàn" như 64 KB làm chậm gần ba lần so với để yên.

Lý do: gọi setsockopt(SO_SNDBUF) tắt cơ chế tự điều chỉnh của nhân. Bình thường nhân theo dõi độ trễ và mức mất gói của kết nối rồi co giãn bộ đệm trong dải tcp_wmem:

tcp_wmem = 4096   16384   4194304
           ^toi thieu  ^khoi dau  ^toi da

Nó điều chỉnh liên tục theo tình hình thật. Một con số cố định do bạn chọn không thể theo kịp, và nó luôn sai ở ít nhất một trong hai chiều — quá nhỏ khi mạng nhanh, quá lớn khi mạng chậm.

Nói lại lần thứ hai vì nó ngược với hầu hết lời khuyên tinh chỉnh trên mạng: để yên SO_SNDBUFSO_RCVBUF là lựa chọn đúng trong gần như mọi trường hợp.

Hai điều ngạc nhiên nhỏ trong bảng

Nhân cấp gấp đôi số bạn xin. Xin 4 KB được 8.192; xin 16 KB được 32.768. Phần thêm là chỗ cho siêu dữ liệu của gói (sk_buff), không phải dữ liệu của bạn. Nên khi đọc getsockopt, chia đôi mới ra dung lượng dùng được.

Trần bị chặn ở 425.984. Xin 1 MB hay 4 MB đều nhận đúng con số đó, vì:

net.core.wmem_max = 212992     ->  212992 x 2 = 425984

Xin nhiều hơn wmem_max không báo lỗi. setsockopt trả về thành công, và bạn nhận được ít hơn nhiều so với tưởng. Luôn đọc lại bằng getsockopt nếu thật sự cần con số đó.

Khi nào đặt tay là đúng

Đúng một trường hợp: đường truyền có tích số băng thông × độ trễ lớn hơn trần tự điều chỉnh.

Ví dụ đường 10 Gbps xuyên lục địa, độ trễ khứ hồi 150 ms:

10 Gbps x 0,15 s / 8 = 187,5 MB

Cần bộ đệm cỡ đó mới lấp đầy được đường truyền. Trần mặc định 4 MB của tcp_wmem chặn ở khoảng 200 Mbps.

Cách đúng là nâng trần để nhân vẫn tự điều chỉnh, chứ không phải đóng đinh một con số:

sysctl -w net.core.wmem_max=134217728
sysctl -w net.core.rmem_max=134217728
sysctl -w net.ipv4.tcp_wmem="4096 87380 134217728"
sysctl -w net.ipv4.tcp_rmem="4096 87380 134217728"

Với mạng nội bộ trung tâm dữ liệu (độ trễ dưới 1 ms), tích số đó chỉ vài chục KB và mặc định đã dư.

Ba chỗ khác đáng nhìn hơn

ss -tim state established | head -20

ss -i hiện cwnd, rtt, retrans cho từng kết nối. retrans khác 0 là mất gói thật, và không bộ đệm nào chữa được mất gói.

netstat -s | grep -iE 'retransmit|overflow|dropped|pruned'
cat /proc/net/softnet_stat | awk '{print "  drop:", $2}' | head -4

listen queue overflow nghĩa là backlog quá nhỏ — dòng này gây ra kết nối bị từ chối trong lúc tải cao, và nó không liên quan gì tới SO_SNDBUF:

sysctl -w net.core.somaxconn=4096
# va trong ma nguon: listen(fd, 4096)

Cả hai chỗ phải cùng tăng; listen() bị cắt xuống theo somaxconn.

Giới hạn của phép đo này

Hai container trên cùng một máy, qua cầu nối Docker. Độ trễ gần bằng 0 và không có mất gói. Đó là lý do thông lượng lên tới 12 GB/s — cao hơn mọi card mạng thật.

Nghĩa là kết luận "chunk 64 KB là điểm gãy""đừng đặt tay SO_SNDBUF" vẫn đứng vững (chúng nói về chi phí lời gọi hệ thống và về cơ chế tự điều chỉnh), còn phần bàn về tích số băng thông × độ trễ thì tôi không đo được và chỉ trình bày phép tính.

Thử ba mươi giây

Xem kết nối của bạn đang dùng bộ đệm bao nhiêu và có mất gói không:

ss -tim state established 2>/dev/null | head -20

echo "--- tran he thong ---"
sysctl net.core.{wmem_max,rmem_max,somaxconn} net.ipv4.tcp_{wmem,rmem}

echo "--- dau hieu nghen ---"
netstat -s 2>/dev/null | grep -iE 'listen queue|overflow|retransmit' | head -5

Nếu ss -i hiện retrans lớn dần, vấn đề của bạn là mất gói, không phải bộ đệm — và mọi giờ bỏ ra chỉnh SO_SNDBUF sẽ không đổi được gì.

Phần sau: TCP — bắt tay, cửa sổ trượt, và những gì xảy ra ở một kết nối chậm.