Nagle 与延迟 ACK:经典的 40ms 延迟
更新时间:2026-09-17。网络协议域·传输层。性能视角的关闭建议与实测见 TCP_NODELAY。
Nagle 算法和延迟 ACK 各自都是"为了减少网络小包"的优化,但在请求-应答、先发小包再发小包的模式下两者叠加会产生著名的约 40ms(Linux delayed ACK 常为 40ms)延迟。理解这个交互,能解释很多"局域网里单次请求莫名卡 40ms"的现象。
一、两个优化各自在做什么
**Nagle(发送方攒包,RFC 896)**规则:如果还有已发出但未被确认的小数据(小于 MSS),新产生的小数据先攒着不发,等 ACK 回来或攒够一个 MSS 再一起发。目的是减少只有几十个字节的小包(如 telnet 单字符)在广域网上的数量。
if 有未确认的在途小报文:
攒数据,等 ACK 或攒满 MSS
else:
立即发延迟 ACK(接收方,delayed ACK):收到数据后不立刻回 ACK,而是等一小段时间(Linux 通常 40ms),希望把 ACK"搭车"在本端要发的响应数据上(piggyback),减少纯 ACK 报文。
二、为什么叠加会卡 40ms
设想应用先发一个小请求(比如写头),紧接着又写一点数据,然后等待服务器响应——典型的"小包 + 小包 + 等应答":
客户端(Nagle 开) 服务端(delayed ACK 开)
发小报文1 -------------------> 收到,先不立刻 ACK,等 40ms 想搭车响应
产生小报文2,但报文1未确认
-> Nagle 让它攒着等 ACK
应用还在等报文2才好生成响应……
双方互相等:客户端等 ACK 才发报文2,
服务端等更多数据/响应才捎 ACK
<---- 40ms 后 delayed ACK 超时,发出纯 ACK
收到 ACK,Nagle 放行报文2 -----> 才继续本质是一个互相等待的小死锁:发送方因 Nagle 等 ACK,接收方因 delayed ACK 想等数据捎带,最后靠接收方 40ms 定时器超时打破。SSH 交互、小请求/响应协议、某些数据库与 HTTP chunked 场景都踩过。
三、怎么解决:TCP_NODELAY
TCP_NODELAY(禁用 Nagle,立即发送)是请求-响应类低延迟服务的标准做法:
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &(int){1}, sizeof(int));- Web/API、gRPC、Redis/Memcached 客户端、游戏等通常默认开 NODELAY:延迟比省几个包头更重要。
- 现代数据中心带宽充裕,40 字节 TCP 头的开销相对 40ms 等待可以接受,Nagle 的历史价值(低速串行链路 telnet)已不适用。
只关一端能不能解决?一般关发送方的 Nagle(让报文2不被攒)即可;但两端都可能既是发送又是接收,服务框架通常默认全局 NODELAY。接收方 delayed ACK 应用无法逐连接方便关闭(
TCP_QUICKACK是一次性的、会被内核重置),所以工程上主要靠 NODELAY + 应用层写包策略。
四、TCP_CORK:想攒包时的更可控选择
如果确实想合并小包(如先写响应头再写 body,希望一起发),TCP_CORK 比 Nagle 更可控:插上"塞子"期间数据攒着,拔掉时一次性发出,时间由应用掌握而不是等 ACK。
| 选项 | 行为 | 适用 |
|---|---|---|
| 默认(Nagle on) | 有在途小报文就攒,等 ACK/MSS | 老式批量、telnet |
TCP_NODELAY | 立即发,不攒 | 低延迟请求-响应(绝大多数服务) |
TCP_CORK | 显式攒到 uncork 再发 | 明确要合并头+体、转发聚合 |
五、应用层"粘包/拆包"视角
Nagle 常被误认为"粘包"的元凶,其实 TCP 是字节流本就无消息边界(见粘包拆包篇),Nagle 只是改变了几个字节被装进同一个段的时机:
- 接收应用一次
read可能读到半条或多条消息——这与 Nagle 无关,是字节流本质,必须靠长度前缀/分隔符/定长帧来解帧。 - Nagle 影响的是发送时机与延迟,不是消息边界。
六、排查
# 现象:局域网/本机单次往返稳定 ~40ms 倍数的延迟
ss -ti '( dport = :6379 )' # 看不到 Nagle 开关,但能看 RTT/RTO;抓包最直接
tcpdump -i any -nn -ttttt host x and port 6379
# 若看到请求报文与下一段之间稳定 40ms、且伴随 delayed ACK,即此问题应用侧确认 socket 是否设置了 NODELAY(很多框架/客户端有 tcpNoDelay、TCP_NODELAY=true 配置项,Nginx、Netty、gRPC 默认开启)。
相关阅读
性能调优与代码示例见 TCP_NODELAY 概念篇,消息边界见粘包拆包,ACK 与窗口见滑动窗口。
总结
Nagle 在有未确认小包时攒发送、delayed ACK 想把确认搭车响应,两者在"小包+小包+等应答"模式下互等,靠 40ms ACK 定时器打破,形成著名 40ms 延迟;解法是请求-响应类服务直接开 TCP_NODELAY 立即发,想合并用更可控的 TCP_CORK,而它与字节流的"粘包"是两回事。