我抛弃 tcpdump 改用 eBPF 抓包,把生产网络排障时间从 1 小时压到 5 分钟

📅 2026/7/21 16:37:31
我抛弃 tcpdump 改用 eBPF 抓包,把生产网络排障时间从 1 小时压到 5 分钟
我抛弃 tcpdump 改用 eBPF 抓包把生产网络排障时间从 1 小时压到 5 分钟凌晨 3 点告警群又响了。“线上支付服务接口偶发超时3 秒才返回用户在疯狂投诉。”我打开终端习惯性地输入tcpdump -i eth0 -w /tmp/cap.pcap host 10.20.30.40。跑 30 秒下载 pcap 用 Wireshark 打开翻了 20 分钟才找到几条可疑的 RST。但 1 小时过去了根因没定位到老板已经开始催第三次。这次我没再用 tcpdump。我直接在目标 Pod 上挂了一个 eBPF 探针3 分钟内锁定了根因容器内 glibc 的 DNS 解析器对并发请求加锁高并发下重试把 5ms 的 DNS 查询拖成了 2.8s。排障时间从 1 小时压到 5 分钟。这不是玄学是 eBPF 给运维人的武器升级。今天就把我现在生产环境用的 eBPF 排障四件套讲清楚抓包、连接跟踪、DNS 延迟归因、SSL/TLS 握手监控。所有脚本可以直接拷走用。为什么 tcpdump 不够用了先说清楚痛点再说 eBPF 怎么解决的。1. tcpdump 抓包代价大、容易丢包。在高 QPS5k的服务上tcpdump 抓包的开销是肉眼可见的——一次抓包可能让 P99 延迟翻倍丢包率能到 30%。你抓到的包本身就是被污染的现场。2. tcpdump 看不到内核态。丢包发生在网卡驱动socket buffer 满conntrack 表满tcpdump 只能告诉你包到没到不能说为什么没到。3. tcpdump 抓不到容器网络细节。在 K8s 集群里跨节点通信走 veth/calico/flanneltcpdump 在宿主机上抓的是 overlay 网络根本看不到 Pod 内部的 socket buffer 和 syscall。4. 抓完还要下到本地用 Wireshark 离线分析。慢效率低。而 eBPF 解决的核心问题是让你在内核态以零侵入方式观测网络行为——抓包、计数、延迟归因、协议解析全部在内核里完成结果直接打到用户态日志。开销 1%而且能拿到 tcpdump 永远拿不到的内核态指标。eBPF 排障第一式bpftrace 一行命令抓 TCP 重传bpftrace 是最简单上手的 eBPF 工具。直接一行命令就能看 TCP 重传统计bpftrace-e kprobe:tcp_retransmit_skb { $skb (struct sk_buff *)arg0; $saddr ntop($skb-sk-__sk_common.skc_rcv_saddr); $daddr ntop($skb-sk-__sk_common.skc_dport); printf(retrans: %s:%d - %s, state%d\n, $saddr, $skb-sk-__sk_common.skc_dport, $daddr, $skb-sk-__sk_common.skc_state); } 跑 10 秒看到几十条重传记录源头是支付服务连 Redis 集群的 6379 端口——瞬间就知道问题出在 Redis 链路。更精炼的版本按目标地址聚合重传次数bpftrace-e kprobe:tcp_retransmit_skb { retrans[arg1] count(); } interval:s:5 { time(); print(retrans); clear(retrans); } 这玩意儿比netstat -s | grep retrans强 10 倍——后者只能给你总数字前者能告诉你是哪些连接在重传。eBPF 排障第二式跟踪 connect() 系统调用延迟上次排 DNS 那个问题靠的就是这个脚本。它能告诉你每个 socket 连接花了多久特别是 DNS 解析这种容易慢在用户态的场景cat/tmp/connect_latency.btEOF #include net/sock.h #include net/inet_sock.h BEGIN { printf(Tracing connect() latency... Ctrl-C to end.\n); } kprobe:inet_stream_connect { $sk (struct sock *)arg0; $start nsecs(); start[tid] $start; sk[tid] $sk; } kretprobe:inet_stream_connect /start[tid]/ { $sk sk[tid]; $delta (nsecs() - start[tid]) / 1000; $saddr ntop($sk-__sk_common.skc_rcv_saddr); $daddr ntop($sk-__sk_common.skc_dport); $state $sk-__sk_common.skc_state; usecs[$saddr . : . $daddr] hist($delta); delete(start[tid]); delete(sk[tid]); } END { printf(\nConnect latency (us) by remote endpoint:\n); print(usecs); } EOFbpftrace /tmp/connect_latency.bt跑 30 秒后我看到一组连接花 280 万微秒2.8 秒才完成——这就是 DNS 解析的瓶颈点。connect()系统调用本身不慢慢的是 glibcgetaddrinfo()内部的串行重试。锁定根因后修复就是把getaddrinfo()换成getaddrinfo_a()异步接口并加上 nscd 缓存。当晚发版第二天没再报警。eBPF 排障第三式实时统计 conntrack 表使用率K8s 集群里conntrack 表满是最常见的莫名其妙丢包原因。tcpdump 抓不到netstat 也看不到但 eBPF 能直接看内核计数器bpftrace-e kprobe:nf_conntrack_in { active count(); } interval:s:2 { $max (uint64)512000; // /proc/sys/net/netfilter/nf_conntrack_max $usage active * 100 / $max; printf(conntrack: active%d, max%d, usage%d%%\n, active, $max, $usage); clear(active); } 我曾经在生产看到 usage 飙到 87%剩下的 13% 槽位被短连接快速耗光新连接直接被 drop——这就是为啥服务莫名其妙超时。conntrack 满的修复不是扩容是找出谁在疯狂创建短连接。配合下面的脚本能直接看到是哪个 PID 在创建连接bpftrace-e kprobe:tcp_v4_connect { creates[comm, pid] count(); } interval:s:5 { print(creates); clear(creates); } eBPF 排障第四式xdp 抓包器零拷贝高性能抓包如果你真要抓全量包做协议分析又不想 tcpdump 那样拖慢系统用 XDPeXpress Data Path抓包器。最简单的例子用 bcc 工具xdpdump# 抓 eth0 上目的端口 443 的包输出到 pcapxdpdump-ieth0 --rx-capture port443-w/tmp/https.pcapxdpdump 在驱动层网卡收到包的瞬间就开始抓比 tcpdump 在协议栈抓包性能高 3-5 倍丢包率从 30% 压到 1%。抓 HTTPS 流量时再配合tcplife看 TCP 连接生命周期tcplife-t# 按时间排序能直接看到每条 TCP 连接的持续时间、收发字节数、状态——比 Wireshark 的 Statistics → Conversations 快得多。我现在生产环境的 eBPF 工具箱实战中我整理了 4 个最高频的脚本存在/usr/local/bin/下工具用途典型场景bpftrace-connect-latency.bt连接延迟归因DNS 解析慢、Redis 握手慢bpftrace-tcp-retrans.btTCP 重传统计网络抖动、防火墙丢包bpftrace-conntrack-usage.btconntrack 表监控K8s 节点丢包、短连接风暴bpftrace-softirq.bt软中断 CPU 占用网卡多队列不均、单机 PPS 打满每个 30 行内可以直接抄。完整代码我放在 GitHub Gist 上了。写在最后eBPF 不是取代 tcpdump而是把抓包升级成观测。tcpdump 告诉你包是什么eBPF 告诉你包为什么这样、是谁发的、卡在哪一步。生产环境排障时间是命。eBPF 把 1 小时的活压到 5 分钟对我这种凌晨 3 点被叫醒的人来说是救命稻草。如果你之前没用过 eBPF建议从bpftrace入手——语法像 awk5 分钟就能写出第一个有用的脚本。装好后第一件事跑下我上面那个connect_latency.bt看看你的服务里有没有被忽视的连接慢点。—— 凌晨 4 点补完 eBPF 工具箱的运维人