bigywater.tech
网络协议 · 52

拥塞控制 1:慢启动与拥塞避免

拥塞窗口(cwnd)是怎么长大的、什么时候缩小?慢启动真的是指数增长吗?丢包后降到一半还是降到 0.7?这篇每 10 毫秒读一次发送方内核里的 cwnd,在 100 ms 往返、0.3% 丢包的回环上记下一次传输的全过程。

10 → 190 个 MSS
0.6 秒内慢启动把 cwnd 从初始窗口放大的倍数:每个 RTT 约翻倍
0.70
快速恢复后 cwnd 降后 / 降前的中位数(7 次),说明本机用 CUBIC 而不是 Reno 的 0.5
1 个 MSS
超时重传(RTO)时 cwnd 的落点——和快速恢复的 0.7 不同

初始窗口

实测 · 本机执行输出
  lo0:snd_cwnd = 163321 字节,maxseg = 16332 字节 → 初始窗口 = 10.0 个 MSS;snd_ssthresh = 1073725440(慢启动阈值,初始近似无穷大)
  en0 到 example.com:snd_cwnd = 13880 字节,maxseg = 1388 字节 → 10.0 个 MSS(RFC 6928 的初始窗口是 10 个 MSS)

新连接的 cwnd 是 10 个 MSS,ssthresh 近似无穷大。

慢启动

实测 · 本机执行输出
  传输 53.8 MB;一共采到 673 个样本;MSS = 16332;srtt 稳定后约 101 ms
    时间(秒)   cwnd(字节)   cwnd(个 MSS)   ssthresh(字节)   累计重传包
       0.00       163321          10.0       1073725440          0
       0.09       163321          10.0       1073725440          0
       0.20       310725          19.0       1073725440          0
       0.30       621033          38.0       1073725440          0
       0.41      1617285          99.0       1073725440          0
       0.50      1617285          99.0       1073725440          0
       0.60      3103497         190.0       1073725440          0
       0.80      2939760         180.0          2939760          1
       1.00        16332           1.0          2939760          2
       1.50      1731192         106.0          2939760          2
       2.00      1796520         110.0          1747524          3
       3.00       685944          42.0           636948          6
       4.00       408300          25.0           408300          8
       6.00       310308          19.0           310308         10
       8.00       326640          20.0           293976         11
  开头 0.5 秒(5 个 RTT):cwnd 依次约 10 → 10 → 19 → 38 → 99 → 99 个 MSS——每个 RTT 翻倍,这就是指数增长的"慢启动"(名字起得不好)

每收到 ACK 就增加 cwnd,等于每个 RTT 翻倍,直到丢包或到达 ssthresh。

丢包反应

实测 · 本机执行输出
  8 秒内检测到 8 次 cwnd 明显下降(下降到降前的 75% 以下):
     0.93 秒:cwnd 2939760 → 16332 字节(180 → 1 个 MSS),降后 / 降前 = 0.01,此时 ssthresh = 2939760
     1.65 秒:cwnd 2498796 → 1747524 字节(153 → 107 个 MSS),降后 / 降前 = 0.70,此时 ssthresh = 1747524
     2.15 秒:cwnd 1829184 → 1273896 字节(112 → 78 个 MSS),降后 / 降前 = 0.70,此时 ssthresh = 1273896
     3.24 秒:cwnd 702276 → 506292 字节(43 → 31 个 MSS),降后 / 降前 = 0.72,此时 ssthresh = 506292
     3.97 秒:cwnd 571620 → 408300 字节(35 → 25 个 MSS),降后 / 降前 = 0.71,此时 ssthresh = 408300
     4.55 秒:cwnd 440964 → 326640 字节(27 → 20 个 MSS),降后 / 降前 = 0.74,此时 ssthresh = 310308
     5.89 秒:cwnd 440964 → 310308 字节(27 → 19 个 MSS),降后 / 降前 = 0.70,此时 ssthresh = 310308
     7.40 秒:cwnd 424632 → 293976 字节(26 → 18 个 MSS),降后 / 降前 = 0.69,此时 ssthresh = 293976
  降后 / 降前的中位数 0.70(Reno 是 0.5;CUBIC 是 0.7)——本机默认用的是CUBIC 的 0.7 附近;net.inet.tcp.cubic_sockets 显示本机有 CUBIC 在用
  net.inet.tcp.cubic_sockets: 445

超时重传让 cwnd 回到 1 个 MSS;快速恢复只降到 0.7 倍(CUBIC),同时 ssthresh 设成降后的值。

拥塞避免

实测 · 本机执行输出
  0.0~0.9 秒这一段:cwnd 从 67 个 MSS 变到 180 个 MSS;斜率 214.0 个 MSS/秒 = 每个 RTT(100 ms)增加约 21.4 个 MSS
  4.5~5.9 秒这一段:cwnd 从 21 个 MSS 变到 27 个 MSS;斜率 7.9 个 MSS/秒 = 每个 RTT(100 ms)增加约 0.8 个 MSS
  5.9~7.4 秒这一段:cwnd 从 22 个 MSS 变到 26 个 MSS;斜率 4.5 个 MSS/秒 = 每个 RTT(100 ms)增加约 0.4 个 MSS
  慢启动:cwnd 每个 RTT 翻倍,直到遇到 ssthresh 或丢包;拥塞避免:每个 RTT 只增加约 1 个 MSS(线性增长),CUBIC 则按三次函数曲线增长;丢包后乘性减小(cwnd 降到 β × cwnd,并把 ssthresh 设成同样的值)——AIMD

之后每个 RTT 只增长不到 1 个 MSS;CUBIC 接近上次丢包窗口时增长更平缓。

分步演示

慢启动与拥塞避免
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。全部在 lo0 上,往返 100 ms 与 0.3% 丢包由 netlab 注入;cwnd / ssthresh / srtt 来自发送方套接字的 TCP_CONNECTION_INFO,每 10 ms 采样一次(Python 线程的采样间隔会有抖动);丢包是随机的,每次运行的曲线不同,本篇的数字取自最后一次运行
    • 没有验证:BBR 或 Reno 的对照(下一篇);cwnd 与接收窗口、发送缓冲区的相互限制;拥塞避免阶段 CUBIC 的三次曲线是否完全符合公式;ECN 标记引起的 cwnd 下降;en0 真实网络上的 cwnd 曲线
    京ICP备2026067502号-1京公网安备11010502063487号