TCP 的 Nagle、Delayed ACK、TCP_NODELAY、TCP_CORK、MSG_MORE 和 TCP_QUICKACK 有什么区别?
排查 TCP 小请求“偶尔慢几十到几百毫秒”时,Nagle、Delayed ACK、TCP_NODELAY、TCP_CORK、MSG_MORE 和 TCP_QUICKACK 经常被放在一起讨论。它们作用方向并不相同:Nagle 算法约束发送端在仍有未确认数据时继续发送小段;Delayed ACK 让接收端短暂等待,以减少纯 ACK;TCP_NODELAY 禁用 Nagle;TCP_CORK 在 Linux 上主动暂缓不完整数据段;MSG_MORE 只提示当前这次发送后面还有数据;TCP_QUICKACK 则临时提示接收端尽快发送 ACK。
本文目录(16 节)
六个机制快速对比
| 机制 | 主要作用端 | 作用范围 | 目标 | 常见误区 |
|---|---|---|---|---|
| Nagle | 发送端 TCP | 连接级算法 | 合并未确认期间的小写入 | 所有延迟都是 Nagle 导致 |
| Delayed ACK | 接收端 TCP | ACK 策略 | 减少纯 ACK,允许捎带确认 | ACK 一定固定延迟 200ms |
TCP_NODELAY | 发送端应用设置 | Socket 级 | 禁用 Nagle,允许尽快发送小段 | 自动让所有请求更快 |
TCP_CORK | Linux 发送端 | Socket 状态 | 暂缓不完整帧,优先形成完整段 | 它等同于 Nagle 或适用于所有系统 |
MSG_MORE | Linux 发送调用 | 单次 send 提示 | 告诉内核后续还有数据 | 设置后必须另行“关闭” |
TCP_QUICKACK | Linux 接收端 | 临时提示 | 尽快发送 ACK | 一次设置后永久关闭 Delayed ACK |
Nagle 算法解决什么问题
Nagle 算法源于“小包问题”:应用频繁写入少量字节时,每个写入都可能产生一个带完整 TCP/IP 头的小段,浪费带宽并增加路由器、主机和网络的处理负担。
其核心思路是:如果连接上仍有未确认数据,发送端通常暂存新的小块数据,等待已有数据被确认,或等待积累到足以发送更大的段。若没有未确认数据,首个小写入通常可以立即发出。因此,Nagle 不是“每次都等待一个定时器”,也不是固定延迟值。
RFC 9293 要求 TCP 实现支持 Nagle,并允许应用在单个连接上禁用。具体分段仍受 MSS、拥塞窗口、接收窗口、发送缓冲和系统实现影响。抓包看到小段并不能反推 Nagle 一定关闭;重传、连接尾部、控制消息和显式刷新都可能产生小段。
Delayed ACK 解决什么问题
TCP 接收端不必对每个数据段立即发送一个纯 ACK。短暂延迟可以等待反向应用数据并把 ACK 捎带在其中,或一次确认多个连续段,从而减少报文数量。
Delayed ACK 是接收方行为,Nagle 是发送方行为。两者由不同主机、不同状态机控制。RFC 1122 与拥塞控制规范为 ACK 延迟设置了边界:延迟不能无限增长,并且对连续全尺寸数据段应保持足够的确认频率。操作系统还可能依据快速路径、乱序、缺口、交互模式和拥塞控制动态调整,不能把所有平台简化成固定 40ms 或 200ms。
ACK 也是 TCP 拥塞控制的时钟信号。过度减少 ACK 会影响发送端增长拥塞窗口和判断交付情况;过度立即 ACK 则会增加报文与处理开销。因此现代实现通常采用自适应策略。
Nagle 与 Delayed ACK 为什么会形成延迟
经典场景是请求被应用拆成两个相关小写入:发送端先发第一小段,第二小段因 Nagle 等待第一段 ACK;接收端收到第一段后又因 Delayed ACK 等待更多数据或反向数据。双方的等待条件短暂互锁,最终由 ACK 定时策略打破,应用看到额外尾延迟。
这不是所有小请求延迟的唯一原因。调度、TLS 缓冲、语言运行时、代理、拥塞窗口、丢包重传、接收窗口、应用队列和服务端处理都可能产生相似现象。必须用两端时间线和抓包验证,不能只凭延迟接近某个数字就修改 Socket 选项。
更根本的修复常是让应用按协议消息边界组织写入,把头部和短正文一次提交,或使用 writev/sendmsg 聚合缓冲区,而不是依赖 TCP 猜测哪些写入属于同一条消息。
TCP_NODELAY:关闭 Nagle
应用在发送 Socket 上设置 TCP_NODELAY 后,TCP 会禁用 Nagle,让符合其他发送条件的小数据尽快发出。这常用于对单次交互延迟敏感、消息较小且应用已合理控制写入次数的协议,例如终端输入、游戏控制消息或低延迟 RPC。
int one = 1;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
TCP_NODELAY 不会绕过拥塞控制、接收窗口、重传、网卡队列或调度,也不保证一次 send 对应一个 TCP 段。TCP 是字节流,内核和网卡仍可合并、分段或卸载处理。
如果应用逐字节调用 send,开启 TCP_NODELAY 可能制造大量小包。正确顺序是先减少碎片化写入,再评估是否禁用 Nagle。对大吞吐传输,Nagle 通常不是瓶颈,因为发送缓冲很快积累到全尺寸段。
TCP_CORK:主动暂缓不完整段
TCP_CORK 是 Linux 特有的 TCP Socket 选项。启用时,内核倾向于暂缓发送部分帧,等待应用提供更多数据,以构造更完整的段;关闭 cork 时,待发送数据会被推出。
典型用途是应用先写一个小头部,再通过零拷贝或文件发送接口写大正文,希望头部和正文尽量组合,而不是让头部单独成为小段。
int one = 1, zero = 0;
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &one, sizeof(one));
send(fd, header, header_len, 0);
sendfile(fd, file_fd, NULL, file_len);
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &zero, sizeof(zero));
Linux 为 cork 设置发送上限,避免应用忘记解除后无限期卡住数据;man-pages 描述的上限约为 200ms。这个保护不是应用刷新机制,正常代码必须在消息准备完成后明确解除。
TCP_CORK 与 TCP_NODELAY 方向相反:前者强调暂存组合,后者强调尽快发送。Linux 允许两者在一定版本后组合使用,TCP_NODELAY 的显式设置可以触发当前待发数据刷新,但不要依赖模糊的组合状态设计协议。跨平台程序也不能假设存在 TCP_CORK。
MSG_MORE:一次 send 的“后面还有”提示
MSG_MORE 是 Linux send/sendmsg 的调用标志。它告诉内核本次提供的数据后面还有内容,效果类似短期 cork;最后一次不带 MSG_MORE 的发送表示这一批数据可以推出。
send(fd, header, header_len, MSG_MORE);
send(fd, body, body_len, 0);
与 TCP_CORK 相比,MSG_MORE 不需要维护长期 Socket 开关,适合少数连续调用组成一条消息。若错误地在最后一次发送也设置它,数据可能被推迟到内核的超时或其他刷新条件。
MSG_MORE 仍只是提示,不能创建消息边界。对端 recv 可能一次读到部分数据,也可能一次读到多个应用消息。协议必须用长度前缀、分隔符、固定格式或上层帧定义边界。
TCP_QUICKACK:临时提示尽快确认
TCP_QUICKACK 是 Linux 特有选项,用于提示接收端进入快速确认模式,立即或更快发送 ACK。它不是永久关闭 Delayed ACK 的连接级保证;后续协议处理会根据内部状态重新进入或离开延迟确认模式。
int one = 1;
setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, &one, sizeof(one));
它作用于 ACK 方向,不会直接刷新本机发送缓冲,也无法控制远端应用何时读取数据。中间代理终止 TCP 时,应用设置只影响到代理这一段连接,代理到上游是另一套 TCP 状态。
频繁强制 Quick ACK 可能增加纯 ACK 数量,并改变对端拥塞控制节奏。只有在测量证明 Delayed ACK 是关键延迟来源、且部署平台明确支持时才适合使用。
TCP_NODELAY、TCP_CORK 和 MSG_MORE 如何选择
低延迟且每条消息已在用户态聚合:考虑 TCP_NODELAY。头部与大正文分两种机制输出,并希望尽量形成完整段:Linux 上可考虑 TCP_CORK。少量连续 send 明确属于同一批数据:可考虑前几次使用 MSG_MORE,最后一次不带标志。
若应用可以一次 sendmsg 或 writev 提交多个缓冲区,通常比在多个 Socket 开关之间切换更清晰。TLS 库还会形成自己的记录并缓存数据;Socket 选项作用在加密后的 TCP 字节流,应用写入与线上分段不再一一对应。
HTTP、TLS 和代理场景的边界
HTTP/1.1 响应头和很短正文若分开写,可能出现额外小段。服务器框架通常已有缓冲或向量写优化,应用层再次设置 cork 前应先检查框架行为。
HTTP/2 把多个流复用到一个 TCP 连接,并有帧与流量控制;某个小帧的延迟还可能来自调度和队头阻塞。HTTP/3 使用 QUIC over UDP,不适用 TCP 的 Nagle、TCP_NODELAY 或 TCP_CORK。
TLS 会把明文组织为记录。频繁刷新微小 TLS 记录既增加密码学和头部开销,也会影响 TCP 分段。应联合观察应用写入、TLS 记录和 TCP 包,而不是只抓包看 PSH 标志;PSH 不是可靠的应用消息边界。
反向代理会创建客户端到代理、代理到上游两条或更多独立连接。前一段关闭 Nagle 不代表后一段也关闭。诊断必须标记每一段的发送端、接收端和 Socket 配置。
如何用抓包确认真实原因
先记录应用每次写入的时间、字节数和返回值,再在发送端与接收端同步抓包。关注序列号范围、ACK 号、段长度、重传、窗口和时间间隔。
若第一小段已发出,第二次应用写入已经完成但网络上迟迟不出现,直到第一段 ACK 到达后才发送,符合 Nagle 参与的特征。若接收端收到有序数据后没有立即 ACK,并在更多数据或计时条件出现后确认,则可能存在 Delayed ACK。只有单侧抓包时,时钟、捕获点和网卡卸载会让判断失真。
TSO/GSO 可让抓包在发送主机看到大于 MSS 的“超级包”,GRO/LRO 可在接收路径合并多个段。必要时在网络侧捕获,或临时调整卸载进行受控测试,但不要在生产中随意关闭卸载。
一套安全的调优顺序
- 建立延迟分位数、包率、吞吐和 CPU 基线,不只看平均值。
- 记录应用写入模式,优先合并同一消息的碎片化写入。
- 区分客户端、代理、服务端各段 TCP 连接,确定延迟在哪一段。
- 用抓包验证未确认数据、小段等待和 ACK 时间线。
- 在可回滚实验中单独切换
TCP_NODELAY,比较尾延迟与包率。 - 需要头部加正文聚合时测试
writev、MSG_MORE或 LinuxTCP_CORK。 - 仅在确认接收端 ACK 策略是瓶颈时实验
TCP_QUICKACK。 - 覆盖高丢包、长 RTT、小窗口、TLS 和代理路径,避免只在本机回环验证。
常见误区
- 把 Nagle 当成固定 200ms 定时器;实际等待与未确认数据、MSS 和 ACK 有关。
- 认为
TCP_NODELAY能关闭远端 Delayed ACK;它只影响本端 Nagle。 - 打开
TCP_CORK后忘记解除,把内核上限误当正常刷新。 - 把
MSG_MORE当成对端可见的消息结束标志。 - 认为
TCP_QUICKACK一次设置后对连接永久有效。 - 只看 PSH 位推断应用消息边界。
- 忽略 TLS 缓冲、代理分段和网卡卸载对抓包的影响。
- 为降低几毫秒延迟制造大量小包,却不监控 CPU 和吞吐成本。
FAQ
1. 开启 TCP_NODELAY 是否一定更快?
不一定。它可能降低小型交互消息等待,但碎片化写入会产生更多小包。大流量传输通常已经形成全尺寸段,收益有限。
2. Delayed ACK 通常延迟多少毫秒?
没有跨平台固定值。规范给出上界和确认频率要求,系统会动态调整。应以目标内核、连接状态和抓包为准。
3. TCP_CORK 和 MSG_MORE 哪个更好?
短小、明确的连续发送适合 MSG_MORE;跨多个发送接口或需要一段状态期的组合可能适合 TCP_CORK。两者都是 Linux 特性,跨平台代码需提供替代方案。
4. 一次 send 会对应一个 TCP 包吗?
不会。TCP 是字节流,内核可合并多个写入、拆分大写入,并使用 TSO 等卸载。对端读取边界也不对应发送调用边界。
5. 设置 TCP_QUICKACK 能修复 Nagle 延迟吗?
它可能缩短某些接收端 ACK 等待,但只是临时提示,而且增加 ACK。更稳妥的做法是先合并应用写入,并在发送端评估 TCP_NODELAY。
6. HTTP/3 需要 TCP_NODELAY 吗?
不需要。HTTP/3 基于 QUIC 和 UDP,不使用 TCP。QUIC 有自己的拥塞控制、确认和数据包发送策略。
7. 为什么抓包显示超过 MSS 的 TCP 段?
主机抓包可能发生在 TSO/GSO 分段之前或 GRO/LRO 合并之后。线上实际报文可能更小,需要结合捕获位置和卸载设置解释。
8. 代理两侧的 TCP 选项会自动继承吗?
不会。代理终止一条连接并新建另一条连接,两侧有独立 Socket、拥塞状态、Nagle 和 ACK 策略,需要分别配置和测量。
结论
Nagle 与 TCP_NODELAY 控制发送端小段策略,Delayed ACK 与临时的 TCP_QUICKACK 控制接收端确认节奏,TCP_CORK 和 MSG_MORE 则帮助 Linux 应用表达“数据尚未准备完整”。先优化应用写入,再通过双端时间线验证机制,最后按连接角色做小范围调优,才能在尾延迟、吞吐和包率之间取得可解释的平衡。
参考来源
- IETF RFC 9293, Transmission Control Protocol (TCP): https:
/ / www. rfc- editor. org/ rfc/ rfc9293. html - IETF RFC 896, Congestion Control in IP/TCP Internetworks: https:
/ / www. rfc- editor. org/ rfc/ rfc896. html - IETF RFC 1122, Requirements for Internet Hosts — Communication Layers: https:
/ / www. rfc- editor. org/ rfc/ rfc1122. html - IETF RFC 5681, TCP Congestion Control: https:
/ / www. rfc- editor. org/ rfc/ rfc5681. html - Linux man-pages, tcp(7): https:
/ / man7. org/ linux/ man- pages/ man7/ tcp. 7. html - Linux man-pages, send(2): https:
/ / man7. org/ linux/ man- pages/ man2/ send. 2. html - Linux man-pages, sendfile(2): https:
/ / man7. org/ linux/ man- pages/ man2/ sendfile. 2. html