在高速网络接收路径上,每处理一个数据包都要经历网卡中断、NAPI轮询、skb分配、协议头解析、校验和验证、TCP状态机处理等多个步骤。当单个连接的数据段很短时,大量CPU时间被消耗在固定开销上,而不是真正搬运数据。Large Receive Offload(LRO)尝试在靠近网卡的层次把多个连续TCP段合并成一个更大的段,再一次性送入内核协议栈。这样内核只需要处理一次skb生命周期,协议栈也只针对合并后的段计算一次校验、查找一次socket。

一、LRO如何把多个段合并成一个
LRO通常实现在网卡驱动或网卡硬件中。以Linux内核的NAPI收包流程为例,驱动在软中断里从接收环取出多个描述符,每个描述符对应一个到来的以太网帧。驱动先不急着把每个skb单独送给协议栈,而是检查相邻skb是否满足合并条件。合并条件包括:TCP连接相同,即源IP、目的IP、源端口、目的端口一致;序列号连续,下一个段的序列号正好等于当前段的序列号加上当前段有效载荷长度;PSH标志没有被置位;TCP选项一致,避免时间戳或SACK选项在合并后失效;IP标识、DF位等字段也能对齐。
满足条件后,驱动会修改聚合skb的线性区和分片区,把后续段的payload追加到同一个skb的数据区,同时更新skb的长度字段和聚合信息。合并后的skb通常被标记为GSO段,记录每个子段的MSS,以便后续发送或转发时能够重新切分。例如一个10GB网卡接收64KB的数据,如果由16个4KB段组成,LRO可以将其合并成一个64KB的skb,协议栈只需处理一次。对于受CPU限制而非带宽限制的服务器,这能显著提高有效吞吐。
#include <stdio.h>
struct lro_packet {
unsigned int seq;
unsigned int next_seq;
unsigned int psh:1;
unsigned int syn:1;
};
static int lro_can_merge(struct lro_packet *a, struct lro_packet *b)
{
if (a->psh || a->syn || b->psh || b->syn)
return 0;
if (a->next_seq != b->seq)
return 0;
return 1;
}
int main(void)
{
struct lro_packet p1 = {1000, 2000, 0, 0};
struct lro_packet p2 = {2000, 3000, 0, 0};
if (lro_can_merge(&p1, &p2))
printf("can merge\n");
return 0;
}
上面这段简化代码展示了合并前最基本的两个判断:是否带PSH或SYN标志,以及序列号是否刚好衔接。实际内核中的lro_mgr和lro_frame结构要复杂得多,还需要考虑IP分片、校验和卸载状态、VLAN标签等。但无论实现细节如何,核心思想都是把协议栈处理从“按包次数”转变为“按数据批次”,从而摊薄CPU开销。
二、LRO与GRO:硬件合并与软件合并的边界
LRO经常和Generic Receive Offload(GRO)放在一起比较。GRO是内核提供的通用接收合并机制,不依赖特定网卡,只要驱动在NAPI轮询时把skb送入协议栈,GRO层就能在软中断中执行合并。GRO的合并条件比LRO更严格,会保留更多原始包信息,因此可以在转发、桥接、Netfilter等路径使用。LRO则发生在驱动或硬件层,合并更早,理论上节省的中断和软中断开销更多,但也更容易破坏转发语义。
两者最重要的差异在于对IP转发和数据包观测的影响。LRO合并后的skb在转发节点上可能超过出接口MTU,如果网卡不支持分段卸载,内核必须重新分片,但重组后的IP分片可能丢失原始TCP段的边界。抓包工具在接收端看到的是巨型TCP段,实际发送方根本没有这么大的段,这会误导排障。GRO虽然也聚合,但它处于协议栈入口,内核可以在必要时重新分离成原始段,对转发和抓包更友好。
| 特性 | LRO | GRO |
|---|---|---|
| 合并位置 | 网卡硬件或驱动 | 内核协议栈入口 |
| 硬件依赖 | 需要网卡支持 | 内核通用能力 |
| 转发节点适用性 | 通常不推荐 | 推荐 |
| 抓包观测 | 可能显示异常大段 | 可保留原始段信息 |
| CPU节省效果 | 更早合并,效果更明显 | 稍弱,但更安全 |
因此,纯主机接收TCP流量的场景,LRO收益较高;而任何需要转发、NAT、隧道、防火墙、负载均衡的场景,都应该优先使用GRO,甚至显式关闭LRO。很多云厂商的虚拟网卡默认关闭LRO,正是为了避免虚拟化环境中的语义破坏。
三、在Linux中启用、关闭和验证LRO
Linux中管理网卡卸载功能最直接的工具是ethtool。下面先查看网卡当前的LRO状态。不同驱动对LRO的命名可能有差异,多数使用large-receive-offload,但某些老版本驱动可能使用rx-lro或lro。执行后如果显示large-receive-offload: on,说明LRO已经启用。
# 查看eth0的LRO状态 ethtool -k eth0 | grep large-receive-offload # 启用LRO ethtool -K eth0 lro on # 关闭LRO ethtool -K eth0 lro off
启用或关闭后,可以通过ethtool -S查看网卡统计计数器变化。有些驱动会提供lro_aggregated、lro_flushed等统计项,能直接看到合并发生的次数。如果计数器不增长,说明网卡或驱动并不支持LRO,或者流量模式不满足合并条件。还可以用perf top观察软中断和协议栈函数的热点变化,开通LRO前后对比,通常能发现tcp_ack、ip_rcv等函数占用下降。
对于使用systemd-networkd或NetworkManager的系统,可以将ethtool命令写入udev规则或网络配置文件的post-up脚本,确保重启后仍然生效。例如在/etc/network/interfaces中添加up ethtool -K eth0 lro on。但要注意,如果网卡驱动不支持LRO,ethtool会返回Operation not supported,此时不应强制开启。
四、LRO的副作用与场景取舍
LRO并非没有代价。最大的问题是它改变了TCP段边界,可能影响TCP时间戳、SACK块和拥塞窗口计算。合并后内核看到的TCP报文长度远大于实际发送的段,如果接收端需要基于每个原始段生成ACK,LRO会延迟确认,甚至改变RTT测量。在丢包重传时,SACK块可能无法精确描述丢失的段,导致发送端重传更多数据。TCP时间戳选项在合并时也可能出现不连续,影响RTTM算法。
另一个典型问题出现在虚拟化和容器网络。虚拟机之间通过vSwitch转发时,宿主机上的LRO会把虚拟机发出的多个TCP段合并成一个,导致VM的vNIC接收到的段大小异常,甚至超过MTU。如果虚拟网卡没有开启相应的分段卸载,数据包会被丢弃或分片。因此,VMware、KVM等虚拟化平台的最佳实践通常是在宿主机物理网卡上关闭LRO,改由虚拟机内的GRO完成聚合。
对于纯接收型的高吞吐服务,比如视频流媒体服务器、文件下载服务器、日志采集节点,LRO可以降低CPU使用率,提高单连接吞吐。但如果服务同时承担转发、代理或深度包检测,或者需要精确抓包排障,则建议关闭LRO。折中方案是在物理网卡上开启LRO,但在网络命名空间或veth接口上启用GRO,不过同一条路径上不要同时启用多个聚合层,否则可能产生二次合并,增加复杂性。
最后,配置变更后应使用iperf3或netperf进行对比测试,并抓取接收端报文长度统计。观察平均包长、CPU占用、吞吐和重传率四个指标。如果吞吐提升明显、CPU下降且重传率没有上升,说明LRO适合当前业务;如果出现TCP乱序或重传增加,应立即回退。性能优化没有全局最优,只有结合流量特征和协议栈行为的持续验证。
Large Receive Offload接收段合并CPU负载修改时间:2026-09-17 13:56:53