标签: 软中断

  • 深入 Macvlan 广播风暴排查:CAM 表溢出引发的未知单播泛洪与 ksoftirqd 软中断打满实战

    某次接手一个号称为了“极致网络性能”而将 K8S CNI 从 VXLAN 模式改为纯 Macvlan 的生产集群。业务上线后,节点负载出现间歇性雪崩,核心接口 P99 延迟从正常的 20ms 飙升至 3s 甚至超时,节点 Load Average 狂飙。排查到底层的结论很直接:高密度 Pod 产生的离散 MAC 地址不仅打穿了单机网卡的硬件过滤表,更是直接打爆了 ToR 交换机的 CAM(MAC 地址表)。交换机被迫退化成 Hub,引发全网段“未知单播泛洪”(Unknown Unicast Flooding)。所有节点的 ksoftirqd 进程因处理海量非本机的垃圾报文将 CPU 软中断打满。盲目追求扁平网络而不评估物理网络和硬件容量,纯属给自己挖坑。

    案发现场与指标表现

    报警爆发时,业务端反馈连接池超时,但在容器内 ping 网关却时通时断。登录其中一台高负载节点排查,执行 mpstat -P ALL 1,发现部分 CPU 核心的 %soft(软中断)指标死死顶在 100%:

    10:14:01 AM  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %idle
    10:14:02 AM    2    1.00    0.00    3.00    0.00    0.00  100.00    0.00    0.00   0.00
    10:14:02 AM    5    0.50    0.00    2.50    0.00    0.00  100.00    0.00    0.00   0.00
    

    查看 /proc/softirqsNET_RX(网络接收软中断)的计数值在特定核上正在以每秒数十万的速率疯狂拉升。

    直接抓取网卡吞吐,sar -n DEV 1 显示物理网卡 eth0rxpck/s(每秒接收包数)高达 40w+,但节点实际的业务 QPS 根本没有这么大,且大部分包被内核默默丢弃了(rxdrop/s 同样极高)。

    剥丝抽茧:谁在塞满接收队列?

    既然有海量不明报文涌入,直接祭出 tcpdump 在宿主机物理网卡上抓包分析。为了过滤掉正常的本机流量,明确指定抓取目的 MAC 不是本机的报文:

    tcpdump -i eth0 -n -e -c 1000 not ether dst $(cat /sys/class/net/eth0/address)
    

    抓包结果让人大跌眼镜:屏幕上疯狂滚动着目的 MAC 地址属于其他节点上运行的 Pod 的单播报文。

    正常情况下,交换机通过 CAM 表记录 MAC 地址与物理端口的映射,单播包应该精确转发到对应端口,为什么这些单播包会被广播到所有节点?

    结合 Macvlan 的底层机制,问题的技术逻辑链条浮出水面: Macvlan 的核心原理是为宿主机网卡创建多个子接口,每个子接口(即每个 Pod)分配一个独立的、真实的 MAC 地址。

    # Macvlan 底层创建逻辑示例
    ip link add link eth0 name macvlan0 type macvlan mode bridge
    

    第一层雪崩:物理网卡 UC Filter 被迫降级 普通的物理网卡(如 Intel X710 等)硬件支持的单播 MAC 地址过滤表(Unicast Filter Table)容量极其有限(通常在 128 到 512 个之间)。当单台宿主机上调度的 Pod 数量超过网卡硬件限制时,网卡驱动为了保证网络连通性,会直接放弃硬件过滤,强制将网卡设置为混杂模式(Promiscuous Mode)。 通过 dmesg | grep promiscuous 确认了这一点,系统日志中赫然躺着: eth0: entered promiscuous mode 这意味着网卡会将网络上收到的所有报文全部通过 DMA 拷贝到内存,并触发中断交由内核 ksoftirqd 处理。

    第二层雪崩:ToR 交换机 CAM 表溢出 集群规模约 100 台,每台节点平均 150 个 Pod,总计 15000+ 个 MAC 地址。而机架顶部的 ToR 交换机的 CAM 表容量上限仅为 8192。 当交换机学习到的 MAC 地址超过 8192 时,新来的 MAC 无法被记录,或者旧的活跃 MAC 被挤出。当交换机收到目的地址不在 CAM 表中的单播报文时,它的处理机制是:将该报文向 VLAN 内除源端口外的所有端口泛洪(Flooding)

    两层雪崩叠加,灾难诞生了:交换机将海量的单播报文当成广播往全网段狂塞,而所有宿主机的物理网卡均因超过硬件过滤上限处于混杂模式,全盘接收这些垃圾报文。内核网络栈被迫对这每秒数十万的包进行解析、路由判断并最终丢弃,直接耗尽了 CPU 的软中断处理能力,导致正常的业务报文排队超时,业务被一波带走。

    破局与架构避坑

    不要一听到 VXLAN 的封包解包开销,就急着上 Macvlan。扁平网络带来的不仅是性能,还有对二层物理网络的巨大冲击。

    对于这种问题,除了临时扩容交换机 CAM 表(如果硬件支持的话)或降低 Pod 密度外,根本的技术解法是抛弃 Macvlan,转向 IPVLAN (L2 模式)

    IPVLAN 与 Macvlan 类似,都能提供直接接入 Underlay 网络的低损耗,但 IPVLAN 的核心区别在于:所有 Pod 共享宿主机物理网卡的 MAC 地址

    # IPVLAN 底层创建逻辑示例
    ip link add link eth0 name ipvlan0 type ipvlan mode l2
    

    使用 IPVLAN 后:

    1. ToR 交换机解脱:无论节点上跑 10 个还是 1000 个 Pod,交换机在对应端口上只看到 1 个宿主机的 MAC,彻底根绝 CAM 表溢出风险。

    2. 物理网卡解脱:无需占用网卡硬件 MAC 过滤表,网卡无需开启混杂模式,异常泛洪报文在网卡硬件层即被丢弃。

    3. 内核分发:报文到达内核后,IPVLAN 驱动根据网络层的 IP 地址(而不是 MAC)将流量精准分发到对应的 Pod 网络命名空间。

    同类问题速查排查清单

    1. NET_RX 软中断飙升定性:遇到网络高延迟,第一时间 mpstat -P ALL 1 查看 %soft,并用 cat /proc/softirqs 确认是否为 NET_RX 引起。若单核被打满,往往伴随网卡多队列未开启或哈希不均(RSS 配置问题)。

    2. 未知单播泛洪检测:使用 tcpdump -i eth0 -n -e not ether dst <本机MAC> 抓包。如果抓到大量不属于本机且非广播/多播的报文,立即检查交换机 MAC 学习表是否溢出或未学习到对应路由。

    3. 网卡混杂模式暗坑排查:高密度容器场景下,通过 dmesg | grep promiscuousip link show 检查物理网卡是否处于 PROMISC 状态。如果是且非主动开启(如抓包),需警惕硬件 MAC 过滤表已满。

    4. Macvlan 宿主机互通死角:如果开发反馈 Macvlan 模式下 Pod 无法 ping 通所在的宿主机(常导致 Kubelet 健康检查失败),这是 Macvlan 规范限制。必须在宿主机额外创建一个 Macvlan 虚接口,并将宿主机 IP 移至该虚接口并配置对应路由才能绕过此隔离限制。

  • 深入 NVMe IO 延迟雪崩排查:默认调度器误用引发的 blk-mq 锁争用与软中断打满实战

    某次核心分布式 KV 存储集群进行底层硬件换代,全面升级至 Gen4 NVMe SSD。本以为硬件红利能让 IO 性能起飞,结果压测一上,QPS 刚到 8 万,99线(p99 latency)就开始周期性从 2ms 剧烈抖动到 300ms 以上。Load Average 狂飙至机器核数的两倍,大量写入线程陷入 D 状态(Uninterruptible Sleep)。 一句话交代结论:这是一起典型的“旧时代运维习惯毒害新硬件”的事故。系统镜像中遗留的 udev 规则将 NVMe 设备的 IO 调度器默认设置成了 mq-deadline。在极高并发下,这个多余的软件调度层在内核 blk-mq(块设备多队列)中引发了极其严重的自旋锁争用(Spinlock Contention),直接打满 ksoftirqd 软中断,导致 IO 请求在 OS 提交队列里排队,根本没送进物理磁盘。 解决方式极其简单:echo none > /sys/block/nvme0n1/queue/scheduler

    案发现场与指标拆解

    排查过程中,第一直觉是新批次的 NVMe 盘存在固件缺陷或发生了 GC(垃圾回收)拥塞。习惯性敲下 iostat -xz 1,看到的数据却极为诡异:

    Device:         rrqm/s   wrqm/s     r/s     w/s    rkB/s    wkB/s avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
    nvme0n1           0.00    12.00  340.00 45000.00  5440.00 720000.00    32.00   145.50  180.20    1.50  182.10   0.01  15.20%
    

    注意看这几个核心指标的矛盾点:

    1. %util 只有 15.20%:说明磁盘底层的物理带宽和 IOPS 根本没跑满,处于极度饥饿状态。

    2. svctm(服务时间)仅为 0.01ms:说明盘的物理响应极快,一旦请求交到硬件手里,瞬间就能处理完。

    3. w_await 高达 182.10ms,avgqu-sz(平均队列长度)达到 145.5:请求虽然处理得快,但排队时间长得离谱。

    这说明一个铁打的事实:IO 根本没有卡在硬件设备上,而是卡在了 Linux 内核的 IO 栈里。

    为了抓现行,直接祭出 perf top 观察内核态的 CPU 热点,结果满屏红彤彤的自旋锁:

      18.45%  [kernel]       [k] native_queued_spin_lock_slowpath
      12.30%  [kernel]       [k] blk_mq_sched_insert_request
       9.15%  [kernel]       [k] sbitmap_get
       7.20%  [kernel]       [k] dd_insert_requests
    

    再看 CPU 状态,top 显示多核的 si(软中断)飙升,对应的进程全是 ksoftirqd/x

    愚蠢的配置与底层原理解析

    走到这一步,根因已经水落石出:dd_insert_requests 这个函数的出现,意味着系统正在使用 mq-deadline IO 调度器。

    查看设备的调度器配置,果不其然:

    $ cat /sys/block/nvme0n1/queue/scheduler
    [mq-deadline] kyber bfq none
    

    为什么在 NVMe 上用 mq-deadline 是不可原谅的低级错误?

    在 SATA/SAS 机械硬盘时代,磁盘只有一个硬件队列(Queue Depth 通常只有 32 或 256)。为了防止磁头剧烈寻道,Linux 内核设计了 IO 调度器(如 Deadline、CFQ)在软件层面对 IO 请求进行合并(Merge)和重排(Sort)。 但在 NVMe 时代,协议原生支持多达 64K 个硬件队列(Submission/Completion Queues),每个队列深度可达 64K。Linux 内核为此重构了 blk-mq(Block Multi-Queue)架构,将 CPU Core 与 NVMe 硬件队列直接建立映射关系(Software Queue -> Hardware Dispatch Queue)。

    如果在 NVMe 上强行开启 mq-deadlinebfq,相当于在原本宽阔的双向 64 车道高速公路上,硬生生设了一个收费站。 所有并发请求走到 blk_mq_sched_insert_request 时,都需要去抢调度器内部的自旋锁,把请求塞进软件队列进行徒劳的合并尝试。在高并发的 KV 存储场景中,小块随机写极多,合并命中率极低,这种操作不仅毫无意义,反而引发了致命的锁争用(native_queued_spin_lock_slowpath)。同时,底层块设备完成 IO 后抛出的中断,在唤醒软中断处理(NET_RX / BLOCK)时又被上层阻塞,最终导致 ksoftirqd 把 CPU 吃干抹净。

    极客实战:彻底打通 IO 栈

    1. 拔掉收费站(关闭调度器)

    直接将调度器设置为 none,让 blk-mq 完全旁路软件调度层,直通硬件队列。

    echo none > /sys/block/nvme0n1/queue/scheduler
    

    修改后,p99 延迟瞬间从 300ms 回落到稳定的 1.5ms,Load Average 下降 80%。

    2. 防御性配置(根治固化)

    永远不要相信手动命令,写进 udev 规则才是 SRE 的基本素养:

    # /etc/udev/rules.d/60-io-scheduler.rules
    ACTION=="add|change", KERNEL=="nvme[0-9]*", ATTR{queue/scheduler}="none"
    ACTION=="add|change", KERNEL=="sd[a-z]*", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="mq-deadline"
    

    (注:对 SATA SSD 依然保留 mq-deadline 是一种保守策略,但对 NVMe 必须一律为 none)。

    3. 压榨最后一滴性能(中断亲和性)

    即使调度器改成了 none,如果你发现某些 CPU 核的 hi/si 依然不均衡,那是因为 NVMe 的 MSI-X 中断没有打散。现代 NVMe 驱动通常会自动分配 IRQ,但如果运行了老旧的 irqbalance 守护进程,可能会发生“劣化漂移”。 建议针对极度依赖 IO 的节点,关掉 irqbalance,并使用内核源码自带的 set_irq_affinity 脚本将 NVMe 的完成队列中断静态绑定到 NUMA 节点的对应 CPU 核心上。

    同类问题速查清单 (Troubleshooting Checklist)

    1. 检查 IO 调度器状态: 快速排查集群中所有块设备的调度器配置: awk -F'[][]' '{print $2}' /sys/block/*/queue/scheduler 如果 NVMe 设备输出非 none,立刻整改。

    2. 区分 OS 队列延迟与硬件延迟: 不要只看 iostatawait。使用 eBPF 工具(如 bcc-toolsbiolatency)深入观测: biolatency -Q 若输出中 OS Queue Time (Q-time) 远大于 Device Time (D-time),说明阻塞点在 Linux Block Layer。

    3. 检查 NVMe 多队列深度配置: 确认内核是否正确识别并启用了 NVMe 硬件队列: ls -d /sys/block/nvme0n1/mq/* | wc -l 该数值应接近或等于机器的 CPU 核心数。

    4. 观测软中断打散情况watch -n 1 'cat /proc/interrupts | grep nvme' 观察各 CPU 列的数值增长率。如果只有极少数 CPU 在狂飙,说明 IRQ 绑定策略失效,正在引发单核软中断瓶颈。

  • 深入 eBPF/XDP 实战:从 Netfilter 软中断打满看 XDP 快速拦截与 kfree_skb 丢包追踪

    传统 iptables/Netfilter 在千万级 PPS 场景下必然成为软中断杀手,协议栈过深的遍历路径是高并发网关的性能毒药。本文直接给出基于 eBPF/XDP 的网络防刷与加速方案,在网卡驱动层(甚至硬件卸载)直接丢弃恶意包,将 CPU si 开销降低 80%,并结合 tracepoint:skb:kfree_skb 彻底终结内核丢包“黑盒”排查。

    案发现场:Netfilter 成为性能瓶颈

    某次生产环境流量突增,某业务 Ingress 网关(Ubuntu 22.04, Kernel 5.15.0-88-generic)QPS 并没有成倍放大,但 P99 延迟直接从 20ms 飙升到了 500ms,部分节点甚至出现 SSH 登录卡顿。

    第一反应看负载,直接上 mpstat -P ALL 1,发现网卡队列绑定的几个 CPU 核心 si(SoftIRQ)直接被打满到了 100%。

    抓取热点函数 perf top -a,霸榜的调用链异常清晰:

      18.52%  [kernel]  [k] nf_hook_slow
      15.21%  [kernel]  [k] ip_rcv
      12.33%  [kernel]  [k] kmem_cache_alloc
      10.14%  [kernel]  [k] __netif_receive_skb_core
    

    典型的 CC 攻击/恶意扫段特征。大量无效的小包涌入,虽然在 iptables/Netfilter 层面配置了 DROP 规则,但由于 iptables 挂载在 PREROUTING 等 Hook 点,数据包走到这里时,内核已经为每一个包分配了 sk_buff 结构体,并走完了复杂的 L2 和 L3 早期协议栈处理

    在动辄几百万 PPS 的冲击下,频繁的 kmem_cache_alloc 和 Netfilter 规则链遍历直接榨干了 CPU。我们需要在更底层“掐断”这些流量。

    为什么 XDP 能在千万级 PPS 下实现防刷降级?

    常规的数据包接收路径是:网卡 -> DMA 拷贝到 Ring Buffer -> 触发硬中断 -> NAPI 轮询拉取 -> 分配 sk_buff -> __netif_receive_skb_core -> 网络协议栈 (Netfilter/IP/TCP 等)。

    XDP(eXpress Data Path)之所以快,根本原因在于它的 Hook 点位于 网络驱动层分配 sk_buff 之前。 当网卡通过 DMA 将数据放入内存后,XDP BPF 程序直接读取这段连续的原始内存(xdp_md),如果是恶意包,直接返回 XDP_DROP,网卡驱动会原地回收页面。没有 skb 内存分配,没有协议栈解析,没有上下文切换。

    XDP 黑名单拦截实战代码

    我们使用 BPF Map 来维护一个高频攻击 IP 黑名单,在 XDP 层直接匹配并丢弃。 以下是精简后的核心 C 代码(xdp_drop.c):

    #include <linux/bpf.h>
    #include <linux/in.h>
    #include <linux/if_ether.h>
    #include <linux/if_packet.h>
    #include <linux/if_vlan.h>
    #include <linux/ip.h>
    #include <bpf/bpf_helpers.h>
    
    // 定义一个 BPF Hash Map 存储黑名单 IP
    struct {
        __uint(type, BPF_MAP_TYPE_HASH);
        __uint(max_entries, 10000);
        __type(key, __u32);   // IPv4 Address
        __type(value, __u32); // Drop counter
    } blacklist SEC(".maps");
    
    SEC("xdp")
    int xdp_drop_prog(struct xdp_md *ctx) {
        void *data_end = (void *)(long)ctx->data_end;
        void *data = (void *)(long)ctx->data;
    
        // 边界检查(必须,否则 eBPF 验证器会拒绝加载)
        struct ethhdr *eth = data;
        if ((void *)(eth + 1) > data_end)
            return XDP_PASS;
    
        if (eth->h_proto != __constant_htons(ETH_P_IP))
            return XDP_PASS;
    
        struct iphdr *iph = data + sizeof(struct ethhdr);
        if ((void *)(iph + 1) > data_end)
            return XDP_PASS;
    
        __u32 src_ip = iph->saddr;
    
        // 查询黑名单 Map
        __u32 *value = bpf_map_lookup_elem(&blacklist, &src_ip);
        if (value) {
            __sync_fetch_and_add(value, 1); // 原子递增拦截计数
            return XDP_DROP; // 核心:在驱动层直接丢弃
        }
    
        return XDP_PASS;
    }
    
    char _license[] SEC("license") = "GPL";
    

    编译与挂载:

    # 使用 clang 编译成 BPF 字节码
    clang -O2 -target bpf -c xdp_drop.c -o xdp_drop.o
    
    # 将 XDP 程序挂载到网卡 eth0 (推荐 Native 模式,如果网卡驱动支持)
    ip link set dev eth0 xdp obj xdp_drop.o sec xdp
    
    # 查看挂载状态
    ip link show eth0
    # 输出会包含: prog/xdp id 123 tag xxxxxxx
    

    此时再用 bpftool map 动态向 blacklist 中写入恶意 IP,被拦截的流量完全不会在 CPU si 中泛起波澜,系统 Load 瞬间恢复。

    丢包排查:用 bpftrace 追踪 kfree_skb 黑盒

    在上述流量清洗的过程中,常会遇到业务方反馈:“我的包明明发过去了,为什么网关没收到?”。此时,如果是协议栈内部某处静默丢包(如 MTU 不匹配、TCP 状态机异常、连接跟踪满),用 tcpdump 是看不出所以然的。

    内核丢弃数据包最终都会调用 kfree_skbconsume_skb(正常释放)。利用 eBPF 追踪 kfree_skb 是降维打击。

    在 Kernel 5.15 下,可以直接使用 bpftrace 一行命令定位丢包的确切内核调用栈:

    # 捕获 10 秒内所有因非正常原因丢包的内核栈并统计次数
    bpftrace -e '
    tracepoint:skb:kfree_skb {
        // args->reason 在 5.1x 较新内核引入,可直接区分丢包原因
        @[kstack] = count();
    }
    '
    

    如果你的内核支持 skb_drop_reason(Kernel 5.17+ 完善),甚至可以直接打印出人类可读的丢包枚举值。 在我们的排查过程中,通过上述命令输出了如下聚合栈:

    @[
        kfree_skb+1
        tcp_v4_rcv+1452
        ip_protocol_deliver_rcu+54
        ip_local_deliver_finish+108
        __netif_receive_skb_one_core+138
        process_backlog+164
        __napi_poll+42
        net_rx_action+582
    ]: 2450
    

    一针见血,包是在 tcp_v4_rcv 中被丢弃的。结合代码和偏移量,立刻定位到是处于 TIME_WAIT 状态的 socket 堆积,导致 PAWS(Protect Against Wrapped Sequence numbers)校验失败,触发了静默丢包。调整 net.ipv4.tcp_tw_reuse 和时间戳设置后,问题迎刃而解。没有 eBPF,这个问题在海量流量下排查至少需要拔几根头发。

    常见问题 (FAQ)

    Q1:XDP 有 Native 和 Generic 两种模式,性能差异多大? Native 模式下,XDP BPF 代码直接嵌入在网卡驱动的 NAPI poll 循环中执行,性能极高(线速丢包可达 10M~20M PPS)。而 Generic 模式(xdpgeneric)是作为回退方案,挂载在 sk_buff 分配之后、协议栈处理之前,性能大打折扣,失去了 XDP “零分配”的核心优势。实战中,如果网卡驱动(如 ixgbe, i40e, mlx5)支持,务必使用 Native 模式(xdpdrv)。

    Q2:加载 XDP 字节码时报错 bpf verifier errors,提示越界访问,怎么解决? eBPF 内核验证器(Verifier)极其严格,采用“防御性加载”策略。如果你在 C 代码中解析 IP 头部,但没有在使用指针前做边界检查(例如 if ((void *)(iph + 1) > data_end) return XDP_PASS;),验证器会认为该程序可能引发 Kernel Panic 并拒绝加载。必须为每一次网络包头部偏移读取增加严格的 data_end 边界校验。

    Q3:网关已经部署了 Cilium (基于 eBPF/XDP),我自己挂载的 XDP 会冲突吗? 会冲突。一个网卡的 RX 队列在同一时间点通常只能挂载一个 XDP 程序。如果强制挂载,后者的会覆盖前者,导致 Cilium 的网络路由与策略失效。在较新的内核中可以使用 libxdp 提供的多程序链(Multi-prog dispatcher)机制,将多个 XDP 程序按优先级串联(如将你的防刷 XDP 作为优先级最高的程序执行,如果 XDP_PASS,再交由 Cilium 的 XDP 程序处理)。

    Q4:为什么不用 TC (Traffic Control) BPF 做拦截? TC BPF 也是极好的网络控制点(支持 Ingress 和 Egress 双向),且能获取完整的 skb 上下文,功能比 XDP 更丰富(比如修改包长、克隆重定向)。但 TC Hook 点位于 skb 分配之后。如果你的首要目标是应对 L3/L4 层的洪水攻击或极限压榨 CPU 性能,选 XDP;如果是做复杂的流量整形、七层之前的深度负载均衡,选 TC。