mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
4902 字
14 分钟
TCP流控与拥塞控制:从滑动窗口到BBR
2022-06-29

TCP连接管理 中,看到了 TCP 如何通过三次握手建立连接、四次挥手释放连接,以及 TIME_WAIT、SYN Flood 等连接层面的工程问题。连接建立只是起点——真正的问题在于:连接建立之后,数据该怎么传?

发太快,接收方处理不过来,数据丢失;发太快,路由器队列溢出,网络崩溃;发太慢,带宽浪费,延迟增加。TCP 需要一套精巧的”交通管制”系统:流控(flow control)保护接收方不被淹没,拥塞控制(congestion control)保护网络不被压垮。两者协同工作,决定了 TCP 连接上数据传输的速率与节奏。

本章从 1986 年的互联网拥塞崩溃事件出发,先看滑动窗口机制如何实现流控,再深入慢启动、拥塞避免、快速重传与快速恢复的完整算法,最后追踪从 Reno 到 Cubic 再到 BBR 的演进脉络,理解拥塞控制如何从”丢包驱动”走向”模型驱动”。这些知识不仅是理解 TCP 性能的关键,也是理解 QUIC与HTTP/3 为何要重新设计传输层的基础——QUIC 的拥塞控制正是站在 TCP 的肩膀上,解决了 TCP 难以逾越的架构限制。

一、为什么需要流控和拥塞控制#

1.1 两种不同的”太快”#

TCP 发送方面临两个独立的速率约束:

  • 接收方约束:接收方的处理速度有限,缓冲区有限。如果发送方发送速率超过接收方的处理能力,数据会被丢弃。这是流控要解决的问题。
  • 网络约束:网络中间的路由器缓冲区有限。如果所有发送方的总速率超过链路带宽,路由器队列溢出,数据包被丢弃。这是拥塞控制要解决的问题。

两者看似相似——都是”发太快导致丢包”——但本质不同:流控是端到端的问题,只需要发送方和接收方协调;拥塞控制是全局问题,需要所有发送方共同协作,而网络中间设备不会直接告诉发送方”我拥塞了”。

1.2 1986 年的拥塞崩溃#

1986 年 10 月,互联网发生了第一次有记录的拥塞崩溃(congestion collapse)。从 Lawrence Berkeley Laboratory 到 UC Berkeley 的吞吐量从正常的 32 Kbps 暴跌到 40 bps——下降了近 1000 倍。

原因令人深思:当路由器开始丢包时,TCP 发送方会超时重传。但当时的 TCP 没有拥塞控制机制——重传的数据包与原始数据包竞争同一拥塞链路,导致更多丢包、更多重传,形成恶性循环。网络中充斥着重传数据包,有效吞吐量趋近于零。

Note

拥塞崩溃的核心特征是:网络负载增加,但有效吞吐量反而下降。这不是简单的”变慢”,而是系统进入了一个正反馈的恶化循环——越拥塞越重传,越重传越拥塞。

这次事件直接催生了 Van Jacobson 在 1988 年提出的 TCP 拥塞控制算法(即 TCP Tahoe),奠定了现代拥塞控制的基础。

1.3 流控与拥塞控制的分工#

维度流控(Flow Control)拥塞控制(Congestion Control)
保护对象接收方网络中间设备
信息来源接收方显式通告(rwnd)发送方隐式推断(丢包、延迟)
作用范围单条连接所有共享路径的连接
窗口变量rwnd(接收窗口)cwnd(拥塞窗口)
生效窗口min(rwnd, cwnd)min(rwnd, cwnd)

发送方实际使用的发送窗口是 rwnd 和 cwnd 中的较小值:发送窗口 = min(rwnd, cwnd)。流控说”接收方还能接收多少”,拥塞控制说”网络还能承受多少”,取两者中更保守的限制。

二、滑动窗口与流控#

2.1 接收窗口的工作原理#

TCP 使用滑动窗口(sliding window)机制实现流控。接收方在每次 ACK 中携带接收窗口(rwnd)字段,告诉发送方”我还有多少缓冲区可用”。

flowchart LR subgraph 发送方 direction TB S1["已发送已确认"] S2["已发送未确认"] S3["可发送<br/>(发送窗口内)"] S4["不可发送<br/>(窗口外)"] end subgraph 接收方 direction TB R1["已接收已确认"] R2["已接收未确认<br/>(等待应用读取)"] R3["可接收<br/>(接收窗口内)"] R4["不可接收"] end S1 ---|"ACK + rwnd"| R1 S2 ---|"数据在途"| R2 S3 ---|"待发送"| R3 style S2 fill:#fff9c4,stroke:#f9a825 style S3 fill:#c8e6c9,stroke:#2e7d32 style S4 fill:#ffcdd2,stroke:#c62828 style R2 fill:#fff9c4,stroke:#f9a825 style R3 fill:#c8e6c9,stroke:#2e7d32 style R4 fill:#ffcdd2,stroke:#c62828

滑动窗口的核心逻辑:

  1. 发送方收到 ACK 后,窗口左沿右移(已确认的数据释放)
  2. 接收方应用读取数据后,rwnd 增大,窗口右沿右移
  3. 发送方发送数据后,可用窗口缩小
  4. 发送方不能发送超过 min(rwnd, cwnd) 的数据

2.2 零窗口探测#

当接收方缓冲区满时,rwnd = 0,发送方必须停止发送。但接收方缓冲区释放后如何通知发送方?TCP 设计了零窗口探测(Zero Window Probe)机制:

# Linux 零窗口探测相关参数
sysctl net.ipv4.tcp_keepalive_time # 7200s,保活探测间隔
sysctl net.ipv4.tcp_keepalive_intvl # 75s,探测间隔
sysctl net.ipv4.tcp_keepalive_probes # 9,探测次数
# 零窗口探测专用参数
sysctl net.ipv4.tcp_retries2 # 15,数据重传最大次数

发送方在收到 rwnd = 0 的 ACK 后,启动持续计时器(persist timer)。计时器超时后,发送方发送一个 1 字节的探测数据段,触发接收方重新通告窗口。如果 rwnd 仍为 0,重新启动计时器,且计时器指数退避。

2.3 糊涂窗口综合征#

糊涂窗口综合征(Silly Window Syndrome, SWS)是流控中一个经典的效率问题:当接收方缓冲区快满时,应用每次只读取少量数据,导致 rwnd 只有一小段可用空间;发送方立即发送小数据段填充这段空间,结果网络中充斥着低效的小包。

解决方案是双端的:

  • 接收方:不立即通告小窗口,等到窗口达到合理大小(MSS 或缓冲区一半)再通告(Clark 方案)
  • 发送方:不立即发送小数据段,等到积累足够数据再发送(Nagle 算法)
# Nagle 算法默认开启,可关闭(低延迟场景如游戏、SSH)
sysctl net.ipv4.tcp_nodelay # 0=开启Nagle, 1=关闭
# 延迟 ACK(与 Nagle 交互可能产生额外延迟)
sysctl net.ipv4.tcp_delack_min # 40ms,延迟ACK最小超时
sysctl net.ipv4.tcp_ato_min # 200ms,延迟ACK自适应最小值
Warning

Nagle 算法和延迟 ACK 同时开启时可能产生”死锁”:发送方等更多数据才发(Nagle),接收方等数据才回 ACK(延迟 ACK),双方都在等对方。这是 TCP 编程中经典的延迟陷阱,交互式应用应关闭 Nagle(设置 TCP_NODELAY)。

三、慢启动与拥塞避免#

3.1 拥塞窗口的增长曲线#

TCP 拥塞控制的核心状态变量是拥塞窗口(cwnd),它决定了发送方在未收到 ACK 之前能向网络注入多少数据。cwnd 的增长分为两个阶段:

xychart-beta title "cwnd 随时间增长曲线" x-axis "传输轮次(RTT)" 0 --> 16 y-axis "cwnd(MSS)" 0 --> 70 line "慢启动(指数增长)" [1, 2, 4, 8, 16, 32, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42]
  • 慢启动(Slow Start):cwnd 从 1 MSS 开始,每收到一个 ACK,cwnd 增加 1 MSS。一个 RTT 内所有段被确认后,cwnd 翻倍——指数增长。名称”慢启动”是相对于”一开始就发送整个窗口”而言,并非增长速度慢。
  • 拥塞避免(Congestion Avoidance):当 cwnd 达到慢启动阈值(ssthresh)后,切换为线性增长——每个 RTT cwnd 增加 1 MSS。
Tip

慢启动的”慢”是历史命名,容易产生误解。实际上慢启动阶段 cwnd 指数增长,速度远快于拥塞避免的线性增长。真正的”慢”是相对于”无控制地一次性发送全部数据”而言——TCP 选择从 1 MSS 开始逐步探测,而非冒进地占满带宽。

3.2 ssthresh 的作用#

ssthresh 是慢启动和拥塞避免的分界线:

事件cwnd 变化ssthresh 变化
连接建立cwnd = initcwnd(默认 10 MSS)ssthresh = ∞
cwnd < ssthresh慢启动(指数增长)不变
cwnd ≥ ssthresh拥塞避免(线性增长)不变
超时丢包cwnd = 1 MSSssthresh = cwnd / 2
3 次重复 ACKcwnd = ssthresh + 3 MSSssthresh = cwnd / 2
# Linux 拥塞控制相关参数
sysctl net.ipv4.tcp_init_cwnd # 初始拥塞窗口(默认10)
sysctl net.ipv4.tcp_no_metrics_save # 0=缓存上次cwnd, 1=不缓存
# 查看当前连接的 cwnd 和 ssthresh
ss -ti dst 10.0.0.1
# 输出示例:
# cubic cwnd:32 ssthresh:24 send 45.2Mbps ...

3.3 初始拥塞窗口的演进#

初始拥塞窗口(initcwnd)的值经历了重要变化:

  • RFC 2581(1999):initcwnd = 1 MSS
  • RFC 3390(2002):initcwnd = min(4 MSS, 4380 字节)
  • RFC 6928(2013):initcwnd = 10 MSS

从 1 MSS 到 10 MSS 的提升,反映了对现代网络的认知:当今互联网的带宽延迟积远大于早期,1 MSS 的初始窗口导致短连接在慢启动阶段就结束了,永远无法充分利用带宽。

四、快速重传与快速恢复#

4.1 三个重复 ACK 的含义#

超时重传的代价很大——RTO 通常在数百毫秒到秒级,期间连接完全空闲。TCP 设计了快速重传(Fast Retransmit)机制,用重复 ACK 来检测丢包,避免等待超时。

当接收方收到乱序数据段时,会立即发送对最后一个按序字节的 ACK。如果段 3 丢失,段 4、5、6 到达时接收方都会发送对段 2 的 ACK——产生三个重复 ACK。发送方收到三个重复 ACK 后,立即重传丢失的段,无需等待超时。

sequenceDiagram participant S as 发送方 participant R as 接收方 S->>R: Seq=1 (数据段1) R-->>S: ACK=2 S->>R: Seq=2 (数据段2) R-->>S: ACK=3 S-xR: Seq=3 (数据段3 丢失!) S->>R: Seq=4 (数据段4) R-->>S: ACK=3 (重复ACK #1) S->>R: Seq=5 (数据段5) R-->>S: ACK=3 (重复ACK #2) S->>R: Seq=6 (数据段6) R-->>S: ACK=3 (重复ACK #3) Note over S: 收到3个重复ACK<br/>触发快速重传! S->>R: Seq=3 (重传数据段3) R-->>S: ACK=7 (确认3-6全部收到)

4.2 快速恢复算法#

快速重传之后,发送方进入快速恢复(Fast Recovery)阶段,而非回到慢启动:

  1. 设置 ssthresh = cwnd / 2
  2. 设置 cwnd = ssthresh + 3 MSS(3 MSS 对应 3 个重复 ACK 已离开网络)
  3. 每收到一个重复 ACK,cwnd 增加 1 MSS(允许发送新数据)
  4. 收到新数据的 ACK 时,cwnd = ssthresh,进入拥塞避免

快速恢复的核心思想:三个重复 ACK 说明网络仍在传递数据(只是丢了一个包),所以不需要像超时那样激进地降低发送速率。

4.3 Reno 与 NewReno 的区别#

特性TCP RenoTCP NewReno
快速恢复退出条件收到首个新 ACK收到确认所有未确认数据的 ACK
单窗口多丢包只能恢复一个丢包,其余等超时可在一次快速恢复中恢复多个丢包
部分 ACK 处理视为新 ACK,退出快速恢复视为重复 ACK,继续快速恢复
典型场景单个丢包恢复效率高多个丢包恢复效率显著提升

NewReno 的改进看似微小,但在高延迟、高丢包率网络中效果显著。考虑一个窗口内丢失两个数据段的情况:Reno 只能恢复第一个,第二个必须等超时重传;NewReno 可以在一次快速恢复中依次恢复两个丢包,避免数百毫秒的等待。

# Linux 默认使用 NewReno 的改进版(内置于各拥塞控制算法)
sysctl net.ipv4.tcp_recovery # 快速恢复模式
# 查看当前拥塞控制算法
sysctl net.ipv4.tcp_congestion_control
# 输出:tcp_congestion_control = cubic

五、Cubic与BBR#

5.1 Cubic:丢包驱动的巅峰#

Cubic 是 Linux 内核 2.6.19 以来的默认拥塞控制算法,也是目前互联网上部署最广泛的算法。它的核心改进是让 cwnd 的增长与 RTT 无关——之前的算法(如 Reno、BIC)在长 RTT 链路上增长过慢。

Cubic 的窗口增长函数:

W(t)=C×(tK)3+WmaxW(t) = C \times (t - K)^3 + W_{max}

其中:

  • C=0.4C = 0.4(默认 Cubic 参数)
  • WmaxW_{max} 是上次丢包时的 cwnd
  • K=Wmax/C3K = \sqrt[3]{W_{max} / C} 是窗口增长到 WmaxW_{max} 所需的时间
  • tt 是距上次丢包的时间

Cubic 的增长曲线呈三次函数形状:丢包后先快速增长(凹函数阶段),接近 WmaxW_{max} 时减速(凸函数阶段),超过 WmaxW_{max} 后缓慢探索(凹函数阶段),直到再次丢包。

# Cubic 参数调整
sysctl net.ipv4.tcp_cubic_beta # 0.7,乘法减少因子
sysctl net.ipv4.tcp_cubic_fast_converge # 1,快速收敛开关
# 切换到 Cubic 算法
sysctl -w net.ipv4.tcp_congestion_control=cubic

5.2 BBR:模型驱动的革命#

BBR(Bottleneck Bandwidth and Round-trip propagation time)由 Google 的 Neal Cardwell 等人在 2016 年提出,彻底改变了拥塞控制的范式:不再以丢包作为拥塞信号,而是通过显式建模网络的瓶颈带宽和最小 RTT 来控制发送速率。

BBR 的两个核心参数:

  • BtlBw(Bottleneck Bandwidth):路径瓶颈带宽,通过测量交付速率的最大值估计
  • RTprop(Round-trip propagation time):路径最小 RTT,通过测量 RTT 的最小值估计

BBR 的发送速率由这两个参数决定:发送速率 = BtlBw在途数据量 = BtlBw × RTprop(即带宽延迟积 BDP)。

stateDiagram-v2 [*] --> Startup Startup --> Drain: BtlBw不再增长 Drain --> ProbeBW: 排空队列 ProbeBW --> ProbeBW: 周期性探测带宽 ProbeBW --> ProbeRTT: RTProp过期 ProbeRTT --> ProbeBW: 探测完成 Startup --> ProbeRTT: RTProp过期

BBR 的四个状态:

  1. Startup:类似慢启动,以 2x 速率增长,直到交付速率不再增长(发现 BtlBw)
  2. Drain:排空 Startup 阶段积累的队列,将发送速率降至 BtlBw
  3. ProbeBW:稳态阶段,以 BtlBw 发送,周期性地以 1.25x 速率探测更高带宽
  4. ProbeRTT:定期降低发送速率(4 个 MSS),探测更小的 RTprop

5.3 Cubic 与 BBR 的对比#

维度CubicBBR
拥塞信号丢包带宽和 RTT 模型
增长函数W(t) = C(t-K)³ + Wmax基于模型计算发送速率
缓冲区占用尽量填满瓶颈缓冲区尽量不产生队列
有损网络表现丢包即减窗,吞吐量骤降丢包不减窗,吞吐量稳定
高带宽长肥网络增长缓慢,难以利用带宽快速探测瓶颈带宽
公平性与 Reno/Cubic 公平共存可能抢占 Cubic 带宽
RTT 公平性长 RTT 增长更慢与 RTT 无关
Linux 默认2.6.19 起需手动启用
适用场景一般互联网高带宽、有损链路(如无线)
# 启用 BBR(需要内核 4.9+)
echo "bbr" | sudo tee /etc/sysctl.d/99-bbr.conf
sysctl -p /etc/sysctl.d/99-bbr.conf
# 验证 BBR 已启用
sysctl net.ipv4.tcp_congestion_control
sysctl net.ipv4.tcp_available_congestion_control
# 输出应包含 bbr
# 查看当前连接使用的拥塞控制算法
ss -ti | grep -E "cubic|bbr"

六、拥塞控制的现实影响#

6.1 慢启动对短流的影响#

互联网上大多数 TCP 连接是短流——一个网页请求可能只有几十 KB 数据。慢启动的指数增长意味着短流往往在慢启动阶段就结束了,永远无法达到可用带宽。

假设路径带宽 100 Mbps、RTT 50ms、MSS 1460 字节:

  • 带宽延迟积 BDP = 100 Mbps × 50ms = 625 KB ≈ 428 MSS
  • 从 initcwnd = 10 MSS 开始,需要约 6 个 RTT 才能超过 BDP
  • 6 个 RTT = 300ms,在此期间传输的数据量约 10+20+40+80+160+320 = 630 MSS ≈ 920 KB

对于 100 KB 以下的短流,慢启动阶段就结束了,带宽利用率极低。这正是 QUIC与HTTP/3 引入 0-RTT 技术的动机之一——复用之前连接的拥塞信息,跳过慢启动。

6.2 TCP 公平性#

TCP 拥塞控制的一个隐含目标是公平性——所有 TCP 连接应公平共享瓶颈链路。Cubic 的 AIMD(加法增乘法减)策略天然趋向公平:增窗时线性增长,减窗时减半,多条连接最终收敛到公平分配。

但公平性并非总是成立:

  • 不同 RTT 的连接:RTT 短的连接 ACK 回得快,cwnd 增长更快,占据更多带宽
  • TCP vs UDP:UDP 不做拥塞控制,可能抢占 TCP 带宽
  • BBR vs Cubic:BBR 不以丢包减窗,在有缓冲区的链路上可能比 Cubic 多占 2-3 倍带宽
  • 多路径:MPTCP 子流共享瓶颈时需要特殊处理

6.3 缓冲区膨胀#

缓冲区膨胀(Bufferbloat)是现代网络中一个被忽视的问题:路由器和交换机的缓冲区越来越大,数据包在队列中等待时间过长,导致 RTT 飙升。

Cubic 的”填满缓冲区”策略加剧了 Bufferbloat:cwnd 持续增长直到丢包,而大缓冲区意味着丢包发生时队列已经很长,RTT 可能从 50ms 膨胀到 500ms 甚至更高。交互式应用(SSH、游戏、视频会议)的延迟因此急剧恶化。

BBR 的设计天然对抗 Bufferbloat:它不依赖丢包信号,而是通过 RTprop 估计最小 RTT,尽量不产生队列。但 BBR v1 在与 Cubic 共存时可能过度占带宽,BBR v2/v3 做了改进。

6.4 显式拥塞通知#

ECN(Explicit Congestion Notification,RFC 3168)让路由器在队列快满时标记数据包(而非丢弃),发送方看到标记后降低发送速率,避免丢包和重传。

# 启用 ECN
sysctl net.ipv4.tcp_ecn # 0=禁用, 1=启用, 2=仅入向
# 查看当前 ECN 状态
sysctl net.ipv4.tcp_ecn
# Wireshark 过滤 ECN 标记的包
# tcp.flags.ecn == 1 或 ip.tos.ecn == 0x01

ECN 使用 IP 头部的 TOS 字段中的 2 位:00 = 不支持 ECN,01/10 = 支持 ECN,11 = 拥塞经历(CE)。TCP 头部也有 ECE 和 CWR 标志用于协商和通告。

七、动手实践:观察拥塞窗口变化#

7.1 用 iperf3 测试带宽#

# 服务端
iperf3 -s -p 5201
# 客户端:基本带宽测试
iperf3 -c server_ip -p 5201 -t 30
# 客户端:指定拥塞控制算法
iperf3 -c server_ip -p 5201 -t 30 -C bbr
# 客户端:输出 JSON 格式(便于脚本分析)
iperf3 -c server_ip -p 5201 -t 30 -J > result.json

7.2 用 ss 观察拥塞窗口#

# 查看所有 TCP 连接的拥塞控制信息
ss -ti
# 过滤特定连接
ss -ti dst 10.0.0.1
# 输出字段解读:
# cwnd = 拥塞窗口
# ssthresh = 慢启动阈值
# rtt = 往返时延
# rttvar = RTT 方差
# bbr = BBR 参数(bw, mrtt)
# send = 发送速率估计
# 持续监控(每秒刷新)
watch -n 1 'ss -ti dst 10.0.0.1'

7.3 用 tcp_probe 追踪 cwnd 变化#

# 加载 tcp_probe 内核模块
sudo modprobe tcp_probe
sudo chmod 444 /proc/net/tcpprobe
# 监控特定端口的 cwnd 变化
sudo cat /proc/net/tcpprobe > /tmp/cwnd_trace.log &
CAT_PID=$!
# 运行 iperf3 测试
iperf3 -c server_ip -p 5201 -t 10
# 停止监控
kill $CAT_PID
# 分析 cwnd 变化
# 日志格式:timestamp src dst sport dport cwnd ssthresh
awk '{print $1, $6, $7}' /tmp/cwnd_trace.log | head -20

7.4 Wireshark TCP 流图#

Wireshark 提供了强大的 TCP 流可视化功能:

# 抓取 TCP 流量
sudo tcpdump -i eth0 tcp port 5201 -w tcp_congestion.pcap
# 在 Wireshark 中打开后:
# 1. Statistics → TCP Stream Graphs → Round Trip Time Graph
# 2. Statistics → TCP Stream Graphs → Throughput Graph
# 3. Statistics → TCP Stream Graphs → Window Scaling Graph
# 4. Statistics → TCP Stream Graphs → Time-Sequence (Stevens)
# Wireshark 显示过滤器
tcp.analysis.retransmission # 重传数据段
tcp.analysis.duplicate_ack # 重复 ACK
tcp.analysis.fast_retransmission # 快速重传
tcp.window_size == 0 # 零窗口
tcp.flags.ecn == 1 # ECN 标记

7.5 Python 模拟拥塞窗口变化#

"""模拟 TCP Reno 拥塞窗口变化"""
import matplotlib.pyplot as plt
def simulate_tcp_reno(max_rtt=50, loss_rtt=20, mss=1460):
"""模拟 TCP Reno 的 cwnd 变化"""
cwnd = 1 # 初始 cwnd(MSS)
ssthresh = float('inf')
cwnd_trace = []
for rtt in range(max_rtt):
cwnd_trace.append(cwnd)
if cwnd < ssthresh:
# 慢启动:指数增长
cwnd *= 2
else:
# 拥塞避免:线性增长
cwnd += 1
# 模拟丢包事件
if rtt == loss_rtt:
ssthresh = max(cwnd // 2, 2)
cwnd = 1 # 超时丢包,回到慢启动
return cwnd_trace
def simulate_tcp_newreno(max_rtt=50, loss_rtt=20):
"""模拟 TCP NewReno 的 cwnd 变化"""
cwnd = 1
ssthresh = float('inf')
cwnd_trace = []
fast_recovery = False
for rtt in range(max_rtt):
cwnd_trace.append(cwnd)
if fast_recovery:
# 快速恢复中
cwnd += 1 # 收到重复 ACK
if cwnd >= ssthresh + 3:
fast_recovery = False
cwnd = ssthresh
elif cwnd < ssthresh:
cwnd *= 2
else:
cwnd += 1
if rtt == loss_rtt:
ssthresh = max(cwnd // 2, 2)
cwnd = ssthresh + 3 # 快速恢复
fast_recovery = True
return cwnd_trace
# 运行模拟
reno = simulate_tcp_reno()
newreno = simulate_tcp_newreno()
# 绘图
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(reno, label='TCP Reno', marker='o', markersize=3)
ax.plot(newreno, label='TCP NewReno', marker='s', markersize=3)
ax.set_xlabel('RTT 轮次')
ax.set_ylabel('cwnd (MSS)')
ax.set_title('TCP Reno vs NewReno 拥塞窗口变化')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('tcp_cwnd_simulation.png', dpi=150)
print("模拟完成,图表已保存")

八、本章小结#

8.1 拥塞控制算法演进总览#

算法年份核心机制拥塞信号优势局限
Tahoe1988慢启动 + 拥塞避免超时/重复ACK首个拥塞控制3重复ACK也回到慢启动
Reno1990+快速重传/恢复3重复ACK避免超时等待单窗口多丢包恢复差
NewReno1999+部分ACK处理3重复ACK多丢包恢复改善仍依赖丢包信号
Cubic2006三次函数增长丢包RTT无关增长填满缓冲区,有损链路差
BBR2016模型驱动带宽+RTT模型不依赖丢包,低延迟与Cubic公平性问题

8.2 拥塞事件与响应#

拥塞事件检测方式cwnd 响应ssthresh 响应状态转换
超时RTO 超时→ 1 MSS= cwnd/2→ 慢启动
3 重复 ACK重复 ACK 计数= ssthresh + 3= cwnd/2→ 快速恢复
部分 ACK(NewReno)快速恢复中收到+1 MSS不变继续快速恢复
新 ACK(Reno)快速恢复中收到= ssthresh不变→ 拥塞避免
ECN 标记IP 头 CE 位= cwnd × β= cwnd/2→ 拥塞避免

8.3 Linux 拥塞控制 sysctl 参数#

参数默认值说明
net.ipv4.tcp_congestion_controlcubic当前拥塞控制算法
net.ipv4.tcp_init_cwnd10初始拥塞窗口(MSS)
net.ipv4.tcp_no_metrics_save0是否缓存上次 cwnd
net.ipv4.tcp_ecn2ECN 设置(0=禁, 1=启, 2=入向)
net.ipv4.tcp_nodelay0Nagle 算法(0=开启)
net.ipv4.tcp_retries215数据重传最大次数
net.ipv4.tcp_cubic_beta7/10Cubic 乘法减少因子
net.core.default_qdiscfq_codel默认队列调度算法

从滑动窗口的端到端流控,到基于丢包的 Reno/Cubic,再到模型驱动的 BBR,TCP 拥塞控制走过了一条从”被动反应”到”主动建模”的演进之路。但 TCP 的拥塞控制始终受限于一个架构约束:拥塞控制状态与传输层绑定,连接切换路径时状态全部丢失

这正是 QUIC 要解决的核心问题之一。在 QUIC与HTTP/3 中,将看到 QUIC 如何通过连接 ID 实现连接迁移、如何将拥塞控制从内核搬到用户空间实现快速迭代、以及 HTTP/3 如何在 QUIC 之上消除 HTTP/2 的队头阻塞——TCP 拥塞控制的经验与教训,全部沉淀在了 QUIC 的设计之中。


参考#

  • RFC 2581 — TCP Congestion Control
  • RFC 3168 — The Addition of Explicit Congestion Notification (ECN) to IP
  • RFC 3390 — Increasing TCP’s Initial Window
  • RFC 6928 — Increasing TCP’s Initial Window to 10 Segments

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

TCP流控与拥塞控制:从滑动窗口到BBR
https://blog.souloss.com/posts/internet-architecture/tcp-flow-and-congestion-control/
作者
Souloss
发布于
2022-06-29
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

相关文章 智能推荐
1
TCP连接管理:三次握手与四次挥手的工程细节
互联网运作 TCP如何建立和释放连接?三次握手为什么不能是两次?四次挥手为什么不能是三次?TIME_WAIT到底在等什么?从报文格式到状态机,从SYN Flood防御到连接异常处理,用抓包实验观察TCP连接的完整生命周期。
2
UDP与传输基础:最简传输协议
互联网运作 数据包到达目的主机后如何交给正确的应用?传输层的角色、UDP报文格式与校验和、端口号与多路复用、UDP的应用场景与局限——从网络层到传输层的跨越。
3
QUIC与HTTP/3:传输层的性能革命
互联网运作 TCP的队头阻塞、握手延迟和连接迁移失败催生了QUIC——一个基于UDP的加密传输协议。0-RTT握手、流级多路复用、连接ID迁移,加上HTTP/3的QPACK头部压缩,传输层迎来了真正的性能革命。
4
系列导读
互联网运作 本系列从物理介质到应用层、从数据包离开网卡到抵达对端,用真实的包级追踪讲述互联网如何运作——物理编码、以太网交换、ARP首跳、IP寻址、NAT中间盒、域内域间路由、BGP安全、运营商骨干网、IXP互联、TCP/UDP/QUIC传输、DNS/TLS安全、HTTP演进、CDN分发、数据中心SDN、无线异构接入,每章配有Wireshark抓包与GNS3/FRR实验,让你从「会上网」进阶到「理解互联网」。
5
网络协议栈
Linux内核 深入解析 Linux 网络协议栈——socket/sock 结构、TCP 连接管理与状态机、sk_buff 数据包表示、NAPI 轮询机制、数据包收发完整路径、Netfilter 框架与 5 个钩子点、路由与邻居子系统、网络命名空间。