本文目录导读:

- 机制概述
- “Compound” 的代码协同过程(基于 Linux 内核实现)
- 关键协同技术:S/G DMA 和 GSO
- 场景复现:Redis 或 gRPC
- 总结:tcp_autocorking 如何 Compound
“tcp_autocorking” 和 “TCP 小包的 compound(合并/聚合)” 是现代 Linux 内核网络协议栈(TCP/IP 栈)中用来提升小数据包发送效率(特别是对于像 Redis、Memcached、HTTP/2 的 gRPC 请求等场景)的两个重要机制。
核心目的:减少网络包的数量,降低 CPU 开销和网络传输次数。
下面从代码实现和协作机制的角度,解释它们是如何 “compound”(协同工作/聚合)的。
机制概述
- tcp_autocorking:这是一个内核标志(sysctl 参数
net.ipv4.tcp_autocorking,默认开启),它允许 TCP 在发送完一个数据包后,不立即清空发送缓存,而是将后续的小数据“黏附”在同一个 skb(socket buffer)上,继续等待填满。 - TCP Small Queues (TSQ):这是另一个关联机制,用于限制每个 socket 在 Qdisc(队列规则)和驱动队列中的未确认字节数,防止单个连接填满缓冲区,它和 autocorking 配合得很好。
- Nagle 算法:这是经典的合并算法(延迟发送等待 ACK),但 autocorking 比 Nagle 更激进,它在发送第一个包后立即开始合并,而不是等 ACK。
“Compound” 的代码协同过程(基于 Linux 内核实现)
当用户态应用(如 Redis)执行 write() 或 send() 发送小数据时,内核的 TCP 传输层会进行如下操作:
步骤 1:数据到达 tcp_sendmsg
假设应用连续发送了两个 1KB 的小数据包。
-
第一次 send():
- 内核分配一个新的 skb(
sk_stream_alloc_skb)。 - 将 1KB 数据拷贝进去。
- 调用
tcp_push尝试发送。 tcp_autocorking检查:如果满足条件(当前 socket 处于 cork 状态,或者刚发送了一个包且还有更多数据可发),内核会不立即刷新。- 关键点:
tcp_autocorking允许这个 skb 保留在发送队列中,即使它没有被填满(而普通模式下,小包会直接发送出去),内核会设置一个“强制 cork”标志(icsk_forced_cork)。
- 内核分配一个新的 skb(
-
第二次 send()(紧接着第一次,毫秒级内):
- 内核检查发送队列的尾部 skb,发现它只有 1KB,还能再放数据。
- Compound 行为:内核不会分配新 skb,而是直接将新来的 1KB 追加到已有的那个 skb 中(调用
skb_put或pskb_expand_head扩大数据区)。 - 效果:原来的 1KB 包和 1KB 新包合并成了一个 2KB 的 skb,即使它们来自不同的
send调用。
步骤 2:触发发送的条件
合并后的 skb 会在以下任一情况被真正推送到网卡:
- 填满了一个 MSS(最大分段大小,通常是 1460 字节):两个 1KB 加起来 2KB,超过了 MSS,内核会进行分段(GSO/TSO 分段),发送第一个 1460 字节的分段,剩下的部分继续合并。
- 应用调用了
tcp_push且满足不出队列条件:应用设置了TCP_CORK(完全 cork)或MSG_MORE标志,然后调用send()并最终关闭。 - 定时器超时:如果应用长时间不发新数据,autocorking 不会无限等待,内核会在一段时间后(如 1ms 或受 TCP 延迟确认影响)强制推送,这被称为“哑巴时间片”。
步骤 3:与 Nagle 的协同(对比)
- Nagle:强制等待上一个包被 ACK 确认,才能发送下一个包(即使包很小),这会导致延迟(受 RTT 影响)。
- Autocorking:不等待 ACK,它在发送完第一个小包后,会立即尝试将后续小数据追加到即将发送的 skb 的末尾,只要 skb 还没真正离开 TCP 层(被交给 IP 层),合并就可以进行。
- Compound 效果:一个典型场景是“请求-响应”模式(如 HTTP 请求头)。
Autocorking 可以把“请求头 + 请求体”合并成一个 skb,避免了 Nagle 导致的 ACK 等待延迟。
关键协同技术:S/G DMA 和 GSO
现代的 “compound” 不仅是逻辑上的追加,还利用了硬件的 Scatter-Gather DMA:
- 不复制数据:当第二次
send()的数据到来时,内核不需要把两次的用户空间数据拷贝到一起,它只需在 skb 的frag_list或frags数组中新增一个页框指针(指向新数据)。 - GSO (Generic Segmentation Offload):网络栈最终在把合并后的“巨型包”(Super Skb)交给驱动前,会通过 GSO 根据 MSS 把它分割成多个标准大小的 TCP 段,但分割前,它们共享同一个 skb 结构,降低了 skb 分配和管理的 CPU 开销。
场景复现:Redis 或 gRPC
以 Redis SET 命令为例(数据较小):
- 应用调用
write(sockfd, "*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$5\r\nhello\r\n", 30)。 - 内核 TCP 层得到这 30 字节。
- 如果不开启 autocorking:直接构建一个 30 字节的 TCP 包立即发送,这是一个小包。
- 开启 autocorking(默认开启):
- 如果此时 socket 的发送队列尾部有一个刚发完但未清空(因为之前有数据量较大刚发完)的 skb 块,并且其剩余空间足够(比如有 1400 字节的空位),这 30 字节会直接追加到这个旧 skb 的末尾。
- 这个 skb 会在填满到 MSS(1460)后,一次性发送出去;或者在定时超时后作为 30 字节的小包发送。
- Compound 结果:3 个独立的
write()调用产生的 30+50+40 = 120 字节,如果它们发生在极短时间内,最终可能只生成 1 个 120 字节的 skb,而不是 3 个 120 字节的小包单独发送。
tcp_autocorking 如何 Compound
| 特性 | 说明 |
|---|---|
| 底层机制 | 通过 skb->data_len 和 skb_shared_info 中的 frag_list 或 frags 数组,将多次 send() 的用户数据逻辑上合并到同一个 skb 结构内。 |
| 触发条件 | sysctl net.ipv4.tcp_autocorking = 1(默认)。当前 socket 的发送队列没有达到 sk->sk_wmem_alloc 的硬限制。数据到达时,队列尾部 skb 未被完全发送至 IP 层。 |
| 何时合并 | 发送完成但 skb 未被释放期间,内核在 tcp_push_one 或 __tcp_transmit_skb 之前,检查是否还有未被 tcp_write_xmit 清空的 skb。 |
| 何时拆分 | 当合并后的总大小超过 MSS 时,提交给 GSO(Generic Segmentation Offload)进行硬件/软件层面的大包拆分,但拆分前,它们共享一个 skb,降低了协议栈的处理次数。 |
| 性能收益 | - 减少 CPU 利用率:减少链路层(Qdisc/驱动)的中断次数。 - 提高带宽利用率:减少小包导致的协议头开销(以太网头 14B + IP 头 20B + TCP 头 20B,小数据时开销比例高)。 - 降低延迟抖动(对比 Nagle):不等待 ACK,因此在“突发小数据 + 后跟大数据”场景下,延迟更低。 |
一句话总结:tcp_autocorking 不是通过修改 TCP 选项实现的,而是通过内核在发送路径上的有条件的 skb 追加合并实现的,它把多次系统调用的数据“复合”成一个较大的内核数据结构块,然后一次性推送给驱动,从而实现了对小包的自主合并(compound),兼顾了低延迟(无 ACK 等待)和低开销(少发送次数)。