3. 物理层与数据链路层优化:网卡调优、中断合并、RSS、XDP基础
各位同学,咱们今天聊点硬核的。很多人写网络程序,上来就调 socket 参数,改内核配置。但说实话,真正的低延迟,得从网卡开始。物理层和数据链路层,这是数据进出的第一道门。门没开好,后面再怎么折腾也是白搭。
我这些年调过不少网卡,从千兆到 100G,从 Intel 到 Mellanox。踩过的坑,比你们吃过的盐还多(笑)。今天就把这些经验掰开揉碎了讲给你们听。
3.1 网卡调优:从硬件层面开始
网卡调优,说白了就是让硬件更听话。默认配置通常是为了兼容性,不是为了性能。你得自己动手改。
3.1.1 网卡队列与 Ring Buffer
每个网卡都有多个队列。数据包进来,先放到 Ring Buffer 里。这个 Buffer 的大小,直接影响丢包率。
Ring Buffer 调优原则:
- 太小:流量突发时直接丢包,你连反应的时间都没有
- 太大:延迟增加,数据包在 Buffer 里排队等处理
我个人习惯,对于低延迟场景,Ring Buffer 设成 1024 或 2048。别太大。为什么?因为我们要的是快,不是能装。数据包进来就得立刻处理,别让它在 Buffer 里睡觉。
核心观点:低延迟场景下,宁可丢包重传,也不要让数据在 Buffer 里排队。丢包是网络问题,排队是性能问题。
查看当前 Ring Buffer 大小:
ethtool -g eth0
修改 Ring Buffer 大小:
ethtool -G eth0 rx 1024 tx 1024
3.1.2 网卡 Offload 功能
网卡 Offload,就是把一些 CPU 干的活,交给网卡干。比如校验和计算、TCP 分段、大包重组等。
我在项目中遇到过一个问题:某台机器延迟突然飙升。查了半天,发现是 TSO(TCP Segmentation Offload)搞的鬼。网卡把大包拆成小包,但拆得太慢,导致发送队列积压。
低延迟场景建议关闭的 Offload:
- TSO / GSO:TCP 分段 Offload,关闭后由 CPU 分段,更可控
- GRO / LRO:接收端合并,关闭后每个小包单独处理,延迟更低
- RXCSUM / TXCSUM:校验和 Offload,建议保留,这个影响不大
小技巧:用 ethtool -k eth0 查看当前 Offload 状态。用 ethtool -K eth0 tso off 关闭特定功能。
3.2 中断合并:延迟与吞吐量的博弈
中断合并,英文叫 Interrupt Coalescing。这玩意儿是个双刃剑。
原理很简单:网卡收到数据包后,不立刻发中断通知 CPU,而是等一会儿,攒几个包再一起通知。这样 CPU 被中断的次数少了,吞吐量上去了。但代价呢?延迟增加了。
为什么会这样?你想想看,每个包都要等一会儿才能被处理。对于高吞吐场景,这没问题。但对于低延迟场景,这就是灾难。
中断合并参数:
| 参数 | 说明 | 低延迟推荐值 |
|---|---|---|
| rx-usecs | 接收中断延迟(微秒) | 0 或 1 |
| tx-usecs | 发送中断延迟(微秒) | 0 或 1 |
| rx-frames | 接收多少个帧后触发中断 | 1 |
| tx-frames | 发送多少个帧后触发中断 | 1 |
我曾经在一个高频交易项目中,把 rx-usecs 设成 0。结果呢?CPU 使用率直接飙到 80%,全是中断处理。但延迟降到了 5 微秒以下。值不值?看你的业务场景。
警告:把中断合并完全关闭(设为 0),会导致 CPU 被中断淹没。特别是 10G 以上网卡,每秒几百万个包,CPU 根本忙不过来。建议从 1 微秒开始试。
设置中断合并:
ethtool -C eth0 rx-usecs 1 tx-usecs 1
3.3 RSS:让多核 CPU 各司其职
RSS,全称 Receive Side Scaling。说白了,就是让多个 CPU 核心一起处理网络数据包,别让一个核心累死。
原理:网卡根据数据包的哈希值(比如 IP、端口),把不同的流分配到不同的队列。每个队列绑定一个 CPU 核心。这样,多个核心并行处理,吞吐量翻倍。
RSS 配置要点:
- 队列数:通常等于 CPU 核心数,或者核心数的一半
- 哈希算法:默认是 Toeplitz,可以改成对称哈希(Symmetric RSS)
- 哈希字段:根据需求选择,比如只哈希 IP 还是哈希 IP+端口
查看 RSS 队列数:
ethtool -l eth0
设置 RSS 队列数:
ethtool -L eth0 combined 8
注意:RSS 的哈希计算是在硬件层面完成的,不消耗 CPU。这是它比软件负载均衡高效的原因。
我建议,对于低延迟场景,把 RSS 队列数设成物理核心数。超线程核心不算。为什么?因为超线程核心共享 L1/L2 缓存,两个逻辑核心抢缓存,反而增加延迟。
3.4 XDP 基础:在网卡驱动层动手脚
XDP,全称 eXpress Data Path。这是 Linux 内核提供的一个框架,让你能在网卡驱动层处理数据包。注意,是驱动层,不是协议栈层。
传统路径:网卡 → 驱动 → 协议栈 → socket → 应用程序。每一步都有开销。
XDP 路径:网卡 → 驱动 → XDP 程序 → 应用程序。跳过了协议栈,延迟大幅降低。
XDP 能做什么?
- 丢弃:直接丢掉不需要的包,比如 DDoS 攻击包
- 转发:把包转发到另一个网卡,实现高速转发
- 修改:修改包内容,比如改 MAC 地址
- 透传:把包传给协议栈或用户态程序
下面是一个简单的 XDP 程序,丢弃所有非 UDP 包:
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>
SEC("xdp")
int xdp_drop_non_udp(struct xdp_md *ctx) {
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_DROP;
if (eth->h_proto != htons(ETH_P_IP))
return XDP_DROP;
struct iphdr *ip = data + sizeof(*eth);
if ((void *)(ip + 1) > data_end)
return XDP_DROP;
if (ip->protocol != IPPROTO_UDP)
return XDP_DROP;
return XDP_PASS;
}
加载 XDP 程序:
ip link set dev eth0 xdp obj xdp_drop_non_udp.o sec xdp
提示:XDP 程序运行在网卡驱动上下文,不能调用内核 API,也不能睡眠。写 XDP 程序时,要时刻记住:你是在中断上下文干活。
我在一个项目中,用 XDP 实现了自定义的负载均衡。延迟从 50 微秒降到了 10 微秒以下。效果立竿见影。但要注意,XDP 不是万能的。它只适合处理简单逻辑。复杂逻辑还是得回到用户态。
3.5 本章知识体系
下面这张图,展示了物理层和数据链路层优化的核心逻辑。从网卡硬件开始,到中断处理,再到多核分发,最后到 XDP 加速。每一步都有优化空间。
嗯,到这里,物理层和数据链路层的优化就讲完了。记住一句话:低延迟不是调出来的,是设计出来的。从网卡开始,每一步都要精打细算。
核心总结:
- 网卡 Ring Buffer 不要太大,1024 够用
- 关闭 TSO/GRO,减少延迟
- 中断合并从 1 微秒开始试
- RSS 队列数等于物理核心数
- XDP 适合简单逻辑,复杂逻辑回用户态
我曾经见过一个团队,花了两周调应用层代码,延迟只降了 5%。后来我帮他们调了网卡参数,延迟直接降了 40%。所以说,别忽视底层。底层优化,性价比最高。
公众号:蓝海资料掘金营,微信deep3321