标签: ksoftirqd

  • 深入 XDP 陷阱排查:Jumbo Frame 触发 Generic 模式降级引发的 ksoftirqd 耗尽与网络吞吐雪崩实战

    排查网络加速和防御类问题时,最怕的不是程序崩溃,而是“静默降级”。近期处理了一起由 eBPF/XDP 引起的生产节点网络吞吐雪崩事故。业务团队在新扩容的 Kubernetes 节点上部署了基于 XDP 的防 DDoS 与流量可观测 Agent,结果上线后该批次节点 P99 延迟从 2ms 飙升至 200ms 以上,QPS 断崖式下跌,单机 Load Average 飙升至机器核心数的 3 倍以上。

    最终结论先行:业务侧编写 XDP 挂载逻辑时,未显式强制指定 XDP_FLAGS_DRV_MODE(Native 模式)。而新批次节点为了大数据传输开启了 Jumbo Frame(MTU=9000),导致底层网卡驱动(以 i40e 为例)拒绝了 Native XDP 挂载。更致命的是,挂载库触发了静默降级,自动回退到了 XDP_GENERIC(SKB 模式)。这使得本该在网卡驱动层极速处理的报文,全部走完了昂贵的 sk_buff 内存分配流程后再执行 XDP 逻辑,彻底击穿了 ksoftirqd,引发软中断 CPU 资源耗尽。

    案发现场与指标异动

    接到告警介入排查时,业务正处于高峰期。通过 mpstat -P ALL 1 观察,发现几个核心的 %soft(软中断 CPU 消耗)长期顶在 100%,而 %usr 和 %sys 极低。

    快速抓取 perf top 热点函数:

    # perf top -a --sort comm,dso,symbol
      28.45%  [kernel]  [k] build_skb
      15.21%  [kernel]  [k] do_softirq
      12.03%  [kernel]  [k] bpf_prog_3b185187f1855c4c_xdp_drop_func
       8.42%  [kernel]  [k] __netif_receive_skb_core
    

    看到 build_skb 高居榜首,且紧跟在 __netif_receive_skb_core 之后执行了 bpf 相关的函数,这绝对不是一个正常的 XDP 行为。XDP (eXpress Data Path) 的核心设计哲学就是在 sk_buff 分配之前,直接在网卡 RX Ring 缓冲区操作报文。如果 XDP 程序的执行路径里大量伴随 sk_buff 的分配,说明 XDP 根本没有在 Native 模式下运行。

    敲入基础网络排查命令查看网卡状态:

    $ ip -details link show dev eth0
    2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc mq state UP mode DEFAULT group default qlen 1000
        link/ether 0c:42:a1:53:xx:xx brd ff:ff:ff:ff:ff:ff
        prog/xdp id 142 tag 3b185187f1855c4c jited xdpgeneric 
    

    注意输出中的两个致命关键点:mtu 9000 和 xdpgeneric。 破案了。

    愚蠢的静默降级与底层机制剖析

    这是一个典型的“为了兼容性牺牲可用性”的低级陷阱。

    在 eBPF/XDP 的生态中,XDP 支持三种运行模式:

    1. Offloaded (XDP_FLAGS_HW_MODE):直接卸载到智能网卡硬件上执行。

    2. Native (XDP_FLAGS_DRV_MODE):在网卡驱动层执行,位于 DMA 完成之后、sk_buff 结构体分配之前。这是生产环境的标配。

    3. Generic (XDP_FLAGS_SKB_MODE):在内核网络协议栈中执行,位于 sk_buff 分配之后(__netif_receive_skb_core 中)。主要是为了让不支持 Native XDP 的网卡也能强行跑通 XDP 程序。

    问题出在哪里?很多开源库或研发在编写 XDP Loader 时,为了“让程序能跑起来”,挂载时如果不指定 Flag(即 flags = 0),内核和 libbpf 等库的默认行为是:先尝试 Native 模式,如果失败,静默回退(Fallback)到 Generic 模式。

    为什么新节点的 Native 模式会失败? 因为新节点由于某些存储业务的混部需求,将 eth0 的 MTU 改成了 9000(Jumbo Frame)。在许多主流网卡驱动(如 Intel ixgbe, i40e 在老版本内核)中,当 MTU 超过一个 Page Size(通常是 4KB),报文会跨页存储,而驱动的 XDP Native 实现由于内存布局限制,无法安全处理这种跨页报文,从而在挂载时返回 EOPNOTSUPP (Operation not supported)。

    挂载程序在收到失败后,自作聪明地用 XDP_GENERIC 重新挂载成功了。 结果就是:对于每一个抵达网卡的网络包,内核不仅要像往常一样消耗 CPU 周期分配 sk_buff,还要额外再跑一遍 XDP 里的 BPF 字节码。对于一个设计初衷是防 DDoS 的 XDP 程序来说,这简直是引狼入室——原本可以单核无压力扛下百万 pps 丢弃的流量,现在几万 pps 就能把机器的软中断 CPU 打满,导致正常业务报文排队直至超时,最终酿成雪崩。

    防御性加固与解决方案

    在基础设施的工程实践中,Fail-Fast(快速失败)永远好于静默降级。对于性能敏感型组件,宁愿程序启动报错退出,也绝不能以一种极其消耗资源的方式苟活。

    1. 强制阻断 Generic 降级路径

    在用户态 Loader(无论是 C/C++ 的 libbpf 还是 Go 的 cilium/ebpf)中,挂载 XDP 程序时必须显式指定 XDP_FLAGS_DRV_MODE。

    如果是基于 libbpf 的 C 代码:

    // 严禁使用 flags = 0;
    __u32 flags = XDP_FLAGS_UPDATE_IF_NOEXIST | XDP_FLAGS_DRV_MODE;
    int err = bpf_set_link_xdp_fd(ifindex, prog_fd, flags);
    if (err) {
        fprintf(stderr, "FATAL: Failed to attach XDP in Native Mode. Error: %d\n", err);
        exit(EXIT_FAILURE); // 拒绝降级,直接报错中断部署
    }
    

    如果是 Go 语言使用 cilium/ebpf:

    link, err := netlink.LinkByName("eth0")
    // ...
    err = netlink.LinkSetXdpFdWithFlags(link, prog.FD(), int(netlink.XDP_FLAGS_DRV_MODE))
    if err != nil {
        log.Fatalf("Failed to attach native XDP: %v", err)
    }
    

    2. 梳理 MTU 与驱动兼容矩阵

    如果业务确实依赖 Jumbo Frame,且必须使用 XDP,则需要评估底层网卡驱动及内核版本。 较新的内核(例如 5.14+)和部分驱动(如 Mellanox mlx5)已经增加了对 XDP 多缓冲(multi-buffer)的支持,能够处理非线性跨页的 XDP 报文。如果硬件和内核不支持,架构上就需要将 XDP 的防御节点前置(例如部署在专属的边界清洗集群,保持 MTU 1500),而不是强行在混合了 Jumbo Frame 的 Worker 节点上跑。

    XDP/eBPF 性能陷阱排查清单

    为了避免类似惨剧,在涉及 eBPF/XDP 的生产环境排查时,请直接核对以下清单:

    1. 挂载模式确认 (ip link show)

      • 检查输出中的标签是 xdp 还是 xdpgeneric。如果是后者,说明你的 XDP 毫无性能加速可言,反而带来了额外的 CPU 损耗。
    2. 软中断热点分析 (perf top)

      • 发现 ksoftirqd CPU 偏高时,如果热点栈顶是 build_skb、__netif_receive_skb_core 伴随 bpf_prog_xxx,直接定性为 XDP 未跑在 Native 模式。
    3. MTU 兼容性预检 (ip link show dev xxx)

      • 当需要挂载 XDP 时,检查目标网卡 MTU 是否 > 1500。如果大于,查阅对应网卡驱动(ethtool -i xxx 获取驱动名)与当前内核版本对 XDP MTU 的限制要求。
    4. XDP Map 并发瓶颈 (bpftool map show)

      • 若挂载模式正确但 CPU 依旧飙高,检查 XDP 路径中是否高频使用了自旋锁(bpf_spin_lock)或存在极端大的 Hash Map 频繁查找与更新。网卡 RX 队列并发处理时,劣质的 Map 操作会引发严重的 Cache Line 颠簸。
    5. 驱动环形缓冲区积压 (ethtool -S xxx | grep rx_missed)

      • 如果 XDP 程序的逻辑过于复杂(超过 BPF 指令限制或涉及深层循环),即使挂载了 Native 模式,也会导致单包处理时间变长。通过观察 rx_missed_errors 或 rx_dropped 指标,判断网卡底层缓冲区是否因消费过慢而被塞满丢包。
  • 深入 eBPF/XDP 丢包雪崩排查:Hash Map 满载引发的 XDP_DROP 风暴与 ksoftirqd 饱和实战

    近期排查了一起极其诡异的边缘节点网络雪崩事故。业务表现为随机的 TCP 建连超时,API 网关 P99 延迟阶段性飙升至 3 秒以上(典型的 SYN 丢包重传)。经过链路排查,最终定位于一个新上线的基于 XDP (eXpress Data Path) 的防 DDoS 阻断程序。 核心结论:开发在编写 eBPF 代码时,错误地使用基础的 BPF_MAP_TYPE_HASH 来记录源 IP 访问频率,且未配置任何过期清理逻辑;当 Map 满载后 bpf_map_update_elem 调用失败,异常处理分支居然默认返回了 XDP_DROP。更致命的是,部署脚本未校验网卡驱动兼容性,在部分节点回退到了 Generic XDP (SKB 模式),不仅没起到加速作用,反而直接打爆了 ksoftirqd 软中断。

    这不仅仅是代码 Bug,更是对生产环境缺乏敬畏之心的体现。写底层网络逻辑,不带防御性编程思维,等同于在主干道上埋雷。

    案发现场:消失的 SYN 包与哀嚎的软中断

    监控告警最先在部分 BGP 边缘节点触发,现象非常割裂:

    1. 网络层面:外部监控探测大面积报 TCP Timeout。ss -s 发现节点 SYN-RECV 极少,但外部抓包显示 SYN 已经到达物理机网卡。

    2. 系统层面:出问题的节点 Load Average 飙高,top -1 发现个别 CPU 核心的 si(Soft Interrupt)使用率长时间顶在 100%,进程 ksoftirqd/x 霸榜。

    3. 传统排查失效:内核 dmesg 无任何报错,conntrack -S 表项未满,iptables/nftables 的 drop 计数器毫无波澜。

    包进来了,但在进入内核协议栈(Netfilter)之前就凭空消失了。结合软中断被打满的现象,直觉告诉我:有人在网卡底层动了手脚,极大概率是 tc 或者 XDP。

    现场拆解:用 bpftool 扒掉“黑盒”的底裤

    既然怀疑是底层 Hook,直接上 bpftool 查户口。

    执行 bpftool net show,果不其然,网卡 eth0 上挂着东西:

    # bpftool net show
    xdp:
    eth0(2) generic id 142 act XDP_DROP
    

    这里立刻暴露了两个致命问题:

    1. 模式不对:显示为 generic 而不是 driver。Native XDP 是在网卡驱动层(Ring Buffer 刚分配完)处理数据,性能极高;而 Generic XDP 是内核为了兼容不支持 XDP 的网卡做的妥协,它是在 sk_buff 已经分配,甚至包已经进入网络协议栈入口后才执行 eBPF 字节码。此时拦截毫无性能优势,反而因为额外的 BPF 执行逻辑增加了 NET_RX 软中断的开销,直接导致 ksoftirqd 饱和。

    2. 阻断风暴:当前挂载的程序 ID 是 142。

    接着查看具体挂载了什么程序和它的 Map 状态:

    # bpftool prog show id 142
    142: xdp  name xdp_ddos_block  tag 3b185187f1855c4c  gpl
            loaded_at 202X-XX-XXT10:00:00+0800  uid 0
            xlated 528B  jited 312B  memlock 4096B
            map_ids 45
    

    提取关联的 Map ID 45,查看 Map 容量与元素数量:

    # bpftool map show id 45
    45: hash  name ip_stat_map  flags 0x0
            key 4B  value 8B  max_entries 65536  memlock 5242880B
    # bpftool map dump id 45 | grep "Found"
    Found 65536 elements
    

    破案了。max_entries 是 65536,当前已经存了 65536 个元素。Map 被彻底打满了。

    源码处刑:无脑的 XDP_DROP 与缺失的驱逐机制

    把开发叫来,翻开 eBPF C 源码,看到如下逻辑片段时,我血压直接上来了:

    struct bpf_map_def SEC("maps") ip_stat_map = {
        .type = BPF_MAP_TYPE_HASH, // 致命错误1:普通的 Hash Map
        .key_size = sizeof(__u32),
        .value_size = sizeof(struct ip_stat),
        .max_entries = 65536,
    };
    
    SEC("xdp")
    int xdp_ddos_block(struct xdp_md *ctx) {
        // ... 解析 IP 头 ...
        __u32 src_ip = iph->saddr;
    
        struct ip_stat *stat = bpf_map_lookup_elem(&ip_stat_map, &src_ip);
        if (!stat) {
            struct ip_stat new_stat = { .count = 1, .last_time = bpf_ktime_get_ns() };
            // 致命错误2:未判断更新失败的情况,直接放任后续逻辑或采取错误假设
            int ret = bpf_map_update_elem(&ip_stat_map, &src_ip, &new_stat, BPF_ANY);
            if (ret != 0) {
                // 致命错误3:更新 Map 失败(如满了),直接当做异常流量 Drop 掉!
                return XDP_DROP; 
            }
        } else {
            // ... 频率检测逻辑 ...
        }
        return XDP_PASS;
    }
    

    灾难逻辑剖析: 开发者的本意是:“如果连记录状态都失败了,说明系统可能在被严重攻击,为了安全起见,宁可杀错不可放过,直接丢弃(XDP_DROP)”。 但他们忽略了网络世界的复杂性:互联网每天有大量的扫描器、僵尸网络发起一次性连接。使用 BPF_MAP_TYPE_HASH,这些单次访问的源 IP 会永远占据坑位。没有用户态进程去定时清理,也没有内核级的 LRU (Least Recently Used) 淘汰机制,不到几个小时,65536 的容量必然耗尽。 Map 满载后,后续所有正常用户的全新 IP 访问,在执行 bpf_map_update_elem 时都会返回 -E2BIG。代码捕获到这个错误,果断执行了 XDP_DROP。 最终结果就是:防 DDoS 的程序自己变成了一个完美的高性能 DDoS 攻击器,对所有新访客实施无差别静默丢包。

    技术结论与重构方案

    XDP 的极强性能来源于其极其底层的执行位置,但这也意味着它完全脱离了 Linux 协议栈成熟的异常处理、垃圾回收和可观测性体系。能力越大,越需要防御性编程。

    针对该故障,我们进行了彻底整改:

    1. 废弃标准 HASH,强制使用 LRU HASH: 将 Map 类型修改为 BPF_MAP_TYPE_LRU_HASH。当 Map 容量达到 max_entries 时,内核会自动淘汰最久未访问的元素,腾出空间给新连接。永远不要在没有外部 GC 守护进程的情况下,在网络数据面使用不具备自动淘汰机制的 Map。

    2. 修正 Fail-Open (容错放行) 逻辑: 监控程序自身的异常不应导致业务中断。如果 Map 更新失败,正确的做法是打印 BPF Trace 日志或增加异常统计 Counter,并返回 XDP_PASS 交给上层内核协议栈处理,而不是傲慢地返回 XDP_DROP。

    3. 强制 Native 模式加载,彻底告别 Generic 软中断陷阱: 修改加载程序(或使用 ip link set dev eth0 xdp obj xxx.o sec xdp 时明确指定模式)。我们重写了加载工具,如果在给定的网卡上 XDP_FLAGS_DRV_MODE (Native 模式) 挂载失败,应当直接终止部署并告警,绝对不允许静默回退到 XDP_FLAGS_SKB_MODE。

    同类问题速查排查清单 (eBPF/XDP 故障急救)

    1. 确认 XDP 是否介入及运行模式: bpftool net show 或 ip link show。重点看接口后是否带有 xdp 标记,以及是 xdpgeneric 还是 xdpdrv。如果是 xdpgeneric 且系统软中断高,直接拔掉 XDP 恢复业务。

    2. 检查 eBPF Map 的满载情况: 使用 bpftool map show 获取所有 Map 的 max_entries,再用 bpftool map dump id | grep "Found" 检查当前元素量。接近或等于满载的,必定会引发 bpf_map_update_elem 返回 -E2BIG,需立即排查代码异常分支逻辑。

    3. 定位静默丢包(Drop)统计: XDP 丢包不会体现在 iptables 里。除了在代码里自建 BPF Perf Event 或 Ring Buffer 输出丢包日志外,可以通过 ethtool -S | grep xdp_drop (依赖具体网卡驱动支持)来观测底层拦截量。

    4. 内核 BPF 调试日志探测: 如果开发在代码中使用了 bpf_printk,可通过 cat /sys/kernel/debug/tracing/trace_pipe 查看实时内核 eBPF 打印的报错信息,往往能一针见血发现诸如 Map Update 失败的错误。