跨越 Veth Pair 的性能鸿沟:高并发场景下 IPVLAN 与 SR-IOV 的底层抉择

上午的流量早高峰刚过,监控大屏上的多条告警逐渐恢复平静。趁着喝口水的功夫,我把刚结束的复盘会内容整理一下。

事情起因是业务线新上线了一个高频交易网关,部署在 K8S 集群中。QPS 刚切过来 30%,节点上几个 CPU 核心的 si(软中断)使用率就直接飙到了 100%,随之而来的是 P99 延迟剧烈抖动,部分请求出现几十毫秒的网络排队延迟。业务研发跑过来问是不是宿主机网卡跑满了,我瞥了一眼监控面板,千兆网卡的带宽连一半都没用到,但这台机器的网络 PPS(每秒包数)已经突破了 40 万。

很明显,这不是带宽瓶颈,而是经典的 Linux 网络协议栈软中断瓶颈。更准确地说,是容器网络默认的 veth pair 在高并发下的底层机制拖垮了 CPU。

Veth Pair 的隐性代价:上下文切换与软中断风暴

目前绝大多数 K8S 的 CNI 插件(如 Flannel、Calico)默认都采用 veth pair + Bridge/路由 的模式。veth pair 本质上是一对虚拟网卡,连接着容器的 Network Namespace 和宿主机的 Root Namespace。

为了定位当时的 CPU 开销,我在物理机上抓了一把 perf top -C <对应核>

  12.45%  [kernel]       [k] veth_xmit
  10.21%  [kernel]       [k] __netif_receive_skb_core
   8.32%  [kernel]       [k] net_rx_action
   7.14%  [kernel]       [k] br_handle_frame
   6.55%  [kernel]       [k] ipt_do_table
   4.10%  [kernel]       [k] ip_forward

注意看 veth_xmit 这个函数。当网关 Pod 处理完请求向外发包时,数据包从容器内的 eth0(veth一端)发出,经过内核协议栈,最终调用 dev_queue_xmit() 发送到虚拟网卡,触发 veth_xmit

veth pair 的底层逻辑是:在发包端调用 veth_xmit 时,实际上是在向对端(宿主机上的 veth 接口)发包。它会调用 netif_rx()(或者更高版本内核中的 netif_rx_ni / netif_rx_internal),把 sk_buff 挂到目标 CPU 的 softnet_data 队列上,然后触发一个 NET_RX_SOFTIRQ 软中断。

这意味着什么?一个数据包从容器到物理网卡,要在内核态经历至少两次完整的网络协议栈处理(容器内一次,宿主机一次),并触发额外的软中断上下文切换。 还要经过宿主机上的 Bridge (br_handle_frame) 和 Netfilter/iptables (ipt_do_table)。在 40万 PPS 的冲击下,这种“纯软件模拟”的转发路径不仅带来了巨大的 CPU 消耗,更是延迟抖动的罪魁祸首。

绕过宿主机协议栈:Macvlan 的局限与 IPVLAN 的突围

既然宿主机协议栈太重,那能不能让容器直接和物理网卡对话?

方案无非是 Macvlan 和 IPVLAN。

Macvlan 的原理是基于物理网卡虚拟出多个带有独立 MAC 地址的子网卡。容器直接使用这些子网卡,数据包在物理网卡的 rx_handler 阶段就被直接截获并分发到对应的容器 Namespace,完全绕过宿主机的 Bridge 和 iptables。

但在企业级网络架构中,Macvlan 有一个致命缺陷:交换机 MAC 表爆炸。 如果一个集群有 100 台宿主机,每台跑 50 个 Pod,物理交换机上就需要学习 5000 个 MAC 地址。多数接入层交换机的 CAM 表容量是有限的(通常 4K-8K),一旦溢出,交换机会降级为广播行为,引发未知的单播泛洪(Unknown Unicast Flooding),这在生产环境是不可接受的灾难。另外,部分公有云环境出于安全考虑,甚至会在底层 vSwitch 直接丢弃非宿主机 MAC 的包。

因此,我们当时毫不犹豫地将网关节点的网络模型切换到了 IPVLAN (L2 Mode)

IPVLAN 最大的特点是:所有虚拟接口共享物理网卡的 MAC 地址,但在 IP 层(L3)进行流量多路复用。

下面是当时我们在测试环境用原生命令验证 IPVLAN 拓扑的配置片段:

# 1. 在物理网卡 eth0 上创建 ipvlan 子接口,模式为 L2
ip link add link eth0 name ipv1 type ipvlan mode l2
ip link add link eth0 name ipv2 type ipvlan mode l2

# 2. 将子接口移入独立的 Network Namespace 模拟容器
ip netns add ns1
ip link set dev ipv1 netns ns1
ip -n ns1 link set ipv1 up
ip -n ns1 addr add 192.168.1.100/24 dev ipv1
ip -n ns1 route add default dev ipv1

# 宿主机上无需配置任何桥接或路由策略即可完成容器对外通信

在内核源码中,当使用 IPVLAN 时,物理网卡 eth0 注册了 ipvlan_handle_frame 作为接收钩子(rx_handler)。当带有宿主机 MAC 的数据包到达时:

  1. 网卡驱动收包。

  2. 触发 ipvlan_handle_frame

  3. IPVLAN 模块解析以太网帧头后面的 IP 头。

  4. 基于目标 IP 地址,通过内部的 Hash 表直接查找到对应的子接口(即某个容器的网卡),并将 sk_buff 直接交过去。

效果立竿见影: 网关集群切换到支持 IPVLAN 的 CNI(配合 multus-cni 使用)后,PPS 承载能力提升了接近一倍,宿主机 CPU 的软中断开销暴降了 60% 以上,网络延迟彻底压平。

极致性能的终局:SR-IOV 硬件直通

如果仅仅是网关,IPVLAN 已经能应付绝大多数场景。但早上的复盘会还讨论了另一个极端的场景:如果未来把核心数据库(比如高频写入的 Redis 集群或者 TiKV)做容器化,百万级 PPS 下,IPVLAN 还能扛住吗?

答案是:勉强,但不够优雅。因为 IPVLAN 依然依赖宿主机 CPU 来处理中断和执行拆包/分发逻辑。要想彻底解放 CPU,只能依靠硬件,这就必须引入 SR-IOV (Single Root I/O Virtualization)

SR-IOV 的本质是让支持该特性的物理网卡(如 Mellanox ConnectX 系列或 Intel X710)在 PCIe 硬件层面“裂变”出多个虚拟功能(VF, Virtual Function)。

相比于 IPVLAN 的纯软件多路复用,SR-IOV 的架构是降维打击:

  1. 每个 VF 拥有独立的 PCI 寄存器、独立的硬件收发队列(TX/RX Queues)、独立的 MAC 和 VLAN 过滤表。

  2. 将 VF 直接通过 VFIO/IOMMU 映射给容器使用。

  3. 数据包到达网卡后,物理网卡的内置交换芯片(eSwitch)直接基于硬件规则将包放入对应 VF 的队列,并通过 DMA 机制直接拷贝到容器分配的内存页中。

我们在验证 SR-IOV 时,常用的排查和分配手段如下:

# 查看物理网卡是否支持及当前配置的 VF 数量
cat /sys/class/net/eth0/device/sriov_numvfs

# 开启 4 个 VF
echo 4 > /sys/class/net/eth0/device/sriov_numvfs

# 使用 lspci 可以看到新生成的 Virtual Function 硬件设备
lspci | grep Ethernet
# 04:00.0 Ethernet controller: Intel Corporation Ethernet Controller X710 for 10GbE SFP+ (PF)
# 04:02.0 Ethernet controller: Intel Corporation Ethernet Virtual Function 700 Series (VF 0)
# 04:02.1 Ethernet controller: Intel Corporation Ethernet Virtual Function 700 Series (VF 1)

当容器配置了 SR-IOV CNI 后,在容器内部看到的就是一块真真切切的硬件网卡。整个发包路径直接从容器内的 Socket 通过驱动写到 PCIe 设备的硬件队列,宿主机的内核网络栈在这个过程中完全是被架空的,没有任何 CPU 会因为这个容器的网络 I/O 被软中断打断

结语

技术架构里从来没有银弹。 对于 90% 的普通微服务,veth pair 配合 eBPF(比如 Cilium 的 sockops 绕过)或者单纯的 iptables/IPVS 已经足够;对于高吞吐的 API 网关或者视频流媒体,切换到 IPVLAN 可以用极小的架构变动换取巨大的性能红利,并且避开交换机 MAC 限制;而对于追求极致微秒级延迟和极高 PPS 的核心存储与交易撮合引擎,SR-IOV 才是最终归宿。

认清瓶颈在内核还是在硬件,在上下文切换还是在队列排队,远比盲目修改内核参数要有效得多。剩下的时间,我得去查查为什么今天那台边缘节点的 kubelet PLEG 会出现轻微卡顿了。