Tại GreenNode, chúng tôi liên tục cải thiện dịch vụ của mình. Đội kỹ sư và QA luôn tìm cơ hội để làm sản phẩm tốt hơn, ngay cả trước khi vấn đề kịp ảnh hưởng đến khách hàng.
Một trong những con số chúng tôi theo dõi sát là network throughput. Với các sản phẩm như Load Balancer (LB), throughput trở nên đặc biệt đáng chú ý khi lưu lượng đi xuyên vùng, nơi độ trễ mạng đóng vai trò lớn hơn nhiều. Trong một lần test hiệu năng nội bộ, chúng tôi phát hiện một khoảng chênh lệch thú vị: lưu lượng TCP đơn luồng (single-threaded) xuyên vùng chạy tốt khi đi trực tiếp VM-to-VM, nhưng throughput tụt rõ rệt khi có LB nằm trên đường truyền.
Chưa khách hàng nào báo đây là vấn đề, nhưng các con số đủ khiến chúng tôi chú ý. Trước khi đi sâu, hãy cùng điểm qua những cơ chế đang giật dây phía sau.
Nền tảng: các cơ chế đứng sau những con số
Nhìn nhanh về Load Balancer (LB) của chúng tôi
Ở mức tổng quan, bạn có thể hình dung LB như một máy ảo chạy HAProxy. Và HAProxy hoạt động như một full proxy, nên thứ trông như một kết nối end-to-end thực chất là hai kết nối TCP độc lập:
Client VM ── TCP #1 ──> LB (HAProxy) ── TCP #2 ──> Backend VMChi tiết này quan trọng. Vì LB tham gia trực tiếp vào cả hai kết nối TCP, TCP stack của chính nó có thể ảnh hưởng trực tiếp đến throughput mà client nhìn thấy. Với điều đó trong đầu, hãy xem nhanh các cơ chế TCP liên quan đến cuộc điều tra.
Những điều cơ bản về throughput của TCP
TCP được thiết kế để truyền dữ liệu một cách tin cậy mà không làm quá tải mạng hay bên nhận. Để làm được, nó dựa vào hai cơ chế bổ trợ cho nhau: congestion control (kiểm soát tắc nghẽn) và flow control (kiểm soát luồng).
Congestion control giới hạn lượng dữ liệu mạng có thể chuyển tải an toàn thông qua congestion window (cwnd). Cửa sổ này tiến hóa thế nào phụ thuộc vào thuật toán congestion control đang dùng. Flow control thì ngăn bên gửi làm quá tải bên nhận: bên nhận quảng bá một receive window (rwnd), cho bên gửi biết nó sẵn sàng nhận bao nhiêu dữ liệu chưa được ACK.
Trên Linux, điều này gắn chặt với việc cấp phát socket buffer của TCP và các tham số như:
net.ipv4.tcp_rmem = <min> <default> <max>Cùng nhau, cwnd và rwnd quyết định lượng dữ liệu TCP có thể giữ “đang bay” (in flight). Nhưng nên có bao nhiêu dữ liệu in flight để tận dụng tối đa một đường mạng? Đây là lúc Bandwidth-Delay Product (BDP) xuất hiện.
Về bản chất, BDP biểu diễn dung lượng của kết nối mạng giữa bên gửi và bên nhận. Nếu coi kết nối mạng như một cái ống, thì bandwidth là độ rộng của ống và RTT là chiều dài. Tổng lượng dữ liệu cần để giữ ống luôn đầy chính là BDP, tính bằng: BDP = Bandwidth × RTT.
Điều này nghĩa là “cái ống” chỉ đạt throughput tối đa khi được lấp đầy hoàn toàn — nói đơn giản, khi bên gửi luôn giữ ống đầy dữ liệu. Tuy nhiên, bên gửi không thể bơm dữ liệu vô hạn. Theo cơ chế của TCP, lượng dữ liệu bên gửi có thể đẩy vào ống bị giới hạn chặt bởi effective window size W, vốn bị chặn bởi cả flow control lẫn congestion control: W = min(cwnd, rwnd).
Nếu W nhỏ hơn BDP của đường truyền, cái ống sẽ “đói”. Để lấp đầy kết nối và đạt throughput tối đa, công thức rất đơn giản: W ≥ BDP.
Điều tra: lần theo các con số
Giờ khi đã nắm phần cơ chế, hãy quay lại bí ẩn ban đầu: vì sao một kết nối TCP đơn luồng qua LB lại chậm hơn hẳn khi đi xuyên vùng? LB cạn compute? Bản thân đường mạng là nút thắt? Hay có gì đó bên trong TCP stack đang âm thầm kìm từng kết nối lại?
Chỉ có một cách để biết — lần theo các con số.
Thiết lập baseline
Chúng tôi bắt đầu với một bài test iperf3 đơn giản giữa vùng Hà Nội (HAN) và TP. Hồ Chí Minh (HCM), so sánh kết nối trực tiếp VM-to-VM (dư CPU và socket buffer cấu hình rộng rãi) với cùng lưu lượng nhưng đi qua LB.
Direct:
VM (HAN) ─────────────────────────────> VM (HCM)
Through LB:
VM (HAN) ──────> LB (HCM) ──────────> VM (HCM)iperf3 -c <IP> -O 2| Chỉ số | VM-LB-VM | VM-VM |
|---|---|---|
| Sender Transfer | 832 MBytes | 3.90 GBytes |
| Sender Bitrate | 698 Mbits/sec | 3.35 Gbits/sec |
| Receiver Transfer | 834 MBytes | 3.89 GBytes |
| Receiver Bitrate | 698 Mbits/sec | 3.34 Gbits/sec |
| Total Retransmits | 54 | 208,213 |
| Average cwnd | ~4.3–4.5 MBytes | ~18–37 MBytes |
Kết nối trực tiếp VM-to-VM đạt khoảng ~3,35 Gbps, trong khi cùng lưu lượng qua LB chỉ quanh ~700 Mbps. Thoạt nhìn, LB là thủ phạm hiển nhiên. Nhưng nguyên nhân gốc vẫn còn bỏ ngỏ: LB bị đói compute? HAProxy tự bóp data path? Hay thứ gì đó sâu hơn trong networking stack đang bóp nghẹt kết nối?
HAProxy hay compute có thực sự là thủ phạm?
Để tách HAProxy khỏi môi trường appliance của LB, chúng tôi dựng một VM sạch với dư tài nguyên, triển khai HAProxy theo đúng topology, và cố tình chỉnh socket buffer TCP với giới hạn rộng rãi:
Client VM ── TCP #1 ──> VM (HAProxy) ── TCP #2 ──> Backend VMKết quả? Throughput dễ dàng khớp với baseline trực tiếp ~3,35 Gbps.
Để loại trừ giới hạn compute, chúng tôi còn hạ VM test xuống flavor nhỏ nhất — nhưng throughput vẫn cao như vậy. Ngược lại, trên chính LB thật, mọi tier đều bị chặn ở đúng ~700 Mbps, dù chạy flavor nhỏ nhất hay instance mạnh nhất.
Điều này làm rõ một điều: cả cạn kiệt compute lẫn bản thân HAProxy đều không phải nguyên nhân của trần throughput.
Để xác nhận, chúng tôi đào vào TCP data path của HAProxy trong mã nguồn. Những gì tìm thấy hoàn toàn chuẩn: HAProxy chỉ đóng vai cầu nối giữa các Linux socket, dùng splice() để forward zero-copy khi có thể và fallback về recv()/send():
// Source code HAProxy
// raw_sock.c, line 91-92:
ret = splice(conn->handle.fd, NULL, pipe->prod, NULL, count, SPLICE_F_MOVE|SPLICE_F_NONBLOCK);
// raw_sock.c, line 189-190:
ret = splice(pipe->cons, NULL, conn->handle.fd, NULL, pipe->data, SPLICE_F_MOVE|SPLICE_F_NONBLOCK);
// raw_sock.c, line 267:
ret = recv(conn->handle.fd, b_tail(buf), try, 0);
// raw_sock.c, line 375-379:
send_flag = MSG_DONTWAIT | MSG_NOSIGNAL;
if (try < count || flags & CO_SFL_MSG_MORE)
send_flag |= MSG_MORE;
ret = send(conn->handle.fd, b_peek(buf, done), try, send_flag);Hoàn toàn không có cơ chế ở tầng ứng dụng áp một cửa sổ TCP cố định hay giới hạn băng thông nhân tạo nào. Vì throughput và số byte in-flight được quyết định hoàn toàn bởi TCP stack của kernel, nút thắt rõ ràng không nằm ở HAProxy mà hướng thẳng về môi trường host của LB.
Tinh chỉnh kernel
Đến đây, cuộc điều tra đã chuyển xuống dưới HAProxy, vào thẳng Linux TCP stack. Chúng tôi đã biết kết nối LB luôn plateau quanh 700 Mbps, với iperf3 báo congestion window chỉ vài MiB. Nhưng trước khi đổi bất kỳ tham số kernel nào, cần trả lời một câu hữu ích hơn: đường truyền này thực sự cần giữ bao nhiêu dữ liệu in flight?
Để trả lời, trước hết cần RTT.
Tính ngược từ kết nối
Trong bài test VM (HAN) → LB (HCM) → VM (HCM), iperf3 báo một congestion window ổn định đáng kể, khoảng 4,3–4,5 MiB, với 4,36 MiB là giá trị đại diện:
| Interval | Transfer | Bitrate | Retr | Cwnd |
|---|---|---|---|---|
| 0.00–1.00 sec | 85.0 MBytes | 713 Mbits/sec | 0 | 4.36 MBytes |
| 1.00–2.00 sec | 83.8 MBytes | 703 Mbits/sec | 0 | 4.35 MBytes |
| 2.00–3.00 sec | 83.8 MBytes | 703 Mbits/sec | 0 | 4.31 MBytes |
| 3.00–4.00 sec | 81.2 MBytes | 682 Mbits/sec | 37 | 4.32 MBytes |
| 4.00–5.00 sec | 81.2 MBytes | 682 Mbits/sec | 16 | 4.34 MBytes |
| 5.00–6.00 sec | 85.0 MBytes | 713 Mbits/sec | 0 | 4.37 MBytes |
| 6.00–7.00 sec | 83.8 MBytes | 703 Mbits/sec | 0 | 4.46 MBytes |
| 7.00–8.00 sec | 83.8 MBytes | 703 Mbits/sec | 0 | 4.42 MBytes |
| 8.00–9.00 sec | 81.2 MBytes | 682 Mbits/sec | 1 | 4.35 MBytes |
| 9.00–10.00 sec | 83.8 MBytes | 703 Mbits/sec | 0 | 4.48 MBytes |
| Sender 10s Average | 832 MBytes | 698 Mbits/sec | 54 | — |
| Receiver 10s Average | 834 MBytes | 698 Mbits/sec | — | — |
Đổi giá trị đại diện này ra byte:
4,36 MBytes = 4,36 × 1.024 × 1.024 ≈ 4.571.791 byte.
Con số này khớp trực tiếp với trần TCP receive buffer đang cấu hình trên kernel của LB:
net.ipv4.tcp_rmem = 4096 131072 4194304Tham số thứ ba giới hạn receive buffer tối đa ở 4 MiB (4.194.304 byte). Congestion window quan sát được plateau quanh 4,3–4,5 MiB, đúng ngay trần này. Vì một luồng bị giới hạn bởi rwnd không bao giờ cho cwnd lý do để lớn tiếp, cửa sổ đơn giản dừng lại ở đúng mức mà bên nhận cho phép.
Trong bài test này, kết nối duy trì khoảng 703 Mbps với gần như không retransmit. Giả sử khoảng 4,36 MiB dữ liệu in flight, ta tính ngược từ quan hệ throughput–BDP — Throughput ≈ Window ÷ RTT — và biến đổi để tìm RTT:
RTT ≈ Window ÷ Throughput = (4.571.791 × 8 bit) ÷ 703.000.000 bps ≈ 0,052 s = 52 ms
Vậy RTT ước lượng cho đường HAN–HCM là khoảng 52 ms.
Từ RTT đến cửa sổ thực sự cần
Biết RTT cho phép ta lật ngược phương trình BDP và hỏi câu thực sự quan trọng cho việc tuning: TCP phải giữ bao nhiêu dữ liệu in flight để duy trì throughput đa gigabit trên đường 52 ms?
Baseline trực tiếp VM-to-VM đạt khoảng 3,34 Gbps, nên chúng tôi dùng nó làm mốc thực tế:
BDP = Bandwidth × RTT = (3,34 × 10⁹ bit/s) × 0,052 s ≈ 173,7 × 10⁶ bit ≈ 20,7 MiB
Do đó, buffer tối thiểu để đường truyền duy trì throughput mục tiêu là 20,7 MiB — vì như đã nói, công thức rất đơn giản: W ≥ BDP. Nếu cửa sổ in-flight W tụt dưới ngưỡng này, bên gửi đơn giản là hết “credit” và ngồi chờ ACK.
Vậy là xong bí ẩn? Chỉ cần đẩy socket buffer lên 32 hay 64 MiB là xong? Chưa hẳn. Đây là chuyện xảy ra khi bạn trao cho TCP một tờ séc khống trên baseline VM-to-VM:
| Interval | Transfer | Bitrate | Retr | Cwnd |
|---|---|---|---|---|
| 0.00–1.00 sec | 394 MBytes | 3.30 Gbits/sec | 34,995 | 9.37 MBytes |
| 1.00–2.00 sec | 406 MBytes | 3.41 Gbits/sec | 17,663 | 34.6 MBytes |
| 2.00–3.00 sec | 378 MBytes | 3.17 Gbits/sec | 22,858 | 28.0 MBytes |
| 3.00–4.00 sec | 435 MBytes | 3.65 Gbits/sec | 20,638 | 29.5 MBytes |
| 4.00–5.00 sec | 449 MBytes | 3.76 Gbits/sec | 12,662 | 37.3 MBytes |
| 5.00–6.00 sec | 409 MBytes | 3.43 Gbits/sec | 31,434 | 32.0 MBytes |
| 6.00–7.00 sec | 412 MBytes | 3.46 Gbits/sec | 31,696 | 28.6 MBytes |
| 7.00–8.00 sec | 429 MBytes | 3.60 Gbits/sec | 20,595 | 30.2 MBytes |
| 8.00–9.00 sec | 356 MBytes | 2.99 Gbits/sec | 12,129 | 18.1 MBytes |
| 9.00–10.00 sec | 330 MBytes | 2.77 Gbits/sec | 3,543 | 17.7 MBytes |
| Sender 10s Average | 3.90 GBytes | 3.35 Gbits/sec | 208,213 | — |
| Receiver 10s Average | 3.89 GBytes | 3.34 Gbits/sec | — | — |
Cửa sổ phình lên khoảng 18–37 MiB, và dù con số tiêu đề nhìn ấn tượng ở mức trung bình 3,34 Gbps, cái giá phải trả thì kinh hoàng: hơn 208.000 lần retransmit. Nhồi nhiều dữ liệu hơn mức các queue phía sau hay QoS policer “tiêu hóa” nổi không tạo ra throughput thật — nó chỉ mua thêm packet drop. Bitrate đẹp trên giấy, lãng phí thuần túy trong thực tế.
Mốc 3,34 Gbps chỉ cho thấy trần vật lý tuyệt đối của đường truyền, không phải một baseline production hợp lý. Thay vì ném thẳng 20,7 MiB BDP vào sysctl, chúng tôi coi nó là điểm neo để tìm “sweet spot”: đủ rộng để thoát khỏi kẹp 4 MiB nhân tạo, nhưng đủ gọn để giữ packet drop và retransmit trong tầm kiểm soát.
Tìm điểm sweet spot
Phép tính BDP cho ta một mốc tham chiếu trên, nhưng tuning cho production rốt cuộc là một sự đánh đổi. Vì vậy, thay vì nhảy thẳng lên ~20,7 MiB, chúng tôi test vài mức trần buffer quanh điểm đó và đo không chỉ throughput mà cả mức retransmit mà mỗi MiB tăng thêm phải trả.
| Giới hạn buffer | Throughput TB | Peak cwnd quan sát | Dữ liệu truyền | Retransmits | Tỉ lệ mất gói |
|---|---|---|---|---|---|
| 17 MiB | 2.59 Gbps | ~17.7 MiB | 3.03 GBytes | 926 | ~0.041% |
| 18 MiB | 2.69 Gbps | ~18.7 MiB | 3.12 GBytes | 2,064 | ~0.089% |
| 19 MiB | 2.74 Gbps | ~19.8 MiB | 3.19 GBytes | 2,916 | ~0.123% |
| 20 MiB | 2.85 Gbps | ~21.2 MiB | 3.32 GBytes | 3,115 | ~0.126% |
Đi từ 17 MiB lên 20 MiB, throughput trung bình tăng từ 2,59 Gbps lên 2,85 Gbps, trong khi cwnd quan sát được lên khoảng 21,2 MiB — rất gần mức ~20,7 MiB BDP tính trước đó. (Peak cwnd có thể hơi vượt giới hạn buffer vì cwnd là giới hạn congestion phía gửi, không phải trần cứng cho số byte in flight. Effective window vẫn bị chặn bởi cửa sổ mà bên nhận quảng bá.) Retransmit có tăng, từ 926 lên 3.115, nhưng tính theo phần trăm vẫn chỉ quanh ~0,126% mất gói. Hơn nữa, TCP là giao thức truyền tin cậy, nên retransmit chính là cách nó phục hồi khi mất segment. Điều chúng tôi quan tâm là liệu retransmit có giữ trong tầm kiểm soát trong khi throughput hữu ích vẫn tiếp tục cải thiện hay không.
Đi xa hơn lại là chuyện khác. Vượt điểm này, throughput tăng thêm trở nên không đáng kể trong khi retransmit leo dốc. TCP không còn tận dụng đường truyền tốt hơn một cách ý nghĩa — nó chỉ giỏi hơn trong việc tự tạo thêm việc cho chính mình. Nên chúng tôi vạch ranh giới ở 20 MiB.
Tiện là thí nghiệm rơi gần như đúng chỗ phép toán đã chỉ: ước lượng BDP là ~20,7 MiB, và 20 MiB hóa ra là sweet spot thực tế.
Thuật toán congestion control
Vậy là đã tìm được sweet spot cho buffer. Xong chưa?
Cho TCP một cửa sổ lớn hơn chỉ quyết định một kết nối có thể giữ bao nhiêu dữ liệu in flight. Nó dùng cửa sổ đó hung hãn đến đâu lại do thuật toán congestion control điều khiển. Hệ thống của chúng tôi chạy CUBIC mặc định của Linux — một thuật toán loss-based kinh điển. CUBIC rất tốt, cho đến khi bạn cố tình “chạy sát” một trần QoS được áp.
Các policer phía sau drop lưu lượng ngay khi một burst chạm rate limit, nhưng với CUBIC, mất gói là mất gói. Nó không phân biệt được giữa tắc nghẽn thật ở core-network và một policer tùy tiện cắt một spike nhất thời. Nên khoảnh khắc một gói biến mất, CUBIC hoảng, đạp phanh, lùi cửa sổ lại, rồi thận trọng bò lên và đâm vào đúng cái trần cũ. Liên tục “yo-yo” khỏi băng thông mục tiêu rõ ràng không phải kế hoạch.
Vì vậy chúng tôi chuyển sang BBR. Khác với cách tiếp cận chủ yếu loss-based của CUBIC, BBR dựng một mô hình của kết nối từ bottleneck bandwidth ước lượng và round-trip time. Hành vi đó hợp hơn nhiều với điều chúng tôi muốn: giữ ống đầy mà không coi mất gói là tín hiệu chính để giảm tốc.
Con voi mang tên bộ nhớ
Được rồi, nhưng với hàng nghìn kết nối thì sao? Lẽ thường kéo tới: đây là load balancer, không phải một server chuyên dụng đơn lẻ. Nếu đẩy socket buffer lên để chứa “ống to”, chẳng phải hàng nghìn kết nối đồng thời sẽ ngốn sạch RAM và OOM cả máy sao? May là Linux không cấp phát bộ nhớ TCP theo kiểu đó. Ba giá trị trong tcp_rmem và tcp_wmem không phải ba lượng cấp cho mọi socket. Chúng là các mức minimum, default và maximum dành cho cơ chế autotuning socket-buffer của TCP.
Từ khóa quan trọng ở đây là maximum. Một kết nối mới không lập tức giữ chỗ 20 MiB chỉ vì ta cấu hình trần 20 MiB. Các kết nối nhỏ, ngắn có thể ở gần mức cấp phát ban đầu, trong khi những kết nối thực sự cần buffer nhiều hơn có thể lớn dần về phía maximum khi autotuning phản ứng theo workload và đường truyền.
Nói đơn giản, 20 MiB max × 10.000 kết nối không tự động nghĩa là 200 GiB bộ nhớ TCP được cấp. 20 MiB đó là headroom, không phải một chỗ đặt trước.
Còn có thêm một lớp bảo vệ nữa. Trong khi tcp_rmem và tcp_wmem kiểm soát từng socket được lớn đến đâu, Linux còn theo dõi tổng tiêu thụ bộ nhớ TCP ở cấp toàn cục qua:
net.ipv4.tcp_mem = <low> <pressure> <high>Các ngưỡng này áp cho bộ nhớ TCP như một tổng thể chứ không cho một kết nối. Dưới low, áp lực bộ nhớ TCP không đáng lo và autotuning chạy bình thường. Khi tiêu thụ tăng vào vùng pressure, kernel ngày càng dè dặt trong việc cho socket buffer lớn lên và tìm cách thu hồi bộ nhớ TCP. Ngưỡng high là biên trên, ngăn TCP tiêu thụ bộ nhớ vô hạn.
Điều này cho ta đúng hành vi mong muốn: một luồng long-RTT đang bận được phép lớn lên khi còn bộ nhớ, nhưng không được giữ 20 MiB mãi mãi, và hàng nghìn kết nối không thể nhân mù quáng mức maximum đó cho đến khi host cạn RAM.
Các tinh chỉnh khác
Với BDP mục tiêu và các ranh giới bộ nhớ đã rõ, chúng tôi nhận ra tuning không chỉ là đẩy các mức maximum. Mỗi tham số trên toàn stack đều có vai trò riêng.
- Các mức minimum (4096 byte): cả receive lẫn transmit minimum đều ghim ở 4 KiB, khớp đúng một memory page trên kiến trúc Linux x86_64 chuẩn. Không có lý do gì để đặt nhỏ hơn đơn vị cấp phát cơ bản của kernel.
- Các mức default (128 KiB cho rmem, 256 KiB cho wmem): đây là kích thước buffer ban đầu một socket khởi đầu sau khi bắt tay 3 bước xong, và autotuning lớn dần từ đó về phía maximum. Ở 128–256 KiB, một kết nối mới có đủ chỗ cho vài vòng slow-start đầu tiên mà không bị kẹt vì buffer, đồng thời đủ gọn để hàng nghìn kết nối rảnh/ít lưu lượng không ôm bộ nhớ. Default của
rmemgiữ nguyên mặc định kernel, cònwmemchúng tôi nâng lên từ 16 KiB. - Chỉnh
tcp_wmemđồng bộ: HAProxy là reverse proxy layer-7 đầy đủ, kết thúc luồng client ở một phía và duy trì một kết nối riêng tới backend ở phía kia. Lưu lượng không chỉ đi vào load balancer; nó còn phải được chuyển ngược về client qua đúng đường high-latency (~52 ms RTT). Chỉ chỉnhtcp_rmemsẽ chỉ sửa chiều vào, để chiều phản hồi ra bị bóp ở trần ~700 Mbps cũ. Cả hai phía của ống đều cần headroom tương xứng. - Ghép BBR với qdisc
fq: đặt packet scheduler mặc định là Fair Queue (fq), giúp “nhịp” (pace) các gói ở tốc độ BBR tính toán thay vì xả theo burst. BBR phụ thuộc vào pacing này để giữ ống đầy mà không vượt quá, nên hai thứ được cấu hình cùng nhau.
Thiết lập kernel mới
sudo sysctl -w net.ipv4.tcp_rmem="4096 131072 20971520"
sudo sysctl -w net.ipv4.tcp_wmem="4096 262144 20971520"
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr
sudo sysctl -w net.core.default_qdisc=fqKết quả throughput
| Đường test | Bandwidth | TCP Retransmissions |
|---|---|---|
| VM → LB → VM (LB cũ) | 698 Mbits/sec (~0.70 Gbps) | 54 |
| VM → LB → VM (LB mới) | 3.42 Gbits/sec | 1,454 |
| VM → VM (baseline trực tiếp) | 3.88 Gbits/sec | 275,092 |
Kết quả tự nó nói lên tất cả. Sau khi tuning, một luồng TCP đơn xuyên vùng qua LB nhảy từ ~700 Mbps lên 3,42 Gbps — cải thiện gần 5× và tiến sát baseline trực tiếp 3,88 Gbps. Quan trọng hơn, nó đạt throughput đó với chỉ 1.454 lần retransmit, so với hơn 275.000 trên đường trực tiếp được tuning theo kiểu “hung hãn”. Không tệ cho một vấn đề khởi đầu từ một buffer 4 MiB đáng ngờ.
Kiểm thử dưới tải đồng thời với Vegeta
Test 1: Baseline vs. gói LB nhỏ nhất (2.000 req/s, trong 10 giây)
| Cấu hình | Tải mục tiêu | Throughput (req/s) | Tỉ lệ thành công | Độ trễ TB | p99 | Max |
|---|---|---|---|---|---|---|
| Direct to Backend (BE) | 2,000 req/s | 1,995.78 | 100.00% | 22.38 ms | 26.82 ms | 256.03 ms |
| Via LB (flavor nhỏ nhất) | 2,000 req/s | 1,995.47 | 100.00% | 22.75 ms | 33.14 ms | 247.80 ms |
Test 2: Stress test gói LB lớn nhất vs. backend (concurrency cao)
| Tải mục tiêu | Endpoint | Rate thực (req/s) | Tỉ lệ thành công | Số HTTP 500 | Độ trễ TB |
|---|---|---|---|---|---|
| 10,000 | LB (lớn nhất) | 10,010 | 100.00% | 0 | 33.9 ms |
| Direct BE | 10,010 | 99.999% | 1 | 35.2 ms | |
| 20,000 | LB (lớn nhất) | 20,046 | 99.996% | 8 | 57.2 ms |
| Direct BE | 20,037 | 99.998% | 4 | 46.0 ms | |
| 30,000 | LB (lớn nhất) | 29,937 | 99.997% | 8 | 121.1 ms |
| Direct BE | 29,964 | 99.999% | 4 | 127.1 ms | |
| 40,000 | LB (lớn nhất) | 32,358 | 99.993% | 22 | 228.2 ms |
| Direct BE | 34,760 | 99.997% | 9 | 212.9 ms | |
| 50,000 | LB (lớn nhất) | 33,028 | 99.994% | 21 | 221.0 ms |
| Direct BE | 31,342 | 99.996% | 13 | 232.3 ms |
Điểm mấu chốt
Ở 2.000 request/giây, gói LB nhỏ nhất gần như không khác gì đi thẳng vào backend: tỉ lệ thành công 100% ở cả hai, độ trễ trung bình qua LB chỉ tăng 0,37 ms.
Ở 10k, 20k và 30k req/s, gói LB lớn nhất tiếp tục bám sát backend trực tiếp. Khi đẩy mục tiêu vượt 40k req/s, cả hai đường đều bắt đầu plateau ở khoảng 32k–35k req/s như nhau — cho thấy nút thắt nằm ở backend và hạ tầng test bên dưới chứ không phải bản thân load balancer, vốn chỉ thêm overhead không đáng kể trong khi duy trì throughput gần như y hệt và tỉ lệ lỗi dưới 0,01% cho tới điểm bão hòa.
Đó đúng là kết quả chúng tôi muốn thấy. Các thiết lập TCP mới làm từng kết nối nhanh hơn, nhưng không biến LB thành một nút thắt concurrency mới. Dưới tải nặng, đường qua proxy và đường trực tiếp rốt cuộc chạm cùng một trần ở cấp hệ thống.
Kết luận
Mục tiêu của công việc này là cải thiện lưu lượng đơn luồng xuyên vùng qua Load Balancer, mà không hy sinh độ trễ hay sự ổn định dưới tải đồng thời.
Và chúng tôi đã làm được. Đôi khi, tất cả những gì cần làm chỉ là cho TCP thêm một chút không gian để thở.