Nagle 算法和延迟 ACK 单独看都合理,放在一起据说会产生几十毫秒的停顿。这篇在本机上实测:延迟 ACK 到底延不延迟、经典冲突能不能复现、真实网络上 Nagle 的代价是多少、它又省下了多少包。有一半的结论是“没复现”,我会如实写。
net.inet.tcp.delayed_ack: 3 sysctl: unknown oid 'net.inet.tcp.sendmoreacks' delayed_ack: 0 = 关(每个包都立刻 ACK);1 = 总是延迟;2 = 每两个包 ACK 一次;3 = 自适应(检测到对端是 Nagle / 交互流量时变成立刻 ACK) 实测:客户端发 1 个 100 字节的包,服务端不回数据,量服务端什么时候发出纯 ACK(10 次) 一个孤立的小包之后,服务端纯 ACK 的延迟(ms):[0.1, 0.1, 0.1, 0.1, 0.1, 0.0, 0.1, 0.1, 0.1, 0.1];中位 0.1 ms,最大 0.1 ms 对照:连续发 2 个小包(TCP_NODELAY,间隔几微秒)时,抓到 20 个数据段(共 10 轮);服务端回的纯 ACK 个数 19——(数一数:几乎每个数据段都有一个纯 ACK,没有看到两个包合并成一个 ACK)
接收方不一定马上回 ACK,可以等一等好捎带数据或合并。macOS 的 net.inet.tcp.delayed_ack 是 3(自适应)。实测回环上孤立小包 0.1 ms 就被确认。
Nagle 开(默认),一次 write 20 字节 :请求到应答 中位 0.22 ms / 最小 0.07 / 最大 0.45 ms Nagle 开(默认),分两次 write :请求到应答 中位 0.22 ms / 最小 0.10 / 最大 0.40 ms TCP_NODELAY,分两次 write :请求到应答 中位 0.20 ms / 最小 0.10 / 最大 0.47 ms 回环上三种写法都在 0.2 ms 左右,没有出现经典的 40 ms 级停顿:这台机器的 delayed_ack=3(自适应),回环上收到小包后立刻回 ACK,Nagle 不用等;经典冲突在这里没有复现
Nagle 要等 ACK,ACK 又被延迟,就会停顿。本机回环上三种写法都是 0.2 ms 左右,没有 40 ms 级的停顿——这是“没复现”,不是“不存在”。
从第一个 write 到收到应答共 0.17 ms。线上(相对第一个数据包的时间):
+ 0.00 ms 客户端→服务端 载荷 10 字节
+ 0.05 ms 服务端→客户端 载荷 0 字节 纯 ACK
+ 0.08 ms 客户端→服务端 载荷 10 字节
+ 0.08 ms 服务端→客户端 载荷 0 字节 纯 ACK
+ 0.11 ms 服务端→客户端 载荷 10 字节
+ 0.13 ms 客户端→服务端 载荷 0 字节 纯 ACK
第二个 write 的 10 字节是在第一个包的 ACK 回来之后才发出的(Nagle:有未确认的小包时,新的小数据先攒着等 ACK);回环上 ACK 几十微秒就回来,所以感觉不到第二个小写入是在第一个包的 ACK 回来之后才发出的,这就是 Nagle;回环上 ACK 几十微秒就到。
握手耗时(≈ 一个 RTT)中位 192 ms Nagle 开(默认):8 次有效,第一次 write 到响应第一批字节 中位 403 ms(最小 377 / 最大 514) TCP_NODELAY:8 次有效,第一次 write 到响应第一批字节 中位 201 ms(最小 189 / 最大 320) 两者之差的中位数 202 ms,约 1.0 个握手耗时
对 example.com 把请求分两次 write,Nagle 开时第二次 write 要等一个 RTT,首字节时间从 201 ms 变成 403 ms。
服务器发来 2 个数据包,其中 1 个找到了对应的本机纯 ACK:5 字节 → 0.1 ms
只抓到一个可配对的数据包,0.1 ms 回 ACK,样本太小,仅作旁证。
Nagle 开(默认):20000 次 write(共 2,000,000 字节)用时 0.04 秒;线上数据段 908 个,平均每段 2203 字节;纯 ACK 908 个 TCP_NODELAY:20000 次 write(共 2,000,000 字节)用时 0.11 秒;线上数据段 20000 个,平均每段 100 字节;纯 ACK 8813 个 Nagle 把小写入合并成大段,包数少得多;代价是交互式小消息可能被推迟。TCP_NODELAY 适合"请求-应答"和低延迟场景,Nagle 适合不关心单个包延迟的批量小写入
2 万次 100 字节小写入,Nagle 合并成 908 个数据段,TCP_NODELAY 则是 20000 个,每段 100 字节。