首页 / 内容指南 / 当前文章

TCP 的 Nagle、Delayed ACK、TCP_NODELAY、TCP_CORK、MSG_MORE 和 TCP_QUICKACK 有什么区别?

发布于 2026-08-26 · xieyipai.com 编辑部

排查 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接收端 TCPACK 策略减少纯 ACK,允许捎带确认ACK 一定固定延迟 200ms
TCP_NODELAY发送端应用设置Socket 级禁用 Nagle,允许尽快发送小段自动让所有请求更快
TCP_CORKLinux 发送端Socket 状态暂缓不完整帧,优先形成完整段它等同于 Nagle 或适用于所有系统
MSG_MORELinux 发送调用单次 send 提示告诉内核后续还有数据设置后必须另行“关闭”
TCP_QUICKACKLinux 接收端临时提示尽快发送 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 可在接收路径合并多个段。必要时在网络侧捕获,或临时调整卸载进行受控测试,但不要在生产中随意关闭卸载。

一套安全的调优顺序

  1. 建立延迟分位数、包率、吞吐和 CPU 基线,不只看平均值。
  2. 记录应用写入模式,优先合并同一消息的碎片化写入。
  3. 区分客户端、代理、服务端各段 TCP 连接,确定延迟在哪一段。
  4. 用抓包验证未确认数据、小段等待和 ACK 时间线。
  5. 在可回滚实验中单独切换 TCP_NODELAY,比较尾延迟与包率。
  6. 需要头部加正文聚合时测试 writev、MSG_MORE 或 Linux TCP_CORK。
  7. 仅在确认接收端 ACK 策略是瓶颈时实验 TCP_QUICKACK。
  8. 覆盖高丢包、长 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 应用表达“数据尚未准备完整”。先优化应用写入,再通过双端时间线验证机制,最后按连接角色做小范围调优,才能在尾延迟、吞吐和包率之间取得可解释的平衡。

参考来源

  1. IETF RFC 9293, Transmission Control Protocol (TCP): https://www.rfc-editor.org/rfc/rfc9293.html
  2. IETF RFC 896, Congestion Control in IP/TCP Internetworks: https://www.rfc-editor.org/rfc/rfc896.html
  3. IETF RFC 1122, Requirements for Internet Hosts — Communication Layers: https://www.rfc-editor.org/rfc/rfc1122.html
  4. IETF RFC 5681, TCP Congestion Control: https://www.rfc-editor.org/rfc/rfc5681.html
  5. Linux man-pages, tcp(7): https://man7.org/linux/man-pages/man7/tcp.7.html
  6. Linux man-pages, send(2): https://man7.org/linux/man-pages/man2/send.2.html
  7. Linux man-pages, sendfile(2): https://man7.org/linux/man-pages/man2/sendfile.2.html