TCP 拥塞控制
TCP 发送方看不到路径容量,只能根据反馈调整发送量。本课展示教材版 Reno:从一个包开始,慢启动时每个往返将窗口翻倍,达到阈值后每次加一,丢包后缩小窗口。图形呈锯齿,窗口不会停在一个已知的“正确速率”。
本例路径每 RTT 排出 8 个包,缓冲区容纳 4 个。发送方只维护 cwnd(允许在途的 MSS 数)和 ssthresh(慢启动阈值),不知道瓶颈容量。三个重复 ACK 表示仍有后续数据到达,本课将窗口减半后继续拥塞避免;超时预设没有 ACK 返回,窗口退到 1,再次慢启动。
TCP 拥塞控制如何工作
每个无丢包 RTT 后,检查 cwnd < ssthresh。
成立时执行慢启动:cwnd = min(cwnd * 2, ssthresh)。真实 TCP 按 ACK 更新;在每个包都得到确认、发满窗口等条件下,窗口约每 RTT 翻倍。延迟 ACK 和字节计数会影响增长。本课把一轮 ACK 合并为一次更新,并在阈值处截断。
达到阈值后进入拥塞避免:cwnd += 1,每 RTT 增加一个 MSS。
模型在 cwnd > C + B 时溢出。C 是每 RTT 排出的包数,在本课固定一 RTT 路径中也代表带宽时延积;B 是缓冲区容量。超过 C 的部分排队,超过 C + B 的部分丢弃。发送方不读取 C,只读取反馈。这里按批次计算容量,再选择重复 ACK 或超时反馈;没有用同一批末尾丢包的位置推导三个重复 ACK。
- 三个重复 ACK,快速重传。
ssthresh = max(cwnd // 2, 2),cwnd = ssthresh,然后继续拥塞避免。真实 Reno 的快速恢复会暂时使用ssthresh + 3,本课省略这段。 - 超时。 重传计时器在所需确认到达前到期。阈值仍按丢失那轮的窗口设为
max(cwnd // 2, 2),cwnd退到 1,重新慢启动。本预设把反馈简化为整轮静默。
AIMD 指加性增大(每 RTT +1)与乘性减小(× ½)。仅减一会让队列仍处于满载附近,再次增长就会溢出;减半为下一轮探测腾出空间。
TCP 拥塞控制逐步演示
默认 Sawtooth:C = 8 包/RTT,B = 4,初始 cwnd = 1、ssthresh = 8。容量条件决定何时丢包。
- 初始化。 管道为空,图上是
(RTT 0, cwnd 1)。ssthresh = 8标为发送方本地状态,C = 8标为发送方不可见。数值相等只是本例选择,不是同一变量。 - 慢启动。 发送并确认 1 个包,
cwnd 1 → 2。 - 慢启动。 发送并确认 2 个包,
2 → 4。 - 慢启动。 发送并确认 4 个包,
4 → 8,在阈值处截断。 - 达到阈值。 后续改用
+1,不再×2。此步不移动数据包。 - 拥塞避免。 发送并确认 8 个包,队列为 0,
8 → 9。 - 拥塞避免。 发送 9 个,队列 1,
9 → 10。 - 拥塞避免。 发送 10 个,队列 2,
10 → 11。 - 拥塞避免。 发送 11 个,队列 3,
11 → 12。 - 拥塞避免。 发送 12 个,队列 4/4,
12 → 13。下一轮会超出容量。 - 溢出。 发送 13 个,12 个容得下,1 个丢弃。展示三个重复 ACK;窗口暂留 13,下一步才缩小。
- 减半。
ssthresh = max(13 // 2, 2) = 6,cwnd = 6。仍处于拥塞避免,图点垂直下降,阈值线从 8 移到 6。 - 拥塞避免。 发送 6 个,
6 → 7。恢复后是加一,不是翻倍;省略快速恢复的临时+3。 - 拥塞避免。 发送 7 个,
7 → 8。 - 拥塞避免。 发送 8 个,
8 → 9,再次经过带宽时延积。 - 第二个锯齿。 合并展示
9 → 10 → 11 → 12 → 13、溢出、退到 6。 - 结束。
cwnd = 6、ssthresh = 6。图中有慢启动上升段和两个拥塞避免锯齿。发送方会继续增加、溢出、减半,而非得到一个精确容量后停止。
No loss 把 B 改为 16,同样增长到 cwnd = 14 时结束,期间没有溢出,也没有缩窗。
Timeout 在发送 13 个包后让整轮反馈消失。RTO 到期,cwnd = 1,再走 1 → 2 → 4 → 6,在新阈值处截断,之后转为拥塞避免。同样的超载,反馈类型不同,缩窗程度也不同。
管道解释当前 RTT 为什么丢包,折线保留各 RTT 的窗口。观察指数增长、线性增长和下降分别对应哪条规则。
窗口为何呈锯齿
理想化的固定路径中,知道 C 的发送方可以把在途量设为 C,但这里没有这个信息。慢启动到窗口 N 需要 Θ(log N) 个 RTT:1, 2, 4, 8 是四批发送窗口。拥塞避免每 RTT 增加一个 MSS,再增加 N 个包需要 Θ(N) 个 RTT,因此锯齿上升段较长。
丢包后的下降让队列腾出空间。若只把 13 减为 12,队列仍为 4/4;发送 12 成功后,窗口又增为 13,下一轮再次溢出。减半提供更大的调整幅度。默认的第二个锯齿用同一组条件验证重复行为。
三个预设对应可识别的窗口变化:
- 线性增长后减半:Reno 式丢包探测,常由三个重复 ACK 触发。
- 退到 1 后重新慢启动:超时或确认未能及时到达。
- 队列增长但窗口尚未下降:缓冲区暂时吸收超载,可能造成缓冲膨胀与排队延迟。
Cubic、BBR 的曲线不同,本课只解释 Reno。
边界情况
- 初始阈值是教学选择。 真实协议可把阈值设得很高,让首次拥塞结束慢启动。本课使用 8,便于在首次丢包前展示两种增长。
- 阈值至少为 2。
max(cwnd // 2, 2)防止整数减半退到零。 - 增长可正好到达阈值。
4在阈值 8 时变为 8;超时后阈值为 6,则4 → 6,然后进入拥塞避免。 - 超时与重复 ACK 不同。 同为窗口 13 的超载,本课前者退到 1,后者保留 6。
- 无丢包期间没有缩窗。 模型会继续增加窗口,但任何有限缓冲区在持续增长下最终仍会溢出。缓冲膨胀表现为更长队列,并不提供无限容量。
常见错误
- 认为慢启动一定持续到首次丢包。 那只适用于初始阈值很大的情况。判定条件是
cwnd < ssthresh;本例在 8 切换,到 13 才丢包。 - 三个重复 ACK 后继续翻倍。 本课 Reno 保留半个窗口并进入拥塞避免。退到小窗口重新慢启动是不同的恢复行为。
- 把 ACK 编号增长当成拥塞控制。 握手中的
ack = seq + 1用于编号确认;cwnd限制在途数据量。 - 把所有丢包叫超时。 重复 ACK 表示接收方仍看到了后续数据,Reno 因而保留半个窗口。
- 从单流锯齿推出无条件公平。 AIMD 在特定共享模型下可趋向公平,多个流的 RTT 等条件也有影响。Chiu–Jain 图才讨论该问题,本课只有一个流。
- 认为发送方已找到 C。 窗口仍在探测,下一次增长还会溢出。
模型省略的内容
本课按 RTT 聚合一个流的 Reno。真实 TCP 按 ACK 更新,初始窗口常大于 1(例如 10 MSS),快速恢复会暂设 cwnd = ssthresh + 3。真实丢包阈值根据 FlightSize 计算,发送量还受接收窗口限制;本例假定发满窗口,使用 cwnd 计算阈值。Cubic、BBR、pacing、延迟 ACK、SACK 和多流公平性均未展开。这里展示增长与退让规律,不实现完整 TCP 栈。