Kubernetes 集群中,节点物理网卡的接收队列数量往往被忽略。默认状态下,不少 10G、25G 网卡只启用了一个 combined 队列,所有入站数据包都通过同一个中断交给同一个 CPU 核心处理。Pod 之间的东西向流量一旦增大,就会出现单核心软中断打满、其他核心空闲的情况,直接拖慢 Service 转发和 CNI 处理效率。这个问题在裸金属 K8s 环境里更加明显,因为那里通常没有虚拟化层的多队列抽象,物理网卡的真实能力需要手动确认和调整。

多队列网卡与 RSS 如何协同工作
多队列网卡指的是硬件层面支持多个收发队列的网络接口卡。每个队列都有自己的中断号和内存描述符环,驱动可以将不同数据流映射到不同队列。这样设计的目的是充分利用多核 CPU,把网络处理负载拆开。支持多队列的网卡在 Linux 下通常显示为 Combined、RX、TX 等队列类型,其中 Combined 是收发合并队列,RX 是接收队列,TX 是发送队列。一个队列在同一时刻只能触发一个中断,如果只有一个队列,那么所有数据包最终都会汇聚到一个核心上。
RSS 的全称是 Receive Side Scaling,接收端扩展。它由硬件完成哈希计算,根据数据包的源 IP、目的 IP、源端口、目的端口等字段生成一个哈希值,再用这个哈希值在间接表里查找对应的队列编号。这样同一条 TCP 连接的数据包始终进入同一个接收队列,保证顺序性,而不同连接可以分散到不同队列。RSS 与多队列网卡是配合关系:多队列提供硬件通道,RSS 决定数据包走哪条通道。如果网卡只有 1 个队列,RSS 相当于没有效果;如果队列很多但哈希表配置不合理,也可能导致某些队列负载过高。
在 Kubernetes 节点上,物理网卡接收到的流量包括 Pod 访问外部服务、NodePort 入站流量、以及跨节点 Pod 通信。这些流量经过网卡、内核协议栈、CNI 网桥或 veth 设备,最后到达 Pod。链路最前端的物理网卡如果只启用单队列,后续所有软件优化都会受制于这个瓶颈。因此,先确认网卡是否支持多队列,再把队列数量调整到与可用 CPU 核心数匹配的水平,是节点网络调优的第一步。
# 查看网卡支持的队列上限 ethtool -l eth0 # 常见输出示例 # Channel parameters for eth0: # Pre-set maximums: # RX: 0 # TX: 0 # Other: 0 # Combined: 16 # Current hardware settings: # RX: 0 # TX: 0 # Other: 0 # Combined: 1
在 Kubernetes 节点上调整网卡队列数量
从上面的输出可以看到,示例网卡最多支持 16 个 combined 队列,但当前只使用了 1 个。调整队列数量使用 ethtool 的 -L 选项。建议先将队列数设置为与可用物理核心数相近的值,而不是盲目拉到最大值。比如一台 16 核的节点,可以设置 combined 8 或 combined 16。队列数过多本身不会带来额外收益,反而会增加中断管理的开销;过少则无法充分分散负载。生产环境中一般建议队列数等于或略小于用于网络处理的 CPU 核心数。
设置队列后还需要配置 RSS 哈希分布。ethtool -X 可以调整间接表和哈希密钥。最简单的方式是使用 equal 参数,让哈希结果尽可能均匀地分布到所有队列。如果没有特殊业务需求,不需要手动修改哈希密钥。对于 Kubernetes 场景,Pod IP 变化频繁,源端口和目的端口变动也很大,默认的四元组哈希通常足够均匀。也可以查看当前 RSS 配置,确认每个队列对应的哈希结果数量是否接近。
# 将 eth0 的 combined 队列设置为 8 ethtool -L eth0 combined 8 # 将 RSS 间接表均匀分配到 8 个队列 ethtool -X eth0 equal 8 # 查看 RSS 间接表 ethtool -x eth0 # 查看接收队列哈希统计 ethtool -S eth0 | grep -i rx_queue
设置完成后,系统不会自动迁移已有中断处理。新到达的数据包会按照新的间接表进入不同队列,但已经建立连接的流量仍受原队列影响。对于 Kubernetes 节点,Pod 生命周期较短,连接会不断新建和销毁,所以一般不需要重启网络服务。如果需要立即生效且不受旧连接影响,可以重启节点或重新加载网卡驱动,但在生产环境应谨慎操作。
中断亲和性与软中断的分配
网卡队列数量调整后,每个队列都会对应一个独立中断号。这些中断默认可能集中在 CPU0 上,即使队列数增加了,软中断依然没有分散。此时需要为每个队列的中断设置 smp_affinity,把不同中断绑定到不同 CPU 核心。手动绑定之前,可以先查看 /proc/interrupts 文件,找到网卡对应的所有中断号。通常 mlx5、i40e、ixgbe 等驱动会为每个队列注册一个中断,名称中带有网卡名和队列编号。
# 查看 eth0 相关中断 cat /proc/interrupts | grep eth0 # 示例输出 # 123: 12345678 PCI-MSI 16-edge eth0-rx-0 # 124: 23456789 PCI-MSI 17-edge eth0-rx-1 # 125: 12345678 PCI-MSI 18-edge eth0-tx-0 # 126: 23456789 PCI-MSI 19-edge eth0-tx-1
把中断号 123 绑定到 CPU2,可以执行 echo 2 > /proc/irq/123/smp_affinity_list。手动绑定时要避免把多个高负载中断绑定到同一个核心,尤其是 CPU0,因为 CPU0 通常还要处理系统定时器和其他管理任务。对于 8 个队列的网卡,可以分别绑定到 CPU1 到 CPU8,或者绑到编号为偶数的核心上,视具体情况而定。另外,irqbalance 服务会自动调整中断亲和性,但它对网络中断的分配策略不一定符合 K8s 节点的实际负载,建议在调优时先关闭 irqbalance,再手动绑定,或者配置 irqbalance 的 banned list。
# 关闭 irqbalance 服务 systemctl stop irqbalance systemctl disable irqbalance # 将中断 123 绑定到 CPU2 echo 2 > /proc/irq/123/smp_affinity_list # 查看绑定结果 cat /proc/irq/123/smp_affinity_list
只配置 RSS 而不绑定中断,负载会从单队列瓶颈转移到默认中断核心瓶颈。反过来,只绑定中断而不增加队列数量,硬件层面仍然只有一条接收通道。两者必须配合使用。Kubernetes 节点上如果运行了 Pod 独占 CPU 的 workload,还需要注意不要让网卡中断和业务 Pod 争抢同一批核心,否则会出现延迟抖动。可以通过 cpuset 或拓扑管理器把网络中断核心排除在可分配 CPU 池之外,但这属于更进一步的调优范畴。
持久化配置与效果验证
直接使用 ethtool 命令修改的队列设置和中断绑定在重启后会丢失。生产环境需要把这些操作固化为 systemd service 或 udev 规则。systemd service 方式比较直观,可以在网络启动后执行队列调整和中断绑定脚本。注意 service 的 After 和 Wants 要包含 network-online.target,确保网卡已经就绪。如果网卡名称可能变化,脚本中应通过变量获取实际接口名,而不是硬编码 eth0。
# /etc/systemd/system/ethtool-rss.service [Unit] Description=Configure NIC queues and IRQ affinity After=network-online.target Wants=network-online.target [Service] Type=oneshot ExecStart=/usr/local/bin/set-nic-rss.sh RemainAfterExit=yes [Install] WantedBy=multi-user.target
# /usr/local/bin/set-nic-rss.sh
#!/bin/bash
NIC=eth0
QUEUES=8
ethtool -L $NIC combined $QUEUES
ethtool -X $NIC equal $QUEUES
# 遍历网卡中断并绑定到 CPU 1-8
cpu=1
for irq in $(grep "$NIC" /proc/interrupts | awk -F: '{print $1}' | tr -d ' '); do
echo $cpu > /proc/irq/$irq/smp_affinity_list
cpu=$((cpu + 1))
if [ $cpu -gt 8 ]; then
cpu=1
fi
done配置固化后,通过 systemctl enable ethtool-rss.service 启用服务。验证时重点看两个方面:一是确认队列数生效,二是确认各队列中断次数增长是否均匀。可以使用 watch -n 1 'cat /proc/interrupts | grep eth0' 观察实时中断变化。如果流量均衡,各 rx-0 到 rx-7 的计数应该在同一数量级。还可以用 mpstat -P ALL 1 查看各核心软中断占用,原本单核 100% 的情况应该消失,整体 si 占比会分散。
# 启用持久化服务 systemctl daemon-reload systemctl enable ethtool-rss.service systemctl start ethtool-rss.service # 实时观察各队列中断 watch -n 1 'cat /proc/interrupts | grep eth0' # 查看各 CPU 软中断占用 mpstat -P ALL 1
容器网络场景中的注意事项
RSS 和多队列网卡主要作用于物理网卡接收路径。Kubernetes 的 Pod 网络接口是 veth 或 virtio 设备,这些虚拟接口本身可能不支持硬件 RSS,或者支持多队列但需要独立配置。例如 virtio_net 在虚拟机场景中可以通过 ethtool -L 设置队列,但物理机上的 veth 设备没有硬件队列概念。对于 CNI 使用的 Linux bridge,入站物理网卡流量经过 RSS 分散后,进入 bridge 转发时已经是在不同 CPU 核心上完成,这样桥接处理也能并行执行。
如果物理网卡接收路径优化完成后,仍发现某个 CPU 核心负载偏高,需要检查 RPS(Receive Packet Steering)和 RFS(Receive Flow Steering)是否与 RSS 冲突。RPS 是软件层面的包分发机制,可以在没有硬件 RSS 或队列数较少时模拟多队列效果。物理网卡已经启用 RSS 的情况下,内核默认不会再对同一设备启用 RPS,但如果手工设置了 rps_cpus,可能会与硬件队列的 CPU 绑定产生重叠,造成不必要的处理延迟。通常物理网卡调优时,不需要额外配置 RPS。
另外,Kubernetes 节点如果运行了 NodePort 服务,外部流量需要经过 kube-proxy 的 NAT 转发。多队列网卡能让这些入站连接在多个核心上完成协议栈处理,减少 conntrack 锁竞争。实际收益取决于网卡驱动、CPU 拓扑和流量模型。建议在调整前先用 pprof 或 perf 观察软中断热点,调整后对比吞吐和 P99 延迟。只有数据包接收路径成为瓶颈时,多队列与 RSS 的调优才会带来明显提升,否则应优先排查 CNI 转发规则、conntrack 表项和应用本身。

