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 加速。每一步都有优化空间。

物理层与数据链路层优化知识体系 网卡调优 中断合并 RSS XDP 基础 Ring Buffer 调优 Offload 功能开关 rx-usecs / tx-usecs rx-frames / tx-frames 队列数配置 哈希算法选择 丢弃 / 转发 / 修改 / 透传 延迟优化方向:硬件 → 中断 → 多核 → 驱动层

嗯,到这里,物理层和数据链路层的优化就讲完了。记住一句话:低延迟不是调出来的,是设计出来的。从网卡开始,每一步都要精打细算。

核心总结:

  • 网卡 Ring Buffer 不要太大,1024 够用
  • 关闭 TSO/GRO,减少延迟
  • 中断合并从 1 微秒开始试
  • RSS 队列数等于物理核心数
  • XDP 适合简单逻辑,复杂逻辑回用户态

我曾经见过一个团队,花了两周调应用层代码,延迟只降了 5%。后来我帮他们调了网卡参数,延迟直接降了 40%。所以说,别忽视底层。底层优化,性价比最高。


公众号:蓝海资料掘金营,微信deep3321