不能直接用 tcp_connect 和 tcp_close 计时,因内核中连接建立/关闭可能跨 CPU、延迟或重传,导致 kprobe 漏事件或配对错误;应跟踪 socket 状态变更,用 tracepoint:sock:inet_sock_set_state 监测 TCP_ESTABLISHED 到 TCP_CLOSE 的跨度,配合 per-CPU map 与 Go 侧批量聚合分析。

为什么不能直接用 tcp_connect 和 tcp_close 计时
因为 TCP 连接建立(SYN → SYN-ACK → ACK)和关闭(FIN/FIN-ACK 交换)在内核中可能跨多个 CPU、被延迟或重传干扰,单纯靠 kprobe 拦截 tcp_v4_connect 和 tcp_close 容易漏事件或配对错误。更可靠的方式是跟踪每个 socket 的生命周期:从 inet_sock_set_state 状态变更入手,只关注 TCP_ESTABLISHED 到 TCP_CLOSE 的跨度。
用 tracepoint:sock:inet_sock_set_state 抓状态跳变
这个 tracepoint 在每次 socket 状态变更时触发,参数包含 sk(socket 地址)、oldstate、newstate,且稳定存在于 5.0+ 内核,比 kprobe 更轻量、无符号解析风险。
实操建议:
- 在 eBPF 程序里用
struct sock *作为 map key,配合bpf_ktime_get_ns()记录TCP_ESTABLISHED时间戳 - 当
newstate == TCP_CLOSE时,查 map 取出起始时间,算差值后存入直方图 map(如BPF_MAP_TYPE_HISTOGRAM) - 务必用
bpf_map_delete_elem()清理已关闭的 key,否则 map 膨胀;注意并发——同一 socket 可能被多个 CPU 同时写,需用 per-CPU map 或加锁(实际推荐前者)
Go 用户态如何读取连接时长直方图
eBPF 程序本身不输出原始数据,需通过 Go 调用 libbpfgo 或 gobpf 加载并轮询 map。关键点不在“怎么读”,而在“读什么”:
立即学习“go语言免费学习笔记(深入)”;
推荐结构:
- 用
BPF_MAP_TYPE_PERCPU_HASH存储每个 CPU 的局部计数,避免原子冲突;key 是uint64(按纳秒分桶,如 1ms~2ms、2ms~4ms…),value 是uint64计数 - Go 中调用
Map.LookupAndDeleteBatch()(libbpfgo 支持)一次性拉取全量桶,再合并各 CPU 值 - 别直接用
Map.Lookup()单查——per-CPU map 的单 key 查询只返回当前 CPU 副本,结果不准
容易被忽略的边界:TIME_WAIT 和连接复用
TCP_CLOSE 状态在主动关闭端出现得早,但被动端可能还在 TCP_TIME_WAIT;如果业务大量短连 + keepalive,一个 socket 可能被 close() 后立即 connect() 复用,导致 map key 冲突。
应对方式:
- 过滤掉
oldstate == TCP_TIME_WAIT的 entry,只统计真正“终结”的连接 - 在用户态加简单去重逻辑:对同一
sk地址,只接受首次TCP_ESTABLISHED→TCP_CLOSE序列,后续忽略(eBPF 层难做,放 Go 侧更可控) - 若需区分客户端/服务端,可在 tracepoint 中提取
sk->__sk_common.skc_daddr和skc_dport,但注意大小端和内核版本字段偏移差异(5.10+ 推荐用bpf_probe_read_kernel()安全读)
inet_sock_set_state 在连接重传失败时根本不会走到 TCP_ESTABLISHED 的情况。


















