不能只靠 Go 单独做 TCP 握手延迟分析,必须用 eBPF 捕获内核态 SYN/SYN-ACK 时间戳再由 Go 聚合计算;否则 net.DialTimeout 测得的是含 DNS、调度、上下文切换等干扰的总耗时,非真实握手 RTT。

直接结论:不能只靠 Go 单独做 TCP 握手延迟分析,必须用 eBPF 捕获内核态的 SYN/SYN-ACK 时间戳,再由 Go 用户态聚合计算;否则你测到的只是 net.DialTimeout 的总耗时,混着 DNS、调度、用户态开销,根本不是真实握手延迟。
为什么 net.DialTimeout 测不准 TCP 握手延迟
很多人用 net.DialTimeout 测“建连时间”,但这个值包含太多干扰:
- DNS 解析耗时(如果传的是域名而非 IP)
- Go runtime 调度延迟(goroutine 启动、网络轮询器唤醒)
- 用户态到内核态的上下文切换开销
- 三次握手完成后的第一次 write/read 准备时间(部分版本会算进去)
真正想看的——SYN 发出到 SYN-ACK 收到之间的时间差(即客户端视角的 handshake RTT),只有在内核协议栈收发包的精确位置打点才能拿到。eBPF 的 tracepoint:tcp:tcp_retransmit_skb 和 kprobe:tcp_v4_connect + kretprobe:tcp_v4_connect 是唯一直接路径。
如何用 eBPF 捕获 SYN 和 SYN-ACK 的时间戳
eBPF 程序需挂载两个钩子,读取 struct sock 和 skb 时间信息:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
- 在
tcp_v4_connect进入时(kprobe),记录sk->sk_write_seq和bpf_ktime_get_ns()作为 SYN 发送起点 - 在
tcp_rcv_state_process中匹配skb->tcp_flag & TCP_FLAG_SYN且skb->tcp_flag & TCP_FLAG_ACK,提取skb->tstamp或用bpf_ktime_get_ns()作为 SYN-ACK 收到时间 - 用连接五元组(
src_ip, src_port, dst_ip, dst_port, netns_id)作 map key,避免 NAT 下冲突 - 注意:Linux 5.10+ 才默认填充
skb->tstamp,旧内核必须用bpf_ktime_get_ns()替代,但会略偏高(含软中断延迟)
Go 用户态怎么安全接收并计算延迟
别用 PerfEventArray 直接读原始事件流——容易丢事件、解析错结构。推荐方案:
- 用
cilium/ebpf库加载 eBPF 程序,attach 到 tracepoint/kprobe - eBPF 侧把每个连接的
syn_ts和synack_ts写进一个BPF_MAP_TYPE_HASH(key 是五元组,value 是两个 uint64) - Go 用户态用定时器(如
time.Ticker)每 100ms 查一次 map,命中后计算差值、清空条目 - 避免用
Map.LookupAndDelete在高并发下引发锁竞争,改用Map.GetNextKey遍历 +Map.Delete分离操作 - 延迟值存为
uint64(纳秒),Go 里转成 float64 毫秒时用float64(ns) / 1e6,别用除法或time.Duration构造,防溢出
常见失败点和绕过方式
实际部署时,80% 的问题卡在加载或事件不触发:
-
Program load failed: invalid func call:说明用了不被校验器允许的 helper,比如bpf_trace_printk在生产环境应禁用,改用 ringbuf - eBPF 程序编译后没触发:检查是否漏了
attach,或目标函数符号名变了(如tcp_v4_connect在某些发行版叫__tcp_v4_connect) - map 查不到数据:确认内核开启了
CONFIG_BPF_SYSCALL=y和CONFIG_NET_CLS_BPF=m,运行cat /proc/sys/net/core/bpf_jit_enable必须是 1 - Go 侧权限不足:attach kprobe 需要
CAP_SYS_ADMIN,普通用户可用sudo setcap cap_sys_admin+ep ./your-binary授权,别直接 sudo 运行
最易被忽略的是:eBPF 捕获的 SYN-ACK 时间戳,和用户态 net.Conn 可写状态之间仍有毫秒级偏差——因为内核要走完整个 socket 状态机(SYN_RECV → ESTABLISHED)、通知等待队列、唤醒 goroutine。如果你要做 P99 握手延迟归因,这个偏差必须单独测量并减去。

















