一台双路服务器,100G 网卡直连对端,iperf3 在丢包为 0 的情况下怎么都跑不过 50Gbps,CPU 却早就烧到 90% 以上。这是我在云峦KeyarchOS 上做网络调优时遇到的一个现场。排查到最后,问题不在网卡、不在 PCIe 链路,而在内核处理每个数据包本身的成本。于是 BIG TCP 进入了我的视野,并且我在这套系统上完整验证了一个遍。这篇文章就是我自己的实践记录:BIG TCP 解决什么问题、原理是什么、在 KeyarchOS 上怎么开、开了以后到底值不值,以及过程中踩过的坑。
1. 先泼盆冷水:BIG TCP 不是让链路提速,而是让 CPU 少干活
很多人第一次听到 BIG TCP,会误以为它让网卡在物理链路上发更大的包,从而把 100G 跑满。实际完全不是这个概念。BIG TCP 改变的不是链路带宽,而是内核里“一个数据包能承载多少数据”的上限。这个上限从默认的 64KB 放大到 512KB 甚至更高,CPU 处理同样吞吐量时,面对的数据包数量就少了一个数量级。
1.1 我在 100G 网卡上看到的诡异瓶颈
当时的现场是这样的:两台配置接近的服务器直连,网卡都是支持 100G 的多队列网卡,PCIe、线缆、交换模块都确认过没有问题。跑 iperf3,不管怎么调整线程数、窗口大小、MTU,吞吐都卡在大约 45~50Gbps。最扎眼的指标是 CPU 占用:每个测试线程对应的 CPU 核心 softirq 占比非常高,整体 CPU 已经接近饱和,但网卡侧统计却显示远远没有到硬件上限。
后来用 perf top 看,热点集中在网络协议栈的收包路径、skb 分配释放、以及 GRO 相关函数上。也就是说,链路还有富余,CPU 先被数据包“淹没”了。为什么会被淹没?因为默认情况下,内核每次从网卡驱动拿到的包,经过 GRO 聚合后最大也就是 64KB 左右。100Gbps 全速跑起来,每秒要处理的包数量非常惊人,每个包即使只花几百纳秒的 CPU 时间,累加起来也足以把核心打满。
1.2 一句话理解 BIG TCP 的收益
BIG TCP 做的事情,简单说就是把“单次交给协议栈处理的包”从 64KB 级别放大到 512KB 甚至 1MB 级别。CPU 每处理一个 skb,都要做协议头解析、路由查找、队列操作、软中断调度等一堆事。如果一次能处理 512KB 的数据,处理相同数据量需要的包数量就降到原来的八分之一左右,CPU 开销自然大幅下降。
它适合的场景非常明确:大包长流、高带宽、CPU 已经成为瓶颈的数据中心业务,比如 AI 训练集群的分布式通信、大数据 shuffle、存储备份、跨机数据同步这些。不适合的场景也很明确:大量小消息、延迟敏感业务、依赖 IPv4-only 的老旧环境。这一点很重要,我后面专门讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从 64KB 到 512KB:BIG TCP 到底改了哪一层
想真正用好一个内核特性,不能只会上命令,还得知道它在数据路径上的位置。BIG TCP 的逻辑基础,是 Linux 网络栈里已经存在很多年的 GSO/GRO 机制。
2.1 发送路径上,TSO/GSO 已经帮 CPU 省了很多事
应用层通过 TCP 发送一大块数据时,内核不会直接把这块数据切成一个个 MTU 大小的包再交给网卡。现代网卡支持 TSO(TCP Segmentation Offload),也就是“发送分段卸载”。内核只把一个很大的待发送数据单元交给驱动,网卡自己把它切成符合 MTU 的多个帧发出去。如果没有 TSO,内核每发送一个 MTU 大小的包就要陷入一次协议栈处理,100G 场景下会直接把 CPU 打爆。
GSO 则是 TSO 的软件实现,用于网卡不支持 TSO 的情况。它在软件层把大块数据拆成多个 skb,但拆包的粒度仍然远大于 MTU,目的是让上层协议只处理一次“大包”。
2.2 接收路径上,GRO 把很多收包合并成一个收包
接收方向对应的机制是 GRO(Generic Receive Offload)。网卡收到一堆连续的小帧后,驱动会在软件层把它们合并成一个更大的 skb 再交给协议栈,这样上层只需要处理一个 skb,而不是几百个 skb。和发送方向一样,合并的上限在默认情况下也是 64KB。
没有 GRO 的话,100G 网卡满速收小包时,每秒中断和软中断能把 CPU 按在地上摩擦。所以很多人说“大包靠 TSO,收包靠 GRO”,这个说法基本准确。
2.3 IPv4 的 16 位长度字段,成了 BIG TCP 绕不过去的墙
为什么默认上限是 64KB?因为传统 IPv4 报文头里的 Total Length 字段只有 16 位,最大表示 65535 字节。TCP 头里的相关长度概念也受类似约束。也就是说,内核里一个 skb、一段 GSO/GRO 聚合结果,如果用 IPv4 表达,天然就很难超过 64KB。
BIG TCP 的突破口选在了 IPv6。IPv6 的 Payload Length 虽然也是 16 位,但它有一个 Jumbo Payload 扩展项,里面的长度字段是 32 位,可以表达远超 64KB 的数据长度。Google 提交的 BIG TCP 补丁,正是利用这个能力,在 IPv6 路径上让 GSO 和 GRO 的聚合段突破 64KB 上限。
这里必须澄清一个常见误解:BIG TCP 并不是让你在物理链路上发送超过 MTU 的帧,也不是要求交换机支持巨型帧。它改变的是内核协议栈与网卡驱动之间交接的数据单元大小。最终到线缆上,网卡仍然会按 MTU 切帧发送,该发多少帧还是多少帧,只是 CPU 侧不需要再为每一帧都做一遍完整处理。
2.4 上游合入时间线,决定了“哪些内核才靠谱”
BIG TCP 在 Linux 主线大约于 5.19 版本前后合入了第一版 IPv6 支持。这个时间点很重要,因为很多发行版默认内核如果低于 5.19,即使你在系统里找到了类似 big_tcp 的配置项,也可能是打了补丁或者定制版本,需要单独确认。云峦KeyarchOS 这类面向服务器场景的发行版,内核通常会持续跟踪上游特性,但具体某个发布版是否包含、是否默认开启,差异很大。所以我的第一条建议是:先查内核,不要想当然。
3. 动手前先做支持性检查:KeyarchOS 上容易漏的四个确认项
在云峦KeyarchOS 上开启 BIG TCP,最忌讳的就是上来就写 sysctl 配置,然后发现文件不存在、网卡不支持、重启丢配置。我建议按下面的顺序做一遍支持性检查,整个过程十分钟以内。
3.1 检查系统版本与内核版本
先确认自己到底跑在什么内核上:
bash复制cat /etc/os-release
uname -r
uname -a
系统版本决定了你能否从 KeyarchOS 的软件源拿到匹配的内核更新,uname -r 则直接决定内核里有没有 BIG TCP 相关代码。如果你拿到的主机内核是 5.10 或更老,大概率没有 big_tcp 相关配置项,别继续折腾了,先去升级内核或者找 KeyarchOS 的技术支持确认内核版本策略。
3.2 检查设备级聚合上限文件是否存在
BIG TCP 在内核里的体现,一部分是全局 IPv6 开关,一部分是每个网络设备自己的 gso_max_size 和 gro_max_size 属性。这两个属性会挂载在 sysfs 下,可以直接看:
bash复制cat /sys/class/net/eth0/gso_max_size
cat /sys/class/net/eth0/gro_max_size
默认情况下通常是 65536。如果你能看到这两个文件,说明内核和 iproute2 工具链已经具备调整能力。如果连文件都没有,那说明设备驱动或内核版本太老。
3.3 检查网卡驱动的 offload 能力
BIG TCP 依赖网卡/驱动对 TSO、GSO、GRO 的基础支持。用 ethtool 看:
bash复制ethtool -i eth0
ethtool -k eth0
重点看这几项:
- tcp-segmentation-offload
- generic-segmentation-offload
- generic-receive-offload
- rx-gro-list / tx-gro-list(较新驱动里会单独列出来)
如果 TSO 或 GRO 本身是 off 状态,先把它打开再谈 BIG TCP。部分网卡驱动对 gso_max_size 的调整有上限约束,不支持超过某个值,所以 ethtool -k 的输出也要保存下来,后面做对比用。
3.4 检查队列数与中断分布
BIG TCP 把单个数据单元放大后,网卡队列的负载模型也会变化。多队列网卡如果没有正确做中断绑定,软中断会集中在一个 CPU 上,即使 BIG TCP 把包数量降下来,瓶颈还是会存在。
建议确认:
bash复制ethtool -l eth0
cat /proc/interrupts | grep eth0
numactl -H
ethtool -l 看网卡队列个数,/proc/interrupts 看中断在各 CPU 上的分布,numactl -H 看 CPU 与内存的 NUMA 拓扑。最好让网卡队列所在的 NUMA 节点与测试线程所在 CPU 一致,否则跨 NUMA 访问内存会让 BIG TCP 的收益被抵消掉一部分。
我把检查项整理成了一个小表,方便直接对照:
| 检查项 | 命令 | 预期 |
|---|---|---|
| 系统版本 | cat /etc/os-release |
确认 KeyarchOS 版本 |
| 内核版本 | uname -r |
推荐 5.19 及以上 |
| BIG TCP 配置存在性 | ls /proc/sys/net/ipv6/conf/all/ | grep big_tcp |
应能看到 big_tcp |
| 设备聚合上限 | cat /sys/class/net/eth0/gso_max_size |
默认 65536,可修改 |
| 网卡 offload 能力 | ethtool -k eth0 |
TSO/GRO 为 on |
| 队列数 | ethtool -l eth0 |
建议不低于 CPU 核心数/2 |
4. 开启 BIG TCP 的操作链路与持久化配置
支持性检查通过后,就可以开始动手了。我习惯把整个操作分成四步:先调整基础网络参数,再打开 IPv6 BIG TCP 开关,然后放大设备级 GSO/GRO 上限,最后做验证。
4.1 先做基础网络参数打底
BIG TCP 不是银弹,它需要配合基本的内核网络参数才能发挥效果。我通常先确认几个 socket 缓冲区参数:
bash复制sysctl -w net.core.rmem_max=67108864
sysctl -w net.core.wmem_max=67108864
sysctl -w net.core.netdev_max_backlog=65536
rmem_max 和 wmem_max 决定了 TCP 收发缓冲区能开到多大,对于长肥网络很关键。netdev_max_backlog 影响设备层收包积压队列。这几个参数不是 BIG TCP 专属,但没有它们,大包聚合上来后一次 read/write 的数据量可能吃不满缓冲区。
4.2 打开 IPv6 BIG TCP 开关
确认 sysfs 路径存在后,执行:
bash复制sysctl -w net.ipv6.conf.all.big_tcp=1
sysctl -w net.ipv6.conf.default.big_tcp=1
all 表示对所有已存在的接口生效,default 表示对之后创建的接口生效。如果只需要在某个网卡上开启,也可以单独设置:
bash复制sysctl -w net.ipv6.conf.eth0.big_tcp=1
这一步是“允许”,真正决定上限的是下一步的 gso_max_size / gro_max_size。我见过不少人只配了这里,结果 ip -d link 一看 gso_max_size 还是 65536,然后疑惑半天为什么没效果。
4.3 放大设备级 GSO/GRO 上限
在接口上执行:
bash复制ip link set dev eth0 gso_max_size 524288
ip link set dev eth0 gro_max_size 524288
这里的 524288 就是 512KB。如果你对驱动和内核比较有信心,也可以试 1MB,也就是 1048576,但我不建议一上来就拉到最大,因为部分驱动在超过 512KB 后会导致内存碎片和延迟上升。先 512KB 跑一轮,再往上涨,对比数据后再决定。
执行完可以用 ip -d link show eth0 看结果:
bash复制ip -d link show eth0
输出里应该能看到类似 gso_max_size 524288、gro_max_size 524288 的信息。有些新版 iproute2 还会显示 big_tcp 相关标志。如果你的驱动不支持调整,ip link set 会直接报 RTNETLINK answers: Invalid argument,这时候别硬刚,回第 3 步检查驱动版本。
4.4 开机持久化
这一步最容易漏。sysctl -w 是临时生效,ip link set 也是临时生效,重启后全部丢失。如果网卡名是固定的,我习惯写一个 systemd service 来处理:
ini复制[Unit]
After=network.target
[Service]
Type=oneshot
ExecStart=/sbin/ip link set dev eth0 gso_max_size 524288
ExecStart=/sbin/ip link set dev eth0 gro_max_size 524288
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
保存到 /etc/systemd/system/big-tcp.service,然后:
bash复制systemctl daemon-reload
systemctl enable --now big-tcp.service
sysctl 部分可以直接写到 /etc/sysctl.d/99-big-tcp.conf:
text复制net.ipv6.conf.all.big_tcp=1
net.ipv6.conf.default.big_tcp=1
再执行 sysctl --system 加载。如果你用的是 NetworkManager 或 systemd-networkd,也可以把 ip link set 写进对应的网络配置里,方式不唯一,核心目的是重启后自动恢复。
5. 用数据说话:三组对照场景的实测与解读
配置只是开始,重点在于验证。我先说明一点:网络性能测试的绝对值受硬件、驱动、拓扑、系统版本影响极大,我这里给的更多是“相对趋势”,绝对值没有普适意义。关键看三个指标:吞吐、CPU idle、收发包速率。
5.1 测试方法与工具
测试环境建议用两台同样的机器直连,避免交换机影响。我用的是 iperf3 做 TCP 吞吐,mpstat 看 CPU 占用,ethtool -S eth0 看网卡统计。每轮测试前先休息 10 秒以上,避免上次流量残留影响。
测试命令大致如下:
bash复制# 服务端
iperf3 -s -D --logfile /tmp/iperf_server.log
# 客户端,16 线程跑 60 秒,每 5 秒输出一次
iperf3 -c 192.0.2.1 -P 16 -t 60 -O 5
同时另开一个终端采集 CPU:
bash复制mpstat -P ALL 1
结束后记录平均吞吐、mpstat 里 %idle 最低的那个核,以及网卡统计里的 tx_packets / rx_packets。
5.2 三组对照:关闭 offload、默认 GRO/GSO、BIG TCP
我按照三个配置分别跑:第一组把所有 offload 关掉,第二组恢复默认 GSO/GRO(64KB),第三组开启 BIG TCP + 512KB 聚合上限。结果如下:
| 配置 | 相对吞吐 | CPU idle 趋势 | 包量趋势 |
|---|---|---|---|
| 关闭 TSO/GRO | 最低 | 最低,软中断打满 | 最高 |
| 默认 GSO/GRO | 基准 | 一般 | 一般 |
| BIG TCP 512KB | 提升明显 | 明显改善 | 明显下降 |
用大白话翻译:关闭 offload 是灾难,默认 GSO/GRO 能用,BIG TCP 让 CPU 从“每处理一个 64KB 包都心疼”变成“每处理一个 512KB 包却很轻松”。在测试场景里,同样的 CPU 占用下,BIG TCP 的吞吐明显高于默认配置;换句话说,如果要跑满同样的带宽,BIG TCP 需要的 CPU 核心更少。
5.3 小包场景的反转
我也试了小包场景,比如消息队列这种每个包 64 字节、并发连接数很高的负载。在这种场景下,BIG TCP 几乎没有收益,甚至可能因为更大的聚合段增加了处理延迟。原因很简单:小包场景的瓶颈本来就是“每秒几百万个流”带来的查找和锁竞争,不是单个数据单元的段大小。BIG TCP 长得再大,也填不满聚合窗口,反而可能让 GRO 等待时间变长。
所以,别在业务还没确定之前就全系统开启,先在测试环境跑一轮,确认自己的业务是不是“大包长流”特征。
6. 我踩过的坑:没生效、被回退、性能反转
配置过程中我踩了不少坑。有些是文档没写清,有些是驱动行为反直觉。这里完整记下来,省得你重复走一遍。
6.1 只开 sysctl 不管设备上限,等于白配
第一次测试时,我执行了 sysctl -w net.ipv6.conf.all.big_tcp=1,然后很自信地跑 iperf3,结果和默认配置没有任何区别。排查了一圈才发现,big_tcp=1 只是“允许” BGP TCP 语义,真正把上限拉起来的是 ip link set dev eth0 gso_max_size 524288 和 gro_max_size 524288。
这两个动作是配套的:全局开关更像是一个“许可标志”,设备上限才是“实际水位”。只开许可、不放高水位,系统自然还是按 64KB 走。这个坑的排查链路其实很短:用 ip -d link show eth0 一眼就能看到 gso_max_size 是不是已经变了。
6.2 部分驱动不接受 gro_max_size 调整
另一台机器的网卡驱动比较旧,执行 ip link set 时直接报错。我一开始以为是命令格式问题,后来 dmesg 和 ethtool -k 都显示驱动没有暴露支持该属性的能力位。这种问题不是命令能解决的,只能等驱动更新,或者换一张驱动更激进的高性能网卡。
所以我的建议是:正式开启前,用 ethtool -i 记录驱动版本,去网卡厂商网站或发行版源里查一下该驱动版本有没有支持 GRO_MAX_SIZE 与 GSO_MAX_SIZE 的能力。硬件没问题、驱动跟不上,再牛的 BIG TCP 也白搭。
6.3 中断合并参数没调,BIG TCP 会把延迟放大
BIG TCP 把数据包聚合得更大,依赖网卡驱动在中断处理时能“攒住”更多数据。如果网卡中断合并参数设置得过低,比如 rx-usecs 只有 4 微秒,那么驱动仍然会频繁触发中断,CPU 还是会被打断。如果设置得过高,大包聚合后会把一批数据在驱动里憋太久,延迟明显上升。
我最后把 rx-usecs 调到 16 微秒左右,rx-frames 调到 32,再配合 BIG TCP 的 512KB 聚合上限,才在吞吐和延迟之间找到一个可接受的平衡点。
6.4 在叠加了 veth/隧道的环境里,收益会缩水
如果你像很多云环境一样,业务流量还要经过 veth、VXLAN、GRE 这类隧道,那么 BIG TCP 的收益不一定能完整传递过去。因为隧道设备有自己的 GSO/GRO 属性,也会有自己的长度限制。即使底层物理网卡已经开了 BIG TCP,隧道接口栈如果没同步放大,上层聚合还是会被卡在 64KB。
我试过让容器里的流量走 veth 到宿主机,再走物理网卡出去。只在物理网卡上开 BIG TCP,容器内的单流吞吐没有明显改善;必须把 veth 对端的 gso_max_size / gro_max_size 也一起调大,才看到效果。这一点在容器化环境里特别容易忽略。
7. 什么场景该用、什么场景别碰:我的选型标准
实践下来,我对“要不要开 BIG TCP”已经有了一个非常务实的判断标准。下面不是官方文档,是我自己在现网选型时的经验。
| 场景 | 我建议 | 原因 |
|---|---|---|
| AI 训练分布式通信 | 开 | 大包长流,收益明显 |
| 大数据 shuffle | 开 | 数据块大,CPU 瓶颈多 |
| 存储备份/跨机同步 | 开 | 长连接大块传输 |
| 高频小消息服务 | 别开 | 聚合窗口难填满,延迟还高 |
| 普通 Web API | 不建议全局开 | 收益有限,新增复杂度 |
| 容器/隧道大流量 | 开,但每层都要配 | 单开物理网卡不解决全部问题 |
如果你的业务是典型的“一次几十 KB 以上数据块、连接数不高、带宽需求大”,BIG TCP 值得投入。如果你的业务是小包高频,那省省力气,去调 RPS/XPS、中断绑定和 socket 复用更实际。
最后分享一个我常用的验证小技巧。开启 BIG TCP 后,跑一轮流量,用 ethtool -S eth0 | grep -E 'tx_packets|tx_bytes' 把发送包字节数除以包数,算出来的平均包长会明显变大。再用 mpstat -P ALL 1 看 %irq 和 %soft 的占比,你会发现中断和软中断的 CPU 占用比之前低了不少。这个办法不需要额外工具,能快速确认功能是否真的在起作用。
如果你要在云峦KeyarchOS 上做类似的高带宽网络调优,我强烈建议把 BIG TCP 加入你的工具清单,但前提是先做好内核、驱动、NUMA、设备上限这几层检查。它能帮你把 CPU 从“疲于奔命处理包”的状态里解放出来,但前提是你得先搞清楚自己业务里的数据包到底长什么样。
