10.1 TCP 拥塞窗口、恢复与 ECN
接收端有空闲缓冲,不代表整条网络路径也有余量。流量控制用接收窗口 rwnd 保护接收端,拥塞控制则根据 ACK、丢包、ECN 和 RTT 等反馈限制发送端施加的负载。本课分别解释窗口、丢包恢复和拥塞反馈,再运行一个简化模型检查阶段切换。
1. cwnd 限制在途的未确认数据
把拥塞窗口记为 cwnd,在途未确认数据量记为 bytes_in_flight,可用发送额度的简化关系为:
usable window ≈ max(0, min(cwnd, rwnd) - bytes_in_flight)例如 cwnd=20 KiB、rwnd=12 KiB、在途数据 8 KiB,此时还可发送约 4 KiB。若 rwnd 降至 0,增大拥塞窗口也不能绕过接收端限制。真实发送还受应用供数、套接字缓冲、pacing、TSO/GSO、Nagle/corking、ACK 行为和调度影响,恢复期间的在途估算也更复杂。
cwnd 是发送端状态,通常以字节表示;教材也常用发送端最大报文段大小(SMSS)为单位。它既不是路由器队列长度,也不是一次 send() 的大小。
带宽时延积(BDP)估算填满无排队路径需要的在途数据量:
BDP = bottleneck bandwidth × round-trip propagation time实测 RTT 还含发送、处理和排队延迟,并不等于纯传播时延。路径或容量变化时,BDP 也会变化。
2. 初始窗口与慢启动
实验性 RFC 6928 提出了以下初始拥塞窗口(IW)上界:
min(10 * SMSS, max(2 * SMSS, 14600 bytes))它不是所有系统都必须固定使用 10 段的命令。实现、缓存路由度量和配置都会影响初始窗口;RFC 6928 的文档类别也是 Experimental,不能说成标准强制值。RFC 6928
慢启动在新数据被确认时快速增加窗口,常被近似为每 RTT 翻倍。实际增速受到延迟 ACK、ACK 丢失或压缩、按确认字节计数、应用供数和实现策略影响,协议并非每过一个 RTT 就定时执行乘二。
达到慢启动阈值 ssthresh 或收到拥塞信号后,会退出慢启动。启动过冲可能把大量数据一次推入瓶颈队列;pacing 和 HyStart++ 等机制可帮助控制这种增长。
3. 拥塞避免与 AIMD
经典 Reno 风格的拥塞避免,近似每 RTT 增加 1 SMSS,遇到丢包拥塞事件后按比例减小窗口。加性增大、乘性减小(AIMD)形成常见的锯齿曲线。
公平性推导依赖理想条件。RTT、MSS、并行连接数、ACK 行为、丢包分布、主动队列管理(AQM)和算法共存都会改变结果。一个用户开 20 条连接,可能比只开一条连接的用户获得更多带宽;按连接公平不等于按用户公平。
拥塞也可以先表现为排队延迟。尾丢弃队列尚未装满时,吞吐可能很高,交互却已经明显变慢,这也是 bufferbloat 的典型现象。支持 ECN 的路径可以在丢包前标记拥塞。
4. 丢包检测不只靠三个重复 ACK
经典快速重传根据重复 ACK 推断缺失段,比等待重传超时(RTO)更快。但乱序、复制和 ACK 行为也会产生重复 ACK,所以达到阈值仍是推断,并非丢包的数学证明。
现代 TCP 可组合使用:
- 累计 ACK:确认连续收到的字节范围;
- SACK:报告已经收到的不连续字节块,帮助发送端识别缺口;
- RACK(Recent Acknowledgment):结合报文段最近发送时间、确认信息和乱序窗口推断丢失;
- TLP(Tail Loss Probe):发送尾部探测以引出 ACK,减少直接等待完整 RTO 的情况;
- RTO:快速检测缺少足够反馈时的超时恢复机制。
这些机制负责检测或修复丢失;确定丢包后,Reno、CUBIC 等拥塞控制器还要决定窗口如何变化。检测算法和控制算法不能互当同义词。RFC 8985
5. 快速恢复与 PRR
Tahoe、Reno 和 NewReno 的历史对比有助于理解,但不能直接当作现代 Linux 的完整状态机:
- Tahoe 的教学模型在丢包后把窗口降为 1 SMSS,重新慢启动;
- Reno 快速恢复利用重复 ACK 提供的发送节奏,避免单次丢包就立即回到慢启动;
- NewReno 用部分确认改进同一窗口内多处丢包的恢复;
- SACK 提供更精确的已收字节范围;
- 比例速率缩减(PRR)根据恢复进展控制实际发送,使在途量逐步接近拥塞控制器给出的
ssthresh。
PRR 的标准轨道规范为 RFC 9937,取代了实验性 RFC 6937。RFC 9937
若代码只写 cwnd = ssthresh + 3 * SMSS,它描述的是 Reno 的一小段行为,没有包含 SACK 记分板、在途量估计、PRR、乱序和重传歧义。
6. RTO 来自平滑 RTT 与变化量
TCP 根据 RTT 样本维护平滑 RTT(SRTT)和变化量(RTTVAR),再计算 RTO,并应用下限、上限和超时退避规则。RFC 6298 的基本形式是 SRTT + max(G, 4 * RTTVAR),其中 G 是时钟粒度;规范建议计算值小于 1 秒时向上取到 1 秒,具体系统可能采用不同下限。RFC 6298 §2
因此 RTO 既不能固定写成 200 ms,也不能把某系统的最小值当成所有路径的实际超时。Karn 算法避免从重传段的含糊 ACK 取 RTT 样本;TCP 时间戳选项可以帮助消除这类歧义。
超时通常表示连续 ACK 反馈已中断,发送行为会更保守,但具体窗口响应仍要按算法和实现解释。不能脱离单位和恢复状态,笼统写成所有超时都执行 cwnd=1。
7. ECN 用标记传递拥塞信号
经典 TCP ECN 先协商支持,发送端再在 IP 首部设置 ECT。队列可以把支持 ECN 的包标为 CE,接收端通过 TCP ECE 报告,发送端按拥塞控制规则降低发送负载,并通过 CWR 表示已作出响应。RFC 3168
AccECN 等扩展提供更细的反馈,具体行为要核对两端支持情况。ECN 也不保证零丢包:严重过载、缓冲耗尽或限速器仍可能丢包;其价值是部分分离“报告拥塞”和“丢掉数据”两件事。
8. 运行一个按 RTT 推进的教学模型
以下 Python 3 代码只演示慢启动和加性增长的形状。保存为 teaching_window.py,运行 python3 teaching_window.py。它不模拟逐包 ACK、SACK、重传或真实 TCP 定时器:
from dataclasses import dataclass
@dataclass
class TeachingWindow:
cwnd_mss: float = 10.0
ssthresh_mss: float = 64.0
def acked_round(self) -> None:
if self.cwnd_mss < self.ssthresh_mss:
self.cwnd_mss = min(self.cwnd_mss * 2, self.ssthresh_mss)
else:
self.cwnd_mss += 1
def congestion_event(self, beta: float = 0.5) -> None:
if not 0 < beta < 1:
raise ValueError("beta must be between 0 and 1")
self.ssthresh_mss = max(2.0, self.cwnd_mss * beta)
self.cwnd_mss = self.ssthresh_mss
state = TeachingWindow()
for _ in range(4):
state.acked_round()
print(state.cwnd_mss)
assert state.cwnd_mss == 65.0
state.congestion_event()
print(state.cwnd_mss)
assert state.cwnd_mss == 32.5预期输出:
20.0
40.0
64.0
65.0
32.5第三轮在模型阈值 64 截停,第四轮进入加性增长,得到 65;一次保留比例为 0.5 的拥塞事件后,窗口变成 32.5。真实 TCP 如何在阈值处过渡和取整取决于实现,这里特意固定规则以便推导。模型没有证明真实连接也会逐轮产生相同窗口。
9. 验收问题
cwnd、rwnd和在途数据分别限制或描述什么?- 慢启动为什么只能近似为每 RTT 翻倍?
- SACK、RACK、TLP 和 PRR 分别处理恢复过程的哪一步?
- 按连接公平为什么不等于按用户公平?
- ECN 为什么不保证零丢包?
下一课进入10.2 CUBIC、BBR、发送节奏与主动队列管理。