标签: SoftIRQ

  • 深入 veth pair 陷阱排查:跨 Namespace 协议栈穿透引发的 SoftIRQ 暴涨与 IPVLAN L2 提效实战

    高并发容器场景下,传统 veth pair 配合网桥的架构会因双向内核协议栈穿透产生海量 SoftIRQ(NET_RX_SOFTIRQ),导致网络 p99 延迟剧烈抖动。本文通过拆解 veth 性能瓶颈,给出向 IPVLAN L2 模式迁移的底层逻辑与实践,彻底规避宿主机 Netfilter 开销,将容器网络吞吐提升近 40%,延迟方差收敛至毫秒级。

    故障现场:消失的 CPU 算力与抖动的 p99

    某次核心网关服务从虚机整体迁移至 Kubernetes(v1.28.2,内核 5.15.x)后,业务侧频繁反馈高并发请求期间(单 Pod QPS 突破 2w 时)出现间歇性超时。监控面板显示 Pod 所在宿主机的 Load Average 飙升,但用户态 CPU(us)消耗并不高,大部分算力被系统态(sy)和软中断(si)吃满。

    切入宿主机直接抓取 CPU 软中断分布,问题浮出水面:

    # 每秒打印一次软中断聚合数据
    mpstat -I SUM -P ALL 1
    
    # 输出片段:
    14:20:01 CPU    intr/s   %usr   %sys   %iowait  %irq   %soft  %steal  %idle
    14:20:02 all   142500.00 25.10  32.40   0.00    0.00   38.20   0.00    4.30
    14:20:02   0    25100.00 10.00  40.00   0.00    0.00   48.00   0.00    2.00
    

    %soft 占用高达 38% 以上,且集中在个别 CPU 核心上。使用 perf top -C 0 查看内核热点函数,排在前列的赫然是 _raw_spin_lock、veth_xmit、netif_rx_internal 和 br_handle_frame。这套基于 Flannel/Calico 默认模式配置的 veth pair + bridge 容器网络架构,在高 PPS(每秒包数)冲击下已被彻底击穿。

    为什么 veth pair 会成为高并发容器的性能毒药?

    要回答这个问题,必须剖析一个网络包在 veth 架构下的内核穿透路径。在常规容器网络中,当外部报文到达宿主机网卡,最终进入 Pod,需要经历以下漫长的旅程:

    1. 宿主机物理网卡 (eth0) 接收报文,触发硬中断,随后在 NET_RX_SOFTIRQ 中由 NAPI 机制拉取。

    2. 报文进入宿主机 TCP/IP 协议栈。

    3. 经由宿主机的路由决策,报文被投递给宿主机网桥 (cni0 / docker0)。

    4. 网桥通过 FDB 表转发至对应的 veth 宿主机端。这里会强制触发宿主机侧的 Netfilter Hook(如 iptables/conntrack),这是性能损耗的重灾区。

    5. 报文调用 veth_xmit 发送,通过内核的 __dev_queue_xmit 放入发送队列。

    6. veth 的对端(Pod 网络命名空间)触发接收,调用 netif_rx,再次引发软中断并进行 Context Switch 切换网络命名空间。

    7. 报文进入 Pod 的 TCP/IP 协议栈,最终被 Socket 接收。

    一条数据流,两次协议栈穿透,两次软中断处理,外加全套 Netfilter 规则匹配。更致命的是,veth 设备在早期内核或默认配置下不支持多队列(Multi-Queue)和 RSS(Receive Side Scaling),导致同一条流的软中断被死死绑定在一个 CPU 核心上,直接引发单核瓶颈。

    架构演进:IPVLAN L2 的零拷贝绕过

    面对海量 PPS,在传统协议栈里做微调(如调大 netdev_max_backlog、优化 iptables 规则)只是杯水车薪。必须从架构上进行降维打击:Bypass 宿主机网络栈。

    IPVLAN 就是为此而生。它直接挂载在宿主机的物理网卡之上,利用 rx_handler 机制在 L2 数据链路层进行报文分发。

    当配置为 IPVLAN L2 模式时,所有容器共用物理网卡(如 eth0)的 MAC 地址,但在宿主机网卡驱动层根据目的 IP 地址进行 Demux(解复用)。报文到达物理网卡后,直接投递到对应容器的 Network Namespace 中,完全绕过宿主机的 iptables/conntrack 和 bridge 处理逻辑。

    我们可以通过原生 iproute2 命令直观感受其极简的内核操作逻辑:

    # 1. 创建名为 ns1 的容器网络命名空间
    ip netns add ns1
    
    # 2. 在宿主机物理网卡 eth0 上挂载一个 ipvlan 子接口,模式为 L2
    ip link add link eth0 name ipvlan0 type ipvlan mode l2
    
    # 3. 将该子接口直接塞进容器命名空间,没有 bridge,没有 veth pair
    ip link set dev ipvlan0 netns ns1
    
    # 4. 在容器命名空间内配置 IP 并启动
    ip netns exec ns1 ip addr add 192.168.1.100/24 dev ipvlan0
    ip netns exec ns1 ip link set ipvlan0 up
    ip netns exec ns1 ip route add default dev ipvlan0
    

    生产环境落地与收益验证

    在 Kubernetes 中落地,我们通常通过 CNI 插件(如 Multus CNI 配合官方的 ipvlan plugin,或 Cilium 的 IPVLAN 模式)来实现。以下是一个典型的基于 IPVLAN 的 Multus NetworkAttachmentDefinition 配置片段:

    {
      "cniVersion": "0.3.1",
      "name": "high-perf-ipvlan",
      "type": "ipvlan",
      "master": "eth0",
      "mode": "l2",
      "ipam": {
        "type": "host-local",
        "subnet": "10.244.0.0/16",
        "rangeStart": "10.244.1.10",
        "rangeEnd": "10.244.1.250",
        "routes": [
          { "dst": "0.0.0.0/0" }
        ],
        "gateway": "10.244.1.1"
      }
    }
    

    验证结果: 完成网关集群的 IPVLAN L2 模式改造后,在同等流量压力(单 Pod 2w QPS)下,复测宿主机指标:

    1. 单核 %soft 消耗从 40%+ 断崖式下降至 8% 左右。

    2. perf top 中 veth_xmit 和 br_handle_frame 的开销彻底消失,取而代之的是物理网卡驱动和 ipvlan_xmit_mode_l2 的极低开销。

    3. 业务端 99 线延迟从 50ms 抖动收敛为稳定的 3-5ms。

    常见问题 (FAQ)

    Q1:同样是绕过网桥,为什么选用 IPVLAN 而不是 Macvlan? Macvlan 会为每个容器生成一个独立的虚拟 MAC 地址。在海量容器规模下,会导致物理交换机(ToR)的 CAM(MAC地址表)迅速耗尽;同时很多公有云环境的底层 VPC 网络默认开启端口安全策略(Port Security),会直接丢弃非宿主机真实 MAC 的报文。IPVLAN 所有子接口共享宿主机的同一个 MAC 地址,对底层物理网络完全透明。

    Q2:IPVLAN L2 和 L3 模式应该怎么选? L2 模式类似于二层交换,容器与宿主机处于同一个广播域,ARP 解析由外部物理网络处理,适合大部分同网段平铺的网络架构。 L3 模式则在宿主机内部进行三层路由,所有容器的外部通信不仅共享 MAC,在外部看来甚至没有 MAC 层特征,纯靠 IP 路由。如果物理网络对广播包有限制或需要跨网段精细化路由控制,选 L3,但常规高性能场景 L2 更通用。

    Q3:排查过程中发现,切换到 IPVLAN 后,容器内部 ping 不通自己所在的宿主机 IP,这是为什么? 这是 IPVLAN 的经典机制。出于内核防止路由环路和防欺骗(Anti-Spoofing)的考虑,宿主机主接口(eth0)和挂载其上的 ipvlan 子接口之间的流量默认是隔离的(Hairpin 限制)。 解决思路:不要让容器直接访问宿主机的 eth0 IP。常规做法是为宿主机自身也创建一个 ipvlan 子接口,将宿主机的管理 IP 配置在该子接口上,而不是直接配在 eth0 上;或者利用多网卡/额外构建专用于 Host-to-Pod 通信的 veth 通道。

  • 深入 Netfilter 排查:海量 iptables 规则引发的 CPU SoftIRQ 暴涨与 nftables 匿名集合 O(1) 重构实战

    结论先行:将 5 万条 iptables 顺序拦截规则重构为原生 nftables 的匿名集合(Set/Map)后,宿主机网卡软中断(SoftIRQ)CPU 开销从 100% 暴降至 2%,网络 P99 延迟从 200ms 恢复至 2ms。在海量 IP 拦截与路由过滤场景下,彻底抛弃 iptables 的 O(N) 线性遍历,改用 nftables 基于红黑树与哈希表的 O(1) 匹配,才是现代网关防御的唯一出路。

    近期在排查某边缘网关集群性能问题时,发现部分节点在 PPS(每秒包数)仅 10 万左右时,Load Average 飙升至 40+,网络出现严重丢包。该节点主要承担流量清洗清洗和恶意 IP 封禁任务,安全组件会通过脚本动态下发拦截规则。

    案发现场:ksoftirqd 满载与 perf 剖析

    登录问题机器(Ubuntu 22.04, Kernel 5.15.0, iptables v1.8.7),通过 mpstat -P ALL 1 观察,发现多个 CPU 核心的 %soft 指标长期顶在 100%,而 %usr 和 %sys 极低。这说明 CPU 周期全部消耗在了内核态网络软中断处理上。

    直接挂载 perf 抓取热点函数:

    perf record -a -g -F 99 -- sleep 10
    perf report
    

    调用栈结果异常刺眼:

    -   78.42%     ksoftirqd/2  [kernel.kallsyms]   [k] nft_do_chain
       - 77.10% nft_do_chain
          - 68.32% nft_cmp_eval
          - 4.21% nft_counter_eval
    

    排查防火墙规则,发现安全组件通过 iptables -A INPUT -s -j DROP 累积下发了高达 5.2 万条黑名单规则。

    为什么 iptables-nft 包装层依然无法拯救 SoftIRQ 雪崩?

    很多人有个误区,认为在较新的 Linux 发行版中,iptables 已经默认切换到了 iptables-nft(即底层调用 nftables API),性能应该自动得到了优化。这是极其致命的错觉。

    iptables-nft 仅仅是一个语法翻译层。当你执行 5 万次 iptables -A INPUT -s IP -j DROP 时,翻译层会在 nftables 中原样生成 5 万条独立的规则。

    在内核 net/netfilter/nf_tables_core.c 的 nft_do_chain() 函数执行时,Netfilter 依然会像老版本一样,对数据包进行 O(N) 的线性遍历匹配。这意味着:

    1. 合法流量的深渊:正常用户的包如果不匹配任何黑名单,必须遍历完这 5.2 万条规则,直到命中链底部的默认 ACCEPT。

    2. SYN 洪水的放大器:虽然通常会在链表顶部配置 iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT,但对于新建连接(SYN)或恶意扫描包,Conntrack 无法命中,依然会陷入 5.2 万次 nft_cmp_eval 的循环对比中。

    单包处理时间的微秒级膨胀,在 10 万 PPS 的乘数效应下,瞬间抽干了 ksoftirqd 的处理能力,导致网卡 Ring Buffer 溢出,触发连环丢包。

    nftables 重构实战:利用 Set/Map 实现 O(1) 降维打击

    要解决这个问题,必须抛弃 iptables 的扁平化规则思维,改用 nftables(推荐版本 >= 1.0.2)原生的集合(Set)数据结构。内核会自动为集合选择合适的数据结构(针对离散 IP 默认使用 rhashtable,针对网段使用 pipapo)。

    1. 编写原生 nftables 配置

    清空旧的 iptables 规则,并加载优化的 nftables 配置:

    # file: /etc/nftables.conf
    flush ruleset
    
    table inet filter {
        # 预先定义一个巨大的哈希集合用于存放黑名单 IP
        set blacklist_ipv4 {
            type ipv4_addr
            flags interval
            auto-merge
            size 100000 # 预分配空间,避免频繁扩容引起的自旋锁竞争
        }
    
        chain input {
            type filter hook input priority filter; policy accept;
    
            # 1. 优先放行已建立的连接,跳过后续检查 (Conntrack 加速)
            ct state established,related accept
    
            # 2. 无效包直接丢弃
            ct state invalid drop
    
            # 3. O(1) 极速匹配黑名单,命中即丢弃
            ip saddr @blacklist_ipv4 drop
    
            # 4. 本地回环与 SSH 放行等常规规则...
            iifname "lo" accept
            tcp dport 22 accept
        }
    }
    

    2. 动态管理海量 IP 且不重载规则

    传统 iptables 增删规则时会对整个链表加锁(xtables_lock),并发极差。而在 nftables 中,向 set 添加元素是轻量级的原子操作,完全不会影响其他规则的执行:

    # 批量导入 5 万个黑名单 IP(执行耗时仅需几十毫秒)
    nft add element inet filter blacklist_ipv4 { 1.1.1.1, 2.2.2.0/24, 3.3.3.3 }
    
    # 删除特定 IP
    nft delete element inet filter blacklist_ipv4 { 1.1.1.1 }
    

    3. 性能验证

    重构上线后,再次压测 30 万 PPS 的新建连接风暴:

    • CPU %soft 消耗直接降至 2% ~ 3%。

    • perf top 中 nft_do_chain 的开销跌出前十。

    • 通过 nft list set inet filter blacklist_ipv4 查看,五万个 IP 被压缩在一次底层的哈希查找中,耗时几近于零。

    底层原理:Pipapo 与 rhashtable 的威力

    在 nftables 的源码实现中(net/netfilter/nft_set_hash.c),当你定义了一个不带 interval(网段)标志的独立 IP set 时,内核默认初始化 rhashtable(可动态扩容的哈希表)。查找复杂度为绝对的 O(1)。

    如果带有 interval 标志(例如混合了 CIDR 网段拦截),内核会使用 pipapo (PIle PAcket POlicies) 算法(net/netfilter/nft_set_pipapo.c)。这是一种针对多维范围匹配高度优化的算法,通过预构建位图(Bitmaps),将多级网段的匹配转换为内存连续的位运算操作,且对 CPU Cache 极其友好。这解释了为何无论黑名单是 5 千还是 5 万,单包处理时延的抖动曲线始终保持平滑。

    常见问题 (FAQ)

    Q1: 在向 nftables 的 Set 中大批量添加 IP 时,会阻塞现有的网络连接吗? 不会。nftables 底层通过 RCU (Read-Copy Update) 机制保障数据的并发读写。对于数据包匹配链路(读侧),RCU 提供了无锁访问能力;而在控制平面批量更新 Set 元素时(写侧),只会发生极短的锁竞争。这与传统 iptables 重载整个规则链引发的剧烈抖动有本质区别。

    Q2: 既然设置了 ct state established accept,为什么之前用 iptables 时,正常流量还是会感觉到高延迟? 因为网络环境并非绝对纯净。即使正常长连接被 Conntrack 放行,但在公网环境中,机器每秒都会遭受大量的垃圾扫描包、伪造包或恶意 SYN 包。这些无状态包无法命中 established,必定会落入后续的 5 万条 iptables 规则中进行线性遍历,进而抢占大量 CPU 资源,导致正常连接的软中断处理被系统调度延后,最终体现为 P99 延迟飙升。

    Q3: 如何将现有的业务脚本从 iptables 平滑迁移到 nftables? 千万不要依赖 iptables-nft 来容忍旧脚本。正确的做法是:

    1. 重写网关的基线规则,用 nft -f 加载带有 Set 的配置结构。

    2. 将现有的封禁/解封脚本,从 iptables -I INPUT -s $IP -j DROP 替换为 nft add element inet filter blacklist_ipv4 { $IP }。

    3. 如果依赖 fail2ban 等第三方工具,需将 action 配置修改为 nftables-multiport 或直接修改其默认的 backend 为 nftables。

  • 深入 K8S veth pair 丢包排查:高 PPS 触发的 SoftIRQ 单核瓶颈与 macvlan 卸载实战

    在 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 并手动绑核。