在 K8S 容器网络中,高并发(PPS > 30万)场景下 veth pair 极易因单队列架构触发宿主机单核 SoftIRQ (NET_RX) 100% 饱和,导致严重丢包与网络抖动。临时止血方案需在宿主机端开启 RPS(Receive Packet Steering)将软中断打散;而彻底解决该类 I/O 密集型业务瓶颈,应引入 macvlan 或 SR-IOV 进行网络栈卸载,直接旁路宿主机内核的复杂转发路径。
故障现场:Redis 容器的神秘丢包与 99 线飙升
近期排查了一起 K8S 集群内 Redis 响应毛刺问题。环境基础信息如下:
-
OS: Ubuntu 22.04 (Kernel 5.15.0-76-generic)
-
K8S 版本: v1.25.9
-
CNI: Calico v3.25.0 (BGP 路由模式)
-
业务表现: 压测期间 Redis 实例的 QPS 达到 8 万时,p99 延迟从 2ms 突变至 150ms 以上,客户端频繁报
Read timed out。
首先登入 Redis 所在宿主机,直接通过 mpstat 查看中断分布:
# 每秒输出所有 CPU 核状态
mpstat -P ALL 1
09:41:01 AM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
09:41:02 AM all 8.23 0.00 6.11 0.00 0.00 12.15 0.00 0.00 0.00 73.51
09:41:02 AM 0 4.00 0.00 3.00 0.00 0.00 1.00 0.00 0.00 0.00 92.00
...
09:41:02 AM 12 2.00 0.00 10.00 0.00 0.00 100.00 0.00 0.00 0.00 0.00
如上所示,CPU 12 的 %soft 已经被彻底打满(100%)。进一步通过 /proc/softirqs 定位具体的中断类型:
watch -d -n 1 "cat /proc/softirqs | grep NET_RX"
确认是 NET_RX 软中断风暴。接着查看容器对应的宿主机端 veth 网卡(假设为 cali9a3b2c1)的丢包统计:
# 确认网卡 rx_dropped 指标疯狂上涨
ip -s link show cali9a3b2c1
现象明确:宿主机单核处理软中断能力达到极限,导致网卡接收队列(Backlog)溢出,底层协议栈开始大面积丢弃数据包。
为什么 veth pair 会成为高吞吐场景的性能毒药?
要搞清楚这个问题,必须深入 veth pair 在内核中的数据流转机制。
veth pair 是一对虚拟以太网设备。在 Calico 网络下,数据包从物理网卡(如 eth0)进入宿主机,经过内核路由判决后,发往对应的宿主机端 veth 设备(caliXXX),然后再进入容器的网络命名空间。
对于物理网卡,现代网卡均支持多队列(RSS, Receive Side Scaling),可以通过 Hash 算法将不同数据流的硬件中断(HardIRQ)分发到多个 CPU 核上,进而触发多核并发处理 NET_RX 软中断。
但 veth pair 是纯软件模拟的虚拟网卡,默认只有单队列(rx-0/tx-0)。
当数据包从物理网卡路由到 caliXXX 时,内核调用 dev_forward_skb,最终触发 netif_rx 将 skb(套接字缓冲区)压入特定 CPU 的 softnet_data->input_pkt_queue 中。
由于 veth 没有硬件多队列支撑,所有发往该容器的数据包,其软中断处理逻辑通常只能由单核(通常是触发调用的源 CPU,或者被网卡中断绑定的固定 CPU)串行执行。当流量达到几十万 PPS 时,这个单 CPU 很快就会触及 100% 的瓶颈,导致后续包因为 Backlog 队满而被丢弃。
实战破局:从软件调优到硬件卸载
针对上述瓶颈,我们在实战中通常采用两个阶段的方案:快速止血与架构重构。
第一阶段:软件层面开启 RPS 打散软中断
RPS(Receive Packet Steering)是 RSS 的软件实现。它能在 netif_rx 接收到包后,利用四元组 Hash 软计算,将包投递到其他 CPU 的积压队列中,强制触发跨核的软中断处理。
找到 Redis 对应的宿主机网卡 cali9a3b2c1,为其配置 RPS(假设宿主机为 16 核,我们将掩码设为 ffff,允许打散到所有核):
# 将 16 进制掩码写入对应接收队列的 rps_cpus 中
echo ffff > /sys/class/net/cali9a3b2c1/queues/rx-0/rps_cpus
# 同步调大内核层面的 backlog 队列深度,防止缓冲击穿
sysctl -w net.core.netdev_max_backlog=10000
开启后,再次观察 mpstat,CPU 12 的 %soft 迅速下降至 30% 左右,其他 CPU 的 %soft 开始均衡上升,Redis 响应延迟立刻恢复到 2ms 的水平。
注意: 这种方案有代价。RPS 带来了额外的 CPU 周期消耗(计算 Hash、跨核 Cache Miss),整体 CPU 负载(Load Average)会显著升高。这是典型的“空间换时间”策略。
第二阶段:引入 macvlan / SR-IOV 卸载网络栈
对于此类极致 I/O 的业务,经过多次踩坑,最终的防线必须是绕过复杂的宿主机网络栈。通过 Multus CNI 引入 macvlan 或 SR-IOV,是当前主流的解法。
以 macvlan 桥接模式为例,它的底层原理是直接在宿主机物理网卡(eth0)上虚拟出一个具有独立 MAC 地址的子接口。数据包到达物理网卡后,底层驱动通过匹配 MAC 地址,直接将包送入容器的 Network Namespace,彻底跳过了宿主机内核的路由查找、Netfilter (iptables/IPVS) 过滤以及 veth pair 的设备中转。 且 macvlan 继承了物理主网卡的 RSS 特性,天然支持多核并发接收。
在 K8S 中配置 Multus 与 Macvlan 混合网络示例 (NetworkAttachmentDefinition):
apiVersion: "k8s.cni.cncf.io/v1"
kind: NetworkAttachmentDefinition
metadata:
name: macvlan-conf
namespace: default
spec:
config: '{
"cniVersion": "0.3.1",
"type": "macvlan",
"master": "eth0",
"mode": "bridge",
"ipam": {
"type": "host-local",
"subnet": "192.168.100.0/24",
"rangeStart": "192.168.100.100",
"rangeEnd": "192.168.100.200",
"routes": [
{ "dst": "0.0.0.0/0" }
],
"gateway": "192.168.100.1"
}
}'
随后在 Redis Pod 中声明注解:
metadata:
annotations:
k8s.v1.cni.cncf.io/networks: macvlan-conf
改造后,Redis Pod 获得了直通物理网络的 eth1 网卡,单机压测极限 PPS 提升了近 3 倍,且宿主机的 CPU sys/soft 占用极低。
常见问题 (FAQ)
Q1:为什么使用 macvlan (bridge 模式) 后,宿主机反而 ping 不通该容器了?
这是 macvlan 驱动设计的经典防线。macvlan 拦截了进出物理网卡的流量,但根据 802.1q 规范,从物理网卡发出的包默认不会回流到自己。宿主机发送的报文直接从底层网卡出去了,无法通过 MAC 匹配路由回该网卡上的 macvlan 子接口。
解法: 在宿主机上再创建一个同网段的 macvlan 接口(例如叫 macvlan-host),将宿主机对该网段的路由指向 macvlan-host,利用 bridge 模式下的内部交换机制实现通信。
Q2:SR-IOV 与 macvlan 相比优势在哪里,什么时候必须上 SR-IOV? macvlan 仍经过宿主机的物理网卡驱动和内核协议栈底层;而 SR-IOV(Single Root I/O Virtualization)是 PCIe 硬件级别的虚拟化。它通过 PF(Physical Function)虚拟出多个 VF(Virtual Function),VF 直接映射给容器。 如果是搞 DPDK 等用户态网络协议栈,或者极低延迟(微秒级)的 HFT (高频交易) 场景,必须用 SR-IOV 彻底 Bypass 内核。普通的高性能 Redis/MySQL,macvlan 已经足够。
Q3:开启了 RPS,但有些网卡的 rps_cpus 修改后提示 “Permission denied” 或无效?
如果是针对容器内的 veth 设备修改,受限于 NetNS 权限,需在宿主机端的对端网卡(如 Calico 的 calixxx、Flannel 的 vethxxx)操作。另外,务必确保宿主机系统服务(如 irqbalance)不要与你手动的 RPS 掩码逻辑发生冲突,排查过程中发现两者打架是常态,针对极端优化的节点,通常建议关闭 irqbalance 并手动绑核。