Flow control & congestion control — vì sao download tăng tốc dần
Sliding window, window scaling, slow start, congestion avoidance, CUBIC, BBR. Bài này phân biệt rõ hai cơ chế kiểm soát tốc độ của TCP — flow control bảo vệ đầu nhận, congestion control bảo vệ mạng — và giải thích vì sao throughput ramp up dần thay vì tối đa ngay từ đầu.
TL;DR: Download một file lớn, bạn thấy tốc độ tăng dần từ vài KB/s lên hàng MB/s thay vì tối đa ngay — đó là TCP slow start trong congestion control đang chạy. TCP có hai cơ chế kiểm soát tốc độ riêng biệt: flow control (sliding window, receive window) ngăn đầu gửi làm ngộp đầu nhận; congestion control (slow start, congestion avoidance, CUBIC/BBR) ngăn đầu gửi làm nghẽn mạng. Tốc độ thực tế bị giới hạn bởi min(rwnd, cwnd) — nhỏ nhất trong hai cửa sổ. Pitfall quan trọng nhất là bufferbloat: buffer router quá lớn làm TCP tưởng mạng thông nhưng độ trễ tăng cao, ảnh hưởng xấu đến mọi kết nối.
Ở bài 02 bạn thấy TCP đảm bảo dữ liệu đến đủ bằng retransmission. Nhưng có một câu hỏi khác: đầu gửi nên gửi nhanh thế nào? Gửi quá nhanh có hai nguy cơ: làm tràn buffer của đầu nhận (đầu nhận không xử lý kịp), hoặc làm nghẽn các router giữa đường. Hai nguy cơ đó được giải quyết bởi hai cơ chế hoàn toàn khác nhau — và hiểu sự khác biệt là hiểu lý do download của bạn không bao giờ đạt tốc độ tối đa ngay từ giây đầu.
1. Analogy — Vòi nước và đường ống
Hình dung bạn đổ nước từ một thùng lớn qua đường ống vào một bình nhỏ. Có hai ràng buộc:
- Dung tích bình — bình chứa tối đa 2 lít, bạn không thể đổ nhanh hơn tốc độ bình được lấy ra dùng, nếu không nước tràn.
- Đường kính ống — đường ống chỉ cho một lượng nước nhất định qua mỗi giây. Mở van quá mạnh không làm nước chảy nhanh hơn khả năng ống, mà chỉ làm ống vỡ áp hoặc nước bắn ngược.
Hai ràng buộc này độc lập: bình lớn không có nghĩa ống rộng. Thực tế bạn luôn bị giới hạn bởi cái nhỏ hơn trong hai.
| Đổ nước | TCP |
|---|---|
| Dung tích bình nhận | Receive window (rwnd) — bộ nhớ buffer của đầu nhận |
| Tốc độ lấy nước ra dùng | Tốc độ application đọc từ socket |
| Đường kính ống (giới hạn ống) | Congestion window (cwnd) — ước tính bandwidth mạng |
| "Mở van từ từ để kiểm tra ống" | Slow start |
| Thực tế = min(bình, ống) | Tốc độ gửi = min(rwnd, cwnd) |
Flow control hỏi: "đầu nhận còn chỗ không?" → giới hạn bởi rwnd (receive window đầu nhận báo về). Congestion control hỏi: "mạng có nghẽn không?" → giới hạn bởi cwnd (congestion window đầu gửi tự ước tính). Hai cơ chế, hai câu hỏi khác nhau.
2. Flow control — sliding window bảo vệ đầu nhận
Flow control đảm bảo đầu gửi không gửi nhanh hơn tốc độ đầu nhận có thể xử lý. Cơ chế là sliding window (cửa sổ trượt):
Mỗi TCP header có trường window (16-bit) — đầu nhận dùng trường này để quảng bá receive window (rwnd): số byte nó còn sẵn sàng nhận vào buffer. Đầu gửi không được có quá rwnd byte "in flight" (đã gửi nhưng chưa được ACK).
Ví du: rwnd = 65535 byte (64 KB)
Dau gui da gui byte 1-30000, nhan ACK toi byte 20000
=> in-flight = 30000 - 20000 = 10000 byte
=> con co the gui them: 65535 - 10000 = 55535 byte
Khi application đầu nhận đọc dữ liệu từ socket buffer, rwnd tăng lên và được quảng bá qua ACK tiếp theo — đầu gửi "trượt" cửa sổ sang phải để gửi tiếp:
sequenceDiagram
participant S as Sender
participant R as Receiver
Note over R: rwnd = 64KB
S->>R: byte 1-1460 (1460 byte)
S->>R: byte 1461-2920
S->>R: byte 2921-4380
Note over S: da gui 4380 byte in-flight
R-->>S: ack=4381, rwnd=61155
Note over R: app doc 3000 byte, buffer tang
R-->>S: ack=4381, rwnd=64155
S->>R: byte 4381-5840 (cua so truot sang phai)Window scaling — vượt giới hạn 64 KB
Trường window trong TCP header chỉ 16-bit → tối đa 65535 byte (64 KB). Nhưng với đường truyền gigabit và RTT 100ms, để tối đa throughput cần window lớn hơn nhiều (bandwidth-delay product: 1 Gbps × 100ms = 12.5 MB).
Window Scaling (RFC 7323) là TCP option thương lượng trong handshake — đầu nhận quảng bá một scale factor (0-14), nhân với giá trị window để mở rộng lên tới 1 GB:
Window scale = 8 → rwnd thuc = window_field × 2^8 = window_field × 256
window_field = 65535 → rwnd thuc = 16 MB
# Kiem tra window scaling co bat khong (Linux):
sysctl net.ipv4.tcp_window_scaling
# Output: net.ipv4.tcp_window_scaling = 1 (mac dinh bat)
rwnd phản ánh dung lượng còn trống trong receive socket buffer — bộ nhớ kernel cấp cho mỗi socket. Trên Linux mặc định tự điều chỉnh (autotuning) từ 4 KB tới 6 MB. Nếu application đọc chậm (CPU bận), buffer đầy → rwnd về 0 → đầu gửi dừng hoàn toàn, gọi là zero window. Đây là một lý do backend service cần đọc từ socket nhanh, không để dữ liệu chất trong buffer.
3. Congestion control — bảo vệ mạng
Congestion control giải quyết một bài toán khác: đầu gửi không biết bandwidth của mạng là bao nhiêu. Gửi quá nhiều → router giữa đường tràn buffer → mất gói → mọi kết nối qua router đó bị ảnh hưởng. Đây là tài nguyên chung — một sender tham lam làm hại tất cả.
Đầu gửi tự duy trì congestion window (cwnd) — ước tính của nó về "network có thể hấp thu bao nhiêu byte in-flight". cwnd bắt đầu nhỏ và được điều chỉnh liên tục dựa trên phản hồi ngầm từ mạng (mất gói = tín hiệu nghẽn).
3.1 Slow start — tăng tốc nhanh nhưng thận trọng
Khi kết nối mới mở (hoặc sau timeout), TCP không biết gì về mạng. Thay vì gửi cwnd tối đa ngay, nó bắt đầu nhỏ và tăng gấp đôi mỗi RTT cho tới khi gặp ngưỡng ssthresh (slow start threshold):
Khoi dau: cwnd = 10 segments (IW — initial window, RFC 6928)
RTT 1: gui 10 segment, nhan 10 ACK → cwnd = 20
RTT 2: gui 20 segment, nhan 20 ACK → cwnd = 40
RTT 3: cwnd = 80
...
Den khi cwnd >= ssthresh → chuyen sang congestion avoidance
Tăng gấp đôi mỗi RTT nghe có vẻ chậm ("slow") nhưng thực ra là cấp số nhân — từ 10 tới 80 segments chỉ mất 3 RTT (~30-90ms với mạng nội địa). Tên "slow start" đến từ việc nó bắt đầu chậm (khác với "bắt đầu tối đa và back off"), không phải quá trình tăng chậm.
3.2 Congestion avoidance — tăng tuyến tính
Sau khi cwnd vượt ssthresh, TCP chuyển sang congestion avoidance: tăng cwnd tuyến tính — cộng thêm 1 MSS mỗi RTT (thay vì nhân đôi). Điều này giúp TCP "thăm dò" bandwidth còn trống mà không leo thang quá nhanh:
cwnd = ssthresh + 1 moi RTT
3.3 Phản ứng khi mất gói (TCP Reno)
TCP Reno (thuật toán cổ điển, đến nay vẫn được dùng làm baseline) phản ứng khác nhau với hai tín hiệu mất gói:
- 3 duplicate ACK (fast retransmit):
ssthresh = cwnd/2,cwnd = ssthresh. Giảm một nửa — ít nghiêm trọng hơn timeout vì 3 dup ACK nghĩa là mạng vẫn đang chuyển gói (chỉ mất 1 gói), tiếp tục congestion avoidance. - RTO timeout:
ssthresh = cwnd/2,cwnd = IW (10 segments). Về lại slow start từ đầu — nghiêm trọng hơn, mạng có thể tắc nặng.
xychart-beta
title "cwnd theo thoi gian (TCP Reno)"
x-axis ["0","1","2","3","4","5","6","7","8","9","10","11","12"]
y-axis "cwnd (segments)" 0 --> 70
line [10, 20, 40, 60, 65, 66, 67, 33, 34, 35, 36, 37, 38]4. CUBIC và BBR — thuật toán hiện đại
TCP Reno thiết kế cho mạng tốc độ thấp thập niên 1990. Với đường truyền gigabit hiện đại, Reno phục hồi quá chậm sau mất gói. Hai thuật toán hiện đại thay thế:
CUBIC (mặc định Linux từ kernel 2.6.19)
CUBIC thay đường tăng tuyến tính của Reno bằng hàm cubic — tăng nhanh khi xa điểm nghẽn, chậm lại khi gần. Sau khi mất gói và giảm cwnd, CUBIC nhớ giá trị cwnd lúc mất và dùng hàm cubic để phục hồi về điểm đó nhanh hơn Reno nhiều:
# Kiem tra congestion control hien tai (Linux):
sysctl net.ipv4.tcp_congestion_control
# Output: cubic (mac dinh)
CUBIC vẫn dùng mất gói làm tín hiệu nghẽn — cùng triết lý với Reno, chỉ thay đổi tốc độ phục hồi.
BBR — Bottleneck Bandwidth and RTT (Google, 2016)
BBR thay đổi triết lý hoàn toàn. Thay vì "coi mất gói = nghẽn" (loss-based), BBR đo trực tiếp bandwidth và RTT của đường truyền:
- BtlBw (bottleneck bandwidth): tốc độ tối đa mạng có thể hấp thu, ước tính từ tốc độ ACK về.
- RTprop (round-trip propagation): RTT tối thiểu khi mạng không bận, đo theo thời gian.
- BBR đặt cwnd sao cho
in-flight = BtlBw × RTprop— không nhiều hơn, không ít hơn.
Lợi thế: BBR không đợi mất gói để điều chỉnh; nó phản ứng sớm hơn và phục hồi nhanh hơn CUBIC với đường truyền latency cao (satellite, xuyên lục địa). Google triển khai BBR trên toàn bộ server YouTube/GCP và báo cáo throughput tăng đáng kể trên đường truyền có packet loss.
# Tat BBR (Linux):
sysctl -w net.ipv4.tcp_congestion_control=bbr
# Kiem tra:
sysctl net.ipv4.tcp_congestion_control
# Output: bbr
CUBIC ổn định và mặc định trên hầu hết Linux production. BBR tốt hơn cho đường truyền có latency cao hoặc có packet loss không do nghẽn (wireless, satellite). Một số CDN lớn (Cloudflare, Google) dùng BBR mặc định. Nếu không chắc, giữ CUBIC; nếu thấy throughput thấp trên mạng có RTT cao, thử BBR và so sánh qua iperf3.
5. Pitfall — hiểu nhầm thường gặp
❌ Nhầm 1: "Flow control và congestion control là một."
✅ Hai cơ chế, hai vấn đề khác nhau. Flow control (rwnd) hỏi "đầu nhận còn buffer không?" — giới hạn bởi đầu nhận quảng bá. Congestion control (cwnd) hỏi "mạng có chịu được không?" — đầu gửi tự ước tính. Một server có buffer khổng lồ (rwnd lớn) vẫn có thể bị congestion control giới hạn nếu mạng yếu. Tốc độ thực = min(rwnd, cwnd) ÷ RTT.
❌ Nhầm 2: "Mất gói luôn là tín hiệu mạng nghẽn, nên giảm cwnd là đúng." ✅ Mất gói trên mạng không dây (Wi-Fi, 4G) thường do lỗi vô tuyến, không phải nghẽn — nhưng TCP Reno và CUBIC vẫn giảm cwnd như thể nghẽn. Đây là lý do TCP throughput trên Wi-Fi kém hơn lý thuyết. BBR giải quyết một phần bằng cách không coi mất gói là tín hiệu duy nhất.
❌ Nhầm 3: "Buffer router lớn hơn = tốt hơn, giảm mất gói." ✅ Đây là bufferbloat — pitfall nghiêm trọng nhất với congestion control. Buffer router quá lớn không giảm nghẽn; nó làm gói xếp hàng trong buffer hàng trăm ms thay vì bị drop. TCP mất gói rất muộn, điều chỉnh cwnd rất muộn. Trong thời gian đó, latency tăng vọt (hàng trăm ms) ảnh hưởng đến mọi kết nối qua router đó — đặc biệt giao thức realtime (video call, game). AQM (Active Queue Management) như fq_codel trên Linux giải quyết bufferbloat bằng cách drop sớm và thông minh thay vì nhồi buffer.
6. Liên hệ các bài khác
- Bài 02 — TCP reliability: khi mất gói xảy ra, congestion control phản ứng bằng cách giảm cwnd. Hai cơ chế (reliability + congestion control) liên hệ mật thiết — retransmit là triệu chứng, congestion control là phản ứng hệ thống.
- Bài 04 — TIME_WAIT & hết port: kết nối TCP sau khi đóng để lại trạng thái TIME_WAIT — số lượng kết nối ngắn tải cao ảnh hưởng đến tổng throughput vì congestion window phải warm up lại từ đầu mỗi lần.
- Bài 06 — Connection pooling & keep-alive: keep-alive tái dùng kết nối đã warm-up — tránh slow start lại từ đầu mỗi request. Đây là lý do connection pool cải thiện cả latency lẫn throughput, không chỉ tiết kiệm handshake.
- Khoá Foundations — Mô hình TCP/IP 4 tầng: congestion control hoạt động ở tầng Transport — nó không thấy được router bên trong mạng (tầng Internet), chỉ nhận tín hiệu gián tiếp qua mất gói và RTT.
7. 📚 Deep Dive — tài liệu gốc
Đọc khi muốn đi tới tận gốc:
- RFC 5681 — TCP Congestion Control — định nghĩa slow start, congestion avoidance, fast retransmit, fast recovery (TCP Reno). Đây là spec chuẩn cho congestion control cổ điển.
- RFC 9293 — Transmission Control Protocol — mục 3.7 (Managing the Window) mô tả flow control và receive window.
- RFC 7323 — TCP Extensions for High Performance — window scaling, timestamps cho RTTM và PAWS. Đây là nơi window scale factor được phát biểu chính thức.
- BBR Congestion Control — Google 2016 — paper gốc của Cardwell et al. giải thích triết lý bandwidth-delay product và so sánh với CUBIC trên YouTube traffic thực tế.
Ghi chú: RFC 5681 là tài liệu tra cứu khi cần hiểu chính xác thứ tự trạng thái slow start vs congestion avoidance. Paper BBR là đọc thêm nên có — giải thích bằng số liệu thực nghiệm, dễ đọc hơn RFC.
8. Tóm tắt
- Flow control (
rwnd, sliding window) bảo vệ đầu nhận: đầu gửi không được có quárwndbyte in-flight.rwndđược quảng bá qua mỗi ACK; window scaling mở rộng lên tới 1 GB. - Congestion control (
cwnd) bảo vệ mạng: đầu gửi tự ước tính bandwidth mạng và điều chỉnhcwndliên tục. - Tốc độ thực tế =
min(rwnd, cwnd)÷ RTT. - Slow start: cwnd bắt đầu từ IW (10 segments), tăng gấp đôi mỗi RTT cho tới
ssthresh. Đây là lý do throughput ramp up chứ không tối đa ngay. - Congestion avoidance: sau
ssthresh, tăng tuyến tính +1 MSS/RTT để thăm dò bandwidth còn trống. - Mất gói (3 dup ACK): giảm
cwndmột nửa, tiếp tục avoidance. Timeout: về slow start từ đầu. - CUBIC (mặc định Linux) dùng hàm cubic để phục hồi nhanh hơn Reno. BBR (Google) đo bandwidth+RTT trực tiếp thay vì coi mất gói = nghẽn.
- Bufferbloat: buffer router quá lớn không giúp throughput, chỉ làm latency tăng vọt — giải quyết bằng AQM (fq_codel).
9. Tự kiểm tra
Q1Flow control và congestion control khác nhau ở điểm gì? Cái nào bảo vệ đầu nhận, cái nào bảo vệ mạng?▸
rwnd — receive window) bảo vệ đầu nhận: đầu nhận quảng bá lượng buffer còn trống trong mỗi ACK, đầu gửi không được gửi vượt quá số đó. Nếu application đọc chậm, buffer đầy, rwnd về 0 → đầu gửi dừng hoàn toàn (zero window). Congestion control (cwnd) bảo vệ mạng: đầu gửi không thấy router giữa đường, chỉ suy đoán nghẽn qua mất gói và RTT, rồi tự điều chỉnh cwnd. Tốc độ thực tế bị giới hạn bởi min(rwnd, cwnd).Q2Vì sao throughput của một kết nối TCP mới tăng dần thay vì đạt tốc độ tối đa ngay?▸
cwnd = 10 segments (khoảng 14 KB) và nhân đôi mỗi RTT cho tới ngưỡng ssthresh. TCP không biết bandwidth của mạng khi kết nối mới mở — gửi tối đa ngay có thể làm nghẽn router và mất gói. Slow start "thăm dò" dần: nếu ACK về đủ, cwnd tăng; nếu gói mất, dừng tăng và giảm. Sau vài RTT mới đạt throughput bình ổn. Đây là tradeoff: an toàn cho mạng đổi lấy thời gian warm up — lý do connection pool giữ kết nối cũ quan trọng hơn bạn nghĩ.Q3TCP Reno phản ứng thế nào khi nhận 3 duplicate ACK? Khác gì với khi xảy ra RTO timeout?▸
ssthresh = cwnd/2, cwnd = ssthresh, chuyển sang congestion avoidance — giảm một nửa nhưng không về slow start. Lý do: 3 dup ACK cho thấy mạng vẫn chuyển được gói (chỉ mất 1 gói), không tắc hoàn toàn. Với RTO timeout: ssthresh = cwnd/2, cwnd = IW (10 segments), về slow start từ đầu — timeout nghiêm trọng hơn, mạng có thể tắc nặng, phải thận trọng hơn nhiều. Phân biệt hai trường hợp này giải thích tại sao timeout làm throughput sụt đột ngột.Q4Vì sao CUBIC cải thiện được throughput so với Reno trên đường truyền tốc độ cao?▸
Q5BBR khác gì CUBIC về triết lý? Khi nào BBR có lợi thế rõ hơn?▸
cwnd = BtlBw × RTprop — không đợi mất gói. Lợi thế của BBR rõ nhất khi: (1) đường truyền có packet loss không do nghẽn (wireless, satellite) — Reno/CUBIC giảm cwnd oan; (2) đường truyền latency cao (xuyên lục địa, satellite) — BBR phục hồi nhanh hơn nhiều. Trên mạng datacenter LAN ổn định, CUBIC và BBR gần tương đương.Q6Bufferbloat là gì? Vì sao buffer router lớn hơn lại gây hại?▸
fq_codel — drop sớm và thông minh để giữ queue ngắn, latency thấp.Q7Window scaling là gì và vì sao cần thiết với đường truyền tốc độ cao?▸
window trong TCP header chỉ 16-bit → tối đa 65535 byte (64 KB). Với đường truyền 1 Gbps và RTT 100ms, để giữ pipeline đầy cần bandwidth-delay product = 1 Gbps × 100ms = 12.5 MB in-flight — vượt xa 64 KB. Window Scaling (RFC 7323) thương lượng trong handshake một scale factor (0-14): rwnd_thực = window_field × 2^scale, mở rộng lên tới 1 GB. Không có window scaling, throughput tối đa của một kết nối TCP trên đường truyền tốc độ cao bị giới hạn ở 64 KB ÷ RTT — ví dụ RTT 100ms thì tối đa 5 Mbps dù đường truyền có 1 Gbps.Bài tiếp theo: TIME_WAIT & hết port — khi cạn ephemeral port
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên