eBPF 网络性能追踪实战:三次握手延迟异常的全链路定位复盘
eBPF 网络性能追踪实战三次握手延迟异常的全链路定位复盘一、异常的 300msTCP 握手为什么比 ping 慢了 10 倍一次线上服务迁移后新机房的 P50 连接建立延迟从旧机房的 8ms 飙升到 320ms。奇怪的是同机房内的 ping 延迟只有 0.3ms网络看起来没有问题。ssh 连接、scp 传文件也都正常。问题只在应用层的 TCP 连接建立上。传统的tcpdump抓包能确认握手确实发生了延迟但无法回答延迟发生在内核的哪个环节。TCP 握手涉及内核协议栈中至少 6 个关键函数tcp_v4_rcv → tcp_v4_do_rcv → tcp_rcv_state_process → tcp_v4_conn_request → tcp_rcv_synsent_state_process → tcp_finish_connect仅凭抓包无法定位是哪一个环节在磨蹭。eBPF 的 kprobe 能力可以在这个场景做到函数级的时间追踪。通过在内核 TCP 握手的关键函数上挂载 kprobe记录每个函数的进入和退出时间戳可以精确量化每个环节的耗时。二、eBPF kprobe 追踪方案的实现使用 BCCBPF Compiler Collection框架编写 eBPF 追踪程序在 TCP 握手的 6 个关键函数上挂载 kprobe 和 kretprobe#!/usr/bin/env python3 # tcp_handshake_trace.py —— eBPF 追踪 TCP 握手各阶段延迟 from bcc import BPF import time bpf_code #include uapi/linux/ptrace.h #include net/sock.h // 定义事件结构记录每个连接的握手各阶段时间戳 struct handshake_event { u64 pid; // 进程 ID u32 saddr; // 源 IP网络字节序 u32 daddr; // 目标 IP u16 sport; // 源端口 u16 dport; // 目标端口 u64 tcp_v4_rcv_enter; // tcp_v4_rcv 进入时间 u64 tcp_v4_rcv_exit; // tcp_v4_rcv 退出时间 u64 state_process_enter; // tcp_rcv_state_process 进入 u64 state_process_exit; // tcp_rcv_state_process 退出 u64 conn_request_enter; // tcp_v4_conn_request 进入 u64 finish_connect_enter; // tcp_finish_connect 进入 }; // BPF_PERF_OUTPUT将事件从内核态推送到用户态 BPF_PERF_OUTPUT(events); // BPF_HASH在内核态存储中间状态连接 → 时间戳映射 BPF_HASH(start_time, u64, struct handshake_event); // kprobe: 在 tcp_v4_rcv 函数入口处记录时间戳 int kprobe__tcp_v4_rcv(struct pt_regs *ctx, struct sk_buff *skb) { u64 pid_tgid bpf_get_current_pid_tgid(); struct handshake_event evt {}; evt.pid pid_tgid 32; evt.tcp_v4_rcv_enter bpf_ktime_get_ns(); // 纳秒级时间戳 // 从 sk_buff 中提取连接四元组 struct tcphdr *tcp skb-hdr.tcp; // 实际需要更复杂的偏移计算 struct iphdr *ip skb-hdr.ip; evt.saddr ip-saddr; evt.daddr ip-daddr; evt.sport tcp-source; evt.dport tcp-dest; start_time.update(pid_tgid, evt); return 0; } // kretprobe: 在 tcp_v4_rcv 函数返回处记录退出时间 int kretprobe__tcp_v4_rcv(struct pt_regs *ctx) { u64 pid_tgid bpf_get_current_pid_tgid(); struct handshake_event *evt start_time.lookup(pid_tgid); if (evt) { evt-tcp_v4_rcv_exit bpf_ktime_get_ns(); // 计算耗时并提交事件 u64 latency evt-tcp_v4_rcv_exit - evt-tcp_v4_rcv_enter; // 只上报延迟 1ms 的异常事件减少用户态处理压力 if (latency 1000000) { // 1ms 1,000,000ns events.perf_submit(ctx, evt, sizeof(*evt)); } start_time.delete(pid_tgid); } return 0; } b BPF(textbpf_code) def print_event(cpu, data, size): event b[events].event(data) latency_ms (event.tcp_v4_rcv_exit - event.tcp_v4_rcv_enter) / 1e6 print(fPID{event.pid} {event.saddr}-{event.daddr}:{event.dport} flatency{latency_ms:.2f}ms) # 注册事件回调打印延迟超过 1ms 的连接 b[events].open_perf_buffer(print_event) print(追踪 TCP 握手中... CtrlC 退出) while True: b.perf_buffer_poll()三、根因定位netfilter conntrack 表的锁竞争eBPF 追踪的结果揭示了延迟热点280ms 的延迟全部集中在tcp_v4_rcv函数内部。进一步缩小范围后发现函数内部的nf_conntrack_in调用连接跟踪表的查找是耗时元凶。连接跟踪表conntrack table默认使用全局自旋锁保护。在新机房场景下大量短连接每秒约 3 万个同时进行连接跟踪的创建和查找导致锁竞争白热化。旧机房未复现此问题是因为连接速率远低于新机房每秒约 3000 个锁竞争不明显。修复方案分为两步。第一步是直接关闭不需要的 conntrack确认系统没有使用 iptables NAT 规则后# 关闭连接跟踪 —— 仅适用于无 NAT 需求的纯路由场景 iptables -t raw -I PREROUTING -j NOTRACK iptables -t raw -I OUTPUT -j NOTRACK第二步是调整 conntrack 哈希表大小以减少锁竞争适用于无法关闭 conntrack 的场景# 扩大 conntrack 哈希桶数量 —— 减少 hash 冲突和锁竞争 # 默认值通常为 8192调整为 262144按最大连接数的 1/4 估算 echo 262144 /sys/module/nf_conntrack/parameters/hashsize # 增大最大连接跟踪条目数 echo 1048576 /proc/sys/net/netfilter/nf_conntrack_max四、排查工具对比方法定位精度对系统影响学习成本tcpdump抓包毫秒级端点级高磁盘 I/O低应用层日志打点毫秒级应用级低低ss -s查看队列状态级无延迟极低低eBPF kprobe纳秒级函数级极低高关闭 conntrack 前后的延迟对比指标优化前优化后改善TCP 握手 P50320ms1.2ms-99.6%TCP 握手 P99850ms3.5ms-99.6%tcp_v4_rcv耗时280ms0.08ms-99.97%连接建立成功率92%99.8%8.5%五、总结eBPF 在内核网络排障中的核心价值函数级精度是传统工具的盲区tcpdump 只能看到端点间的数据包交换无法知晓内核内部哪个函数在耗时。kprobe 填补了这个盲区纳秒级时间戳是找根因的基础280ms 的延迟分散在 tcp_v4_rcv 内部只有纳秒级采样才能区分 nf_conntrack_in 和其他子函数的耗时占比过滤低延迟事件减少数据洪流eBPF 程序中添加延迟阈值过滤只上报 1ms 的事件将用户态的数据处理量降低 2~3 个数量级适合生产环境的持续运行conntrack 是高频短连接场景的常见陷阱每秒 3 万级别的连接跟踪足以压垮默认配置的锁机制无 NAT 需求时直接关闭是最简方案。适用边界eBPF kprobe 方案要求内核版本 4.9BCC 支持且启用了 CONFIG_DEBUG_INFO_BTF。CentOS 7 等较老内核需要额外编译 BTF 信息。