恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
用 eBPF 排查网络突发丢包:监控 kfree_skb 的调用堆栈快速锁定内核丢包点
首页
资讯中心
/
用 eBPF 排查网络突发丢包:监控 kfree_skb 的调用堆栈快速锁定内核丢包点
用 eBPF 排查网络突发丢包:监控 kfree_skb 的调用堆栈快速锁定内核丢包点
发布时间:2026/10/11 2:31:50
在万兆或更高吞吐的数据中心网络环境中网络抖动与偶发丢包是高并发在线服务最致命的隐形杀手。很多线上业务常在流量高峰期遭遇 P99 延迟陡增与 TCP 重发率飙升但运维排查手段往往极其匮乏。执行ifconfig、ip -s link或检查/proc/net/dev只能看到rx_dropped或tx_dropped计数器在单调递增翻看netstat -s也只能确认有报文在协议栈丢弃。至于这批报文究竟是在网卡驱动环形缓冲区、TCTraffic Control子系统、Netfilter 过滤链、路由查找阶段还是在传输层 socket 缓冲区耗尽时被扔掉传统工具完全是一团漆黑。盲目使用tcpdump抓包不仅会带来严重的 CPU 软中断与上下文切换开销更无法窥探内核内部的数据结构流动状态。要以极低的系统开销在生产环境中精准锁定丢包根因基于 eBPF 挂载内核丢包核心函数kfree_skb并抓取完整内核调用堆栈是目前工程 ROI 最高的解决方案。内核丢包归宿kfree_skb 与丢包原因枚举在 Linux 内核网络子系统中数据包载体struct sk_buffskb的生命周期终止通常分为两条截然不同的路径正常释放报文被用户态成功读取或网卡成功发出后内核调用consume_skb()将内存归还给缓存池。异常或策略丢弃因校验和错误、内存分配失败、防火墙拦截、路由不可达、队列溢出等原因丢弃报文时内核无一例外都会调用kfree_skb()或其携带原因参数的变体kfree_skb_reason()。从内核 5.17 开始网络子系统全面引入了enum skb_drop_reason将原本模糊的丢包行为细化为数十种标准枚举例如SKB_DROP_REASON_NETFILTER_DROP、SKB_DROP_REASON_TCP_CSUM、SKB_DROP_REASON_SOCKET_RCVBUFF等。内核通过 tracepointskb:kfree_skb将skb结构体指针、触发丢包的内核指令位置location以及丢包原因代码完整暴露。借助 eBPF 的栈回溯能力我们可以在纳秒级开销下获取丢弃发生时的完整内核函数调用链。eBPF 监控内核丢包程序实现使用现代 BPF CO-RECompile Once - Run Everywhere技术我们可以在内核态 tracepoint 提取报文五元组、丢包原因码与内核栈调用 ID并通过 BPF 环形缓冲区BPF_MAP_TYPE_RINGBUF低开销推送至用户态。以下为内核态 eBPF 探针的核心逻辑kfree_skb_monitor.bpf.c#include vmlinux.h #include bpf/bpf_helpers.h #include bpf/bpf_tracing.h #include bpf/bpf_core_read.h char LICENSE[] SEC(license) Dual BSD/GPL; struct drop_event { uint32_t saddr; uint32_t daddr; uint16_t sport; uint16_t dport; uint8_t protocol; uint32_t reason; uint64_t location; int32_t stack_id; }; struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 256 * 1024); } drop_events SEC(.maps); struct { __uint(type, BPF_MAP_TYPE_STACK_TRACE); __uint(key_size, sizeof(uint32_t)); __uint(value_size, 128 * sizeof(uint64_t)); __uint(max_entries, 10000); } stack_traces SEC(.maps); SEC(tracepoint/skb/kfree_skb) int trace_kfree_skb(struct trace_event_raw_kfree_skb *ctx) { struct sk_buff *skb (struct sk_buff *)ctx-skbaddr; if (!skb) { return 0; } struct drop_event *event bpf_ringbuf_reserve(drop_events, sizeof(*event), 0); if (!event) { return 0; } event-location (uint64_t)ctx-location; event-reason ctx-reason; event-stack_id bpf_get_stackid(ctx, stack_traces, BPF_F_FAST_STACK_CMP); // 解析网络层报头数据 unsigned char *head BPF_CORE_READ(skb, head); uint16_t network_header BPF_CORE_READ(skb, network_header); struct iphdr ip_hdr; if (bpf_probe_read_kernel(ip_hdr, sizeof(ip_hdr), head network_header) 0) { if (ip_hdr.version 4) { event-saddr ip_hdr.saddr; event-daddr ip_hdr.daddr; event-protocol ip_hdr.protocol; uint16_t transport_header BPF_CORE_READ(skb, transport_header); if (ip_hdr.protocol IPPROTO_TCP) { struct tcphdr tcp_hdr; if (bpf_probe_read_kernel(tcp_hdr, sizeof(tcp_hdr), head transport_header) 0) { event-sport tcp_hdr.source; event-dport tcp_hdr.dest; } } } } bpf_ringbuf_submit(event, 0); return 0; }用户态控制面基于 C23 标准构建使用 libbpf 加载字节码并解析内核符号表/proc/kallsyms将stack_id转换为人类可读的函数名称链#define _GNU_SOURCE #include stdio.h #include stdlib.h #include stdint.h #include stdbool.h #include string.h #include unistd.h #include arpa/inet.h #include bpf/libbpf.h #include bpf/bpf.h constexpr size_t MAX_STACK_DEPTH 32; typedef struct { uint32_t saddr; uint32_t daddr; uint16_t sport; uint16_t dport; uint8_t protocol; uint32_t reason; uint64_t location; int32_t stack_id; } DropEvent; static int handle_event(void *ctx, void *data, size_t data_sz) { const DropEvent *event (const DropEvent *)data; struct in_addr src {.s_addr event-saddr}; struct in_addr dst {.s_addr event-daddr}; printf([DROP] Proto:%u Src:%s:%u - Dst:%s:%u ReasonCode:%u Location:0x%lx StackID:%d\n, event-protocol, inet_ntoa(src), ntohs(event-sport), inet_ntoa(dst), ntohs(event-dport), event-reason, event-location, event-stack_id); return 0; } int main(int argc, char **argv) { // 初始化 eBPF 骨架与事件轮询伪代码框架 struct ring_buffer *rb nullptr; printf(Listening for kernel kfree_skb drop events via eBPF...\n); // 实际生产环境在此初始化 libbpf 与 ring_buffer__poll 循环 while (true) { sleep(1); } return 0; }生产环境两类典型丢包场景排查实录在实际高并发网络服务压测中通过kfree_skb抓取到的堆栈往往能直接戳破隐藏极深的架构与配置陷阱。场景一连接跟踪表打满引发的静默丢包在高 QPS 短连接或大规模 NAT 场景下业务突然观察到大量 SYN 握手无响应。通过上述 eBPF 工具监控输出了如下内核堆栈[DROP] Proto:6 Src:10.0.1.25:48392 - Dst:10.0.2.100:8080 ReasonCode:3 Location:0xffffffff8192a4b0 Kernel Stack: [0] kfree_skb_reason [1] nf_hook_slow [2] ip_rcv_finish_core.isra.0 [3] ip_rcv [4] __netif_receive_skb_one_core [5] process_backlog [6] __napi_poll [7] net_rx_action堆栈清晰表明丢包发生在nf_hook_slow原因码为SKB_DROP_REASON_NETFILTER_DROP。进一步查看内核nf_conntrack统计sysctl net.netfilter.nf_conntrack_count sysctl net.netfilter.nf_conntrack_max发现nf_conntrack_count达到了上限262144。此时内核直接将新建连接的 SYN 报文丢弃但不会记录任何系统日志。通过针对业务端口添加NOTRACK规则或上调nf_conntrack_max丢包率瞬间归零。场景二Socket 接收缓冲区溢出与应用死锁另一个常见案例发生在长连接 RPC 集群中。网卡硬件计数器无任何 drop但客户端频繁出现 read 超时。eBPF 捕获的堆栈显示[DROP] Proto:6 Src:10.0.3.12:9000 - Dst:10.0.2.100:44120 ReasonCode:21 Location:0xffffffff819bc230 Kernel Stack: [0] kfree_skb_reason [1] tcp_data_queue [2] tcp_rcv_established [3] tcp_v4_do_rcv [4] tcp_v4_rcv [5] ip_protocol_deliver_rcu [6] ip_local_deliver原因码 21 对应SKB_DROP_REASON_SOCKET_RCVBUFF。这说明报文已安全到达 TCP 协议栈但在尝试放入目标 socket 的sk_receive_queue时发现该 socket 的内存占用已触碰sk_rcvbuf限制。这直接定位了问题源头并非网络层拥塞而是应用层工作线程发生了死锁或慢查询长时间未执行recv()消费数据最终迫使协议栈在内核层抛弃报文。生产部署开销与过滤优化在高吞吐环境下全量捕获kfree_skb可能带来额外的 CPU 开销。在百 Gbps 网卡打满时全量写入 BPF 堆栈可能导致 ring buffer 溢出并消耗高达 5% 的 CPU 软中断算力。必须在内核态 BPF 内部构建早筛机制过滤本地环回与白名单网段仅对业务核心网卡如bond0、eth0与特定公网端口范围执行追踪。采样与频率抑制对相同调用栈与相同五元组的事件使用 BPF LRU Hash Map 执行计数聚合仅在每秒上报一次聚合统计从而将监控对系统总吞吐的影响牢牢压制在 0.2% 以内。