标签: XDP

  • 深入 eBPF/XDP 网络雪崩排查:Netfilter 软中断打满引发的丢包与 XDP 内核级加速防御实战

    高并发下 Netfilter 必然成为性能瓶颈。排查某次网关节点大面积丢包时,确认系海量小包打满 ksoftirqdnf_conntrack 溢出导致。直接抛弃 iptables 方案,通过 eBPF 挂载 XDP 程序在网卡驱动层(SKB 分配前)进行拦截与转发,CPU 软中断开销骤降 80%,99线延迟从 200ms 恢复至 2ms,系统吞吐量提升三个数量级。

    故障现场:ksoftirqd 榨干 CPU 与 Conntrack 溢出

    某次生产环境的高并发突发流量下,K8S Ingress 节点(OS: Ubuntu 22.04, 内核 Linux 5.15)出现大面积请求超时。前端监控显示 99 线延迟飙升至 200ms 以上,甚至出现 502/504 错误。

    登录宿主机,第一眼看系统负载:

    $ uptime
     10:14:32 up 45 days, 14:20,  2 users,  load average: 84.12, 75.33, 60.10
    

    Load Average 极高,敲击键盘都有迟滞感。直接看 CPU 消耗,top 里的 si(Soft Interrupt)指标在多个核心上死死顶在 100%,相关的进程全是 ksoftirqd/n

    %Cpu(s):  1.5 us,  3.2 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi, 95.3 si,  0.0 st
      PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
       14 root      20   0       0      0      0 R  99.9   0.0  10:23.12 ksoftirqd/1
       20 root      20   0       0      0      0 R  99.9   0.0   9:14.05 ksoftirqd/2
    

    与此同时,dmesg 中正在疯狂刷屏经典报错:

    $ dmesg -T | tail -n 5
    [Thu Oct 12 10:15:01 2023] nf_conntrack: nf_conntrack: table full, dropping packet
    [Thu Oct 12 10:15:01 2023] nf_conntrack: nf_conntrack: table full, dropping packet
    

    典型的网络软中断风暴 + 连接跟踪表打满。虽然通过 sysctl -w net.netfilter.nf_conntrack_max=2097152 临时缓解了丢包,但这只是扬汤止沸,软中断依然居高不下,节点的网络栈已经处于半瘫痪状态。

    为什么传统的 iptables/Netfilter 在高并发下必然雪崩?

    要理解这场雪崩,必须拆解 Linux 传统的网络收包路径。

    当网卡收到一个数据包时,硬中断触发后,真正的重头戏在软中断 NET_RX_SOFTIRQ。此时,内核会为每个数据包调用 __alloc_skb() 分配一个 sk_buff 结构体。这个结构体极其庞大(通常包含数百个字段),高频的内存分配和释放本身就是巨大的开销。

    紧接着,包会进入内核协议栈,穿越 Netfilter 的重重关卡(PREROUTING, INPUT, FORWARD 等)。如果是 K8S 环境,kube-proxy 写入的数万条 iptables 规则会以线性或树状(ipset)进行匹配。最致命的是 Conntrack(连接跟踪) 机制。每次建连,内核都要加锁更新连接状态表。当 PPS(每秒包数)达到数十万级别时,nf_conntrack 的自旋锁竞争会导致 CPU 缓存命中率暴跌,最终表现为 ksoftirqd 吃满 CPU,后续的包连 sk_buff 都分配不到,直接在网卡 Ring Buffer 处被丢弃。

    可观测性介入:用 eBPF/bpftrace 精准定位丢包点

    在实施改造前,我们需要硬核的数据佐证。只看 dmesg 不够,到底包是在协议栈的哪一步被 Drop 的? 利用 bpftrace 编写一行脚本,直接 Hook 内核的 kfree_skb 函数(内核丢弃数据包时通常会调用它),并打印调用栈:

    # 依赖环境: bpftrace 0.14.0+
    $ bpftrace -e 'kprobe:kfree_skb /comm == "ksoftirqd/1"/ { @[kstack] = count(); }'
    

    运行 10 秒后 Ctrl+C 停止,输出的核心堆栈如下:

    @[
        kfree_skb+1
        nf_conntrack_in+1345
        ipv4_conntrack_in+28
        nf_hook_slow+66
        ip_rcv+165
        __netif_receive_skb_core+2180
        net_rx_action+354
        __do_softirq+215
        run_ksoftirqd+42
    ]: 45210
    

    数据确凿:短短 10 秒内,在 nf_conntrack_in 链路下触发了 4.5 万次 kfree_skb。传统的防御方案(如加机器、调大 sysctl 参数)在百万级 PPS 面前毫无招架之力。必须进行降维打击——绕过 sk_buff 和 Netfilter。

    降维打击:XDP (eXpress Data Path) 零拷贝拦截实战

    XDP 是基于 eBPF 的一项技术,它允许我们在网卡驱动层,即数据包刚通过 DMA 拷贝到内存,尚未分配 sk_buff 之前,执行我们自定义的 eBPF 程序。

    排查过程中,我们发现异常流量具有明显的端口和 IP 聚集特征。直接编写 XDP 程序,对恶意流量执行 XDP_DROP,对合法突发流量直接在驱动层打标或放行。

    以下是精简后的 XDP C 代码(xdp_filter.c),实现对特定目标端口(如 8080)的异常小包直接 Drop:

    #include <linux/bpf.h>
    #include <linux/if_ether.h>
    #include <linux/ip.h>
    #include <linux/tcp.h>
    #include <linux/in.h>
    #include <bpf/bpf_helpers.h>
    
    SEC("xdp")
    int xdp_drop_prog(struct xdp_md *ctx) {
        // 获取数据包的起止指针
        void *data_end = (void *)(long)ctx->data_end;
        void *data     = (void *)(long)ctx->data;
    
        // 解析以太网头部
        struct ethhdr *eth = data;
        if (data + sizeof(*eth) > data_end)
            return XDP_PASS;
    
        // 仅处理 IPv4
        if (eth->h_proto != bpf_htons(ETH_P_IP))
            return XDP_PASS;
    
        // 解析 IP 头部
        struct iphdr *iph = data + sizeof(*eth);
        if ((void *)iph + sizeof(*iph) > data_end)
            return XDP_PASS;
    
        // 解析 TCP 头部
        if (iph->protocol == IPPROTO_TCP) {
            struct tcphdr *tcph = (void *)iph + sizeof(*iph);
            if ((void *)tcph + sizeof(*tcph) > data_end)
                return XDP_PASS;
    
            // 如果目标端口是 8080,直接在网卡驱动层丢弃 (模拟黑洞)
            if (tcph->dest == bpf_htons(8080)) {
                // 可在此处加入 eBPF Map 统计丢包数量
                return XDP_DROP;
            }
        }
    
        // 其他数据包正常进入协议栈
        return XDP_PASS;
    }
    
    char _license[] SEC("license") = "GPL";
    

    编译与挂载: 利用 Clang 将 C 代码编译为 BPF 字节码,并通过 iproute2 工具直接挂载到宿主机物理网卡(如 eth0)。

    # 编译 (需安装 clang 12+ 和 linux-headers)
    $ clang -O2 -g -Wall -target bpf -c xdp_filter.c -o xdp_filter.o
    
    # 以 Native 模式挂载到 eth0
    $ ip link set dev eth0 xdp obj xdp_filter.o sec xdp
    
    # 查看挂载状态
    $ ip link show eth0
    2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdp/id:45 qdisc mq state UP mode DEFAULT group default qlen 1000
        link/ether 00:16:3e:xx:xx:xx brd ff:ff:ff:ff:ff:ff
        prog/xdp id 45 tag 8fxxxxxx
    

    效果对比: 挂载 XDP 后,恶意流量在网卡驱动层即被截断,根本不会触发 alloc_skb,更不会进入 Netfilter。

    • ksoftirqd 的 CPU 占用率从 100% 瞬间暴降至 15% 左右。

    • dmesgnf_conntrack 报错消失。

    • 合法业务流量的 99 线延迟恢复到健康的 2ms 范围内。

    常见问题 (FAQ)

    Q1:XDP 的 Generic 模式和 Native 模式有什么性能差异? Generic 模式(xdpgeneric)是内核网络栈模拟的 XDP,此时 sk_buff 已经分配,性能提升有限,主要用于测试或不支持 XDP 的网卡驱动。Native 模式(xdp)是在网卡驱动层实现,包刚放入内存就触发,零拷贝,性能是 Generic 模式的 4-5 倍。生产环境必须确保网卡驱动(如 ixgbe, mlx5)支持 Native XDP。

    Q2:eBPF Map 并发读写时如何保证数据一致性? 在多核并发场景下,统计包量等操作直接更新普通的 Array/Hash Map 会有竞态问题。应当使用 BPF_MAP_TYPE_PERCPU_ARRAYBPF_MAP_TYPE_PERCPU_HASH。这种 Map 会为每个 CPU 核心维护独立的数据副本,更新时无锁,用户态读取时再遍历所有 CPU 的值进行汇总。

    Q3:使用 Cilium 替换 kube-proxy 后,NodePort 流量依然有延迟,如何排查? Cilium 默认并不全量开启底层 XDP 加速。如果 NodePort 流量仍有延迟,需检查 Cilium Agent 配置是否启用了 bpf-node-portkube-proxy-replacement=strict。可以通过 cilium status 查看 XDP 加速状态,并使用 cilium bpf nat list 确认底层的 eBPF NAT 表是否正常接管了 iptables 规则。如果网卡不支持 Native XDP,Cilium 会退化到 TC (Traffic Control) 层的 eBPF hook,性能会打折扣。

  • 深入 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。