eBPF 零侵入可观测性:无需改代码也能拿网络内核数据

发布时间:2026/7/16 17:20:49

eBPF 零侵入可观测性:无需改代码也能拿网络内核数据 eBPF 零侵入可观测性无需改代码也能拿网络内核数据一、你想知道 Pod 之间的真实延迟但不想在每个服务里加埋点代码传统可观测性三板斧都有侵入性日志需要log.Info()格式要统一Metrics需要引入 Prometheus client定义指标Tracing需要在请求链路里透传 trace-id每个中间件都要加改代码意味着改了几十个服务、发版、灰度、验证。如果一个服务是第三方镜像或者是别人团队维护的你根本改不了。eBPF 的思路我不改你的代码我在内核里挂个钩子你的网络包、系统调用、CPU 采样我全能看到。对应用进程透明。二、eBPF 可观测性的内核级架构eBPF 钩子分类钩子类型挂载点能抓到什么典型应用kprobe/kretprobe内核函数入口/出口系统调用参数、返回值文件 I/O 监控tracepoint内核静态追踪点网络、调度、内存事件TCP 重传监控uprobe/uretprobe用户态函数Go/Python 函数调用慢查询定位XDP网卡驱动层最原始的包数据DDoS 防护TC (Traffic Control)内核协议栈进出流量负载均衡socket filtersocket 层套接字数据连接级监控perf eventCPU PMUCPU 周期、缓存命中性能分析火焰图三、实际 eBPF 程序示例追踪 HTTP 请求延迟基于内核 kprobe// http_trace.bpf.c #include linux/bpf.h #include bpf/bpf_helpers.h #include bpf/bpf_tracing.h #include bpf/bpf_endian.h // 事件结构传给用户态的数据 struct http_event { __u64 timestamp_ns; // 请求开始时间 __u64 duration_ns; // 请求耗时 __u32 pid; // 进程 ID __u32 tid; // 线程 ID __u32 status_code; // HTTP 状态码 __u32 data_len; // 响应体长度 char method[8]; // GET/POST char path[128]; // 请求路径 char comm[16]; // 进程名 }; // BPF Map内核态→用户态环形缓冲区 struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 256 * 1024); } events SEC(.maps); // BPF Map记录请求开始时间keygoroutine_id struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 10240); __type(key, __u64); // goroutine ptr __type(value, __u64); // start timestamp } start_time SEC(.maps); // kprobe挂载在 tcp_sendmsgGo 写 HTTP 响应最终调用这里 SEC(kprobe/tcp_sendmsg) int trace_tcp_sendmsg(struct pt_regs *ctx) { __u64 pid_tgid bpf_get_current_pid_tgid(); __u32 pid pid_tgid 32; __u64 goid get_goroutine_id(ctx); // 从 Go runtime 获取 // 只追踪目标进程 if (pid ! TARGET_PID) return 0; __u64 ts bpf_ktime_get_ns(); bpf_map_update_elem(start_time, goid, ts, BPF_ANY); return 0; } // kretprobetcp_sendmsg 返回时计算延迟 SEC(kretprobe/tcp_sendmsg) int trace_ret_tcp_sendmsg(struct pt_regs *ctx) { __u64 goid get_goroutine_id(ctx); __u64 *start_ts bpf_map_lookup_elem(start_time, goid); if (!start_ts) return 0; __u64 end_ts bpf_ktime_get_ns(); __u64 duration end_ts - *start_ts; bpf_map_delete_elem(start_time, goid); // 写入环形缓冲区给用户态读取 struct http_event *event; event bpf_ringbuf_reserve(events, sizeof(*event), 0); if (!event) return 0; event-timestamp_ns *start_ts; event-duration_ns duration; event-pid bpf_get_current_pid_tgid() 32; bpf_get_current_comm(event-comm, sizeof(event-comm)); bpf_ringbuf_submit(event, 0); return 0; } char LICENSE[] SEC(license) GPL;用户态 Go 程序读取 eBPF 事件并导出指标// main.go - 使用 cilium/ebpf 库 package main import ( bytes encoding/binary log net/http github.com/cilium/ebpf github.com/cilium/ebpf/link github.com/cilium/ebpf/ringbuf github.com/cilium/ebpf/rlimit github.com/prometheus/client_golang/prometheus github.com/prometheus/client_golang/prometheus/promhttp ) // HTTPEvent 对应 eBPF 中的 struct http_event type HTTPEvent struct { TimestampNs uint64 DurationNs uint64 PID uint32 TID uint32 StatusCode uint32 DataLen uint32 Method [8]byte Path [128]byte Comm [16]byte } var ( httpRequestDuration prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: ebpf_http_request_duration_seconds, Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5}, }, []string{method, path, comm}, ) ) func main() { prometheus.MustRegister(httpRequestDuration) // 移除内存限制加载 eBPF 需要 if err : rlimit.RemoveMemlock(); err ! nil { log.Fatal(err) } // 加载编译好的 eBPF 字节码 objs : bpfObjects{} if err : loadBpfObjects(objs, nil); err ! nil { log.Fatal(loading eBPF objects:, err) } defer objs.Close() // 挂载 kprobe kp, err : link.Kprobe(tcp_sendmsg, objs.TraceTcpSendmsg, nil) if err ! nil { log.Fatal(attaching kprobe:, err) } defer kp.Close() krp, err : link.Kretprobe(tcp_sendmsg, objs.TraceRetTcpSendmsg, nil) if err ! nil { log.Fatal(attaching kretprobe:, err) } defer krp.Close() // 从 ring buffer 读取事件 rd, err : ringbuf.NewReader(objs.Events) if err ! nil { log.Fatal(creating ringbuf reader:, err) } defer rd.Close() log.Println(eBPF HTTP tracer started) // 事件处理循环 go func() { var event HTTPEvent for { record, err : rd.Read() if err ! nil { log.Printf(reading from ringbuf: %v, err) continue } if err : binary.Read( bytes.NewBuffer(record.RawSample), binary.LittleEndian, event, ); err ! nil { continue } method : cstring(event.Method[:]) path : cstring(event.Path[:]) comm : cstring(event.Comm[:]) duration : float64(event.DurationNs) / 1e9 httpRequestDuration.WithLabelValues( method, path, comm, ).Observe(duration) } }() // 暴露 Prometheus 指标 http.Handle(/metrics, promhttp.Handler()) log.Fatal(http.ListenAndServe(:2112, nil)) } func cstring(b []byte) string { if i : bytes.IndexByte(b, 0); i ! -1 { return string(b[:i]) } return string(b) }四、边界分析与架构权衡边界问题方案内核版本要求eBPF 需要 Linux 4.x检查内核版本降级使用 perf_eventGo 函数参数获取Go ABI 不标准使用 uretprobe 寄存器分析eBPF 程序大小限制最大 1M 指令5.2拆分多个 eBPF 程序HTTPS 流量无法看到加密内容在应用层用 uprobe 抓取容器网络命名空间eBPF 挂载需要 PID namespace在宿主机上挂载 cgroup/skb 级别Map 大小限制Ring buffer 满了丢数据加大 buffer采样降级Verifier 拒绝复杂逻辑可能通不过简化循环使用 bounded loops权衡eBPF vs 传统 APMeBPF 优势零代码改动最大的卖点覆盖所有应用包括第三方性能开销极低 1% CPU可以看到内核级数据TCP 重传、网络延迟等eBPF 劣势调试困难eBPF 程序在内核里崩了很难排查缺乏业务上下文只知道系统调用不知道业务含义语言绑定不完整不同语言的函数调用约定不同HTTPS 流量盲区需要应用层配合解密推荐组合eBPF 做网络/系统层监控 OpenTelemetry 做应用层 Tracing。一个看底层一个看业务互为补充。五、总结eBPF 零侵入可观测性不是银弹但它在不需要改代码这个维度上确实碾压传统方案。适合的场景已有服务不想动代码用 eBPF 快速建立可观测性第三方/开源组件你控制不了它的代码网络/存储问题排查需要内核级数据的场景安全审计记录所有系统调用和行为常用的 eBPF 可观测性工具Cilium Hubble网络、PixieK8s 全栈、Falco安全、bpftrace调试。一句话总结如果你还只在应用层加埋点你已经漏掉了 50% 的可用信息。

相关新闻