高并发下 Netfilter 必然成为性能瓶颈。排查某次网关节点大面积丢包时,确认系海量小包打满 ksoftirqd 且 nf_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% 左右。 -
dmesg中nf_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_ARRAY 或 BPF_MAP_TYPE_PERCPU_HASH。这种 Map 会为每个 CPU 核心维护独立的数据副本,更新时无锁,用户态读取时再遍历所有 CPU 的值进行汇总。
Q3:使用 Cilium 替换 kube-proxy 后,NodePort 流量依然有延迟,如何排查?
Cilium 默认并不全量开启底层 XDP 加速。如果 NodePort 流量仍有延迟,需检查 Cilium Agent 配置是否启用了 bpf-node-port 和 kube-proxy-replacement=strict。可以通过 cilium status 查看 XDP 加速状态,并使用 cilium bpf nat list 确认底层的 eBPF NAT 表是否正常接管了 iptables 规则。如果网卡不支持 Native XDP,Cilium 会退化到 TC (Traffic Control) 层的 eBPF hook,性能会打折扣。