连载中 8/20

Nagle 与延迟确认:一个攒包一个拖确认,40ms 卡顿的百年恩怨

2026-10-04 · 10 阅读 · 0 评论 · 0 赞

Nagle 算法:别为一个小字节占一辆车

1984 年,John Nagle 发现有人在 telnet 里敲一个字符,网络上就要跑一个完整的 40 字节 IP 头 + 32 字节 TCP 头 + 1 字节数据的包——效率 1.4%。他的解法:发送方把小包攒起来,满足任一条件才发:① 收到了所有在途数据的 ACK;② 攒够了一个 MSS(约 1460 字节)。规则一句话:同一时刻,最多只能有一个没被确认的小包在外面飘着。

延迟确认:对端的温柔一刀

几乎同时,接收方也有自己的优化:收到数据不立刻回 ACK,等一等——盼着应用层正好有响应数据,ACK 搭在数据包上捎回去(捎带确认);实在没得捎,最多等 40ms(Linux 的典型值)也必须回。单独看都没毛病:Nagle 省带宽,延迟确认省 ACK 包。

相遇即死锁:40ms 从哪来

场景:客户端写-读一问一答,双方都开默认选项

客户端                                服务端
  write(小数据) ────────→              收到,想捎带 ACK 等响应
  Nagle:上一个包没确认?              应用 40ms 后才慢慢生成响应
         攒着不发 ←──── 无 ACK
  ……僵持,直到某一边破局
  实际表现:每个请求多出 40ms 左右的莫名延迟

诊断特征很典型:抓包看 RTT 正常、CPU 不忙、DB 很快,但接口就是稳定慢 40ms 上下——十有八九是它俩。Wireshark 里能看到 ACK 前有一段 40ms 的沉默。

解法:关 Nagle + 应用层自己攒

交互式、请求-响应式的服务(RPC、数据库驱动、游戏、IM)一律关闭 Nagle:

// Java
Socket socket = new Socket();
socket.setTcpNoDelay(true);

// Go
conn.SetNoDelay(true)

// MySQL 驱动、Redis 客户端大多默认已关;确认一下总没错

// 注意:关 Nagle 不等于放弃攒包,是把攒包的责任收归应用层:
//   反例:循环 100 次每次 write 8 字节 → 100 个小包
//   正解:拼成 800 字节的 byte[] 一次 write → 1 个包
// 延迟敏感 + 批量能力,两头都要

什么时候可以留着 Nagle

大流量、非交互的场景反而受益:日志采集上报、指标推送这类高频小包遥测,对延迟不敏感、对包量敏感,Nagle 帮你省真实带宽。判断标准就一条:这条连接上,是人等结果,还是机器攒数据。人等的,关。下一篇从传输层抬头上岸,进入应用层:HTTP/1.1 攒了三十年的家底,和它天生的欠账。

☕
503

10 年全栈工程师 · 503咖啡馆主理人

#Nagle算法#延迟确认#TCP_NODELAY#小包#40ms延迟

评论 (0)

热门推荐

连载中 11/22

主从搭建实操:从零配出一主两从

光讲原理不过瘾?手把手搭一主两从:my.cnf 六个参数、复制账号、GTID、CHANGE REPLICATION SOURCE TO、SHOW REPLICA STATUS 验收,附翻车排查清单。

#MySQL#主从复制#GTID#主从搭建#高可用
2026-05-07 · 10100 阅读 · 0 评论 · 0 赞
连载中 16/22

连接池:HikariCP 参数与连接风暴

连接池不是越大越好:8 核机器配 1000 连接反而更慢的数学原理,HikariCP 四个必调参数,maxLifetime 与 wait_timeout 的隐形陷阱。

#MySQL#连接池#HikariCP#maxLifetime#连接风暴
2026-05-10 · 9872 阅读 · 0 评论 · 0 赞
连载中 4/16

缓存穿透:恶意 ID 打穿 MySQL 的四道防线

请求的数据在缓存和数据库里都不存在时,缓存形同虚设。聊聊参数校验、空值缓存、布隆过滤器、限流熔断四道防线的原理与组合打法。

#Redis#缓存穿透#布隆过滤器#高可用
2026-05-16 · 9293 阅读 · 21 评论 · 287 赞