ebpf日志采集更可靠,因其在内核态事件驱动拦截sys_write等系统调用,不依赖日志路径或应用配合,避免轮询漏事件、延迟高及hook稳定性差等问题。

为什么用 eBPF 做日志采集比轮询 / hook 更可靠
因为传统方式(如修改应用打日志、或用 inotify 监控文件)要么要改代码,要么漏事件、丢日志、有延迟。eBPF 在内核态拦截系统调用(比如 write、sys_write),只要进程往 stdout/stderr 写,就能捕获——不依赖日志路径、不关心格式、也不需要应用配合。
但注意:eBPF 不能直接读用户态内存里的日志字符串,必须靠 bpf_probe_read_user() 安全拷贝,且长度受限(通常 ≤ 128 字节,否则触发 verifier 拒绝加载)。
- 只适合采集「短日志行」(如 Go 的
log.Println()输出);长日志需截断或走 perf ring buffer 分段传 - Go 程序默认用
write()写 stdout,但启用GODEBUG=asyncpreemptoff=1或高并发时可能走writev(),需额外 attach - eBPF 程序必须用
GPL许可声明,否则bpf_probe_read_user()被禁用
如何用 libbpf-go 加载并 attach write() tracepoint
别碰 clang + bpftool 手动编译那一套——用 libbpf-go 可以内联加载,省去 ELF 解析和 map 映射的手动管理。
关键步骤:
- 在 eBPF C 代码里用
SEC("tracepoint/syscalls/sys_enter_write"),不是 kprobe;tracepoint 更稳定,不随内核符号变化失效 - Go 侧用
ebpf.LoadCollectionSpec()读取嵌入的 BPF bytecode(推荐用//go:embed打包) - attach 时指定
target.PID = 0表示全局监听,若只抓某 Go 进程,设为对应pid即可 - 务必检查
errno返回值:常见失败是EPERM(没 CAP_SYS_ADMIN)、EINVAL(verifier 拒绝,比如越界读)
示例片段(Go):
在 Go 中使用 google/wire 实现编译时依赖注入——wire.NewSet、wire.Build、wire.Bind(接口→实现)、wire.Struct、wire.Value、wire.Interface
spec, err := LoadCollectionSpec("bpf.o")
if err != nil { panic(err) }
coll, err := ebpf.NewCollection(spec)
if err != nil { panic(err) }
prog := coll.Programs["tracepoint__syscalls__sys_enter_write"]
link, err := prog.Attach(&ebpf.ProgramAttachOptions{
Target: 0,
})
从 perf event ring buffer 读日志数据的正确姿势
eBPF 侧把日志内容写进 perf_event_array map,Go 侧要用 perf.NewReader() 持续消费——但直接 Read() 会阻塞,且容易丢事件。
- 必须设非阻塞模式:
perf.NewReader(perfMap, os.Getpagesize()*4)(buffer 太小会丢数据) - 每次
Read()返回的是perf.Record,其中Data是原始字节数组,需按你定义的结构体unsafe.Sizeof()解包 - Go 中解析结构体时,字段必须用
binary.Read()或unsafe.Slice()(Go 1.21+),不能直接*MyStruct强转——否则内存对齐失败,读出乱码 - 记得调用
record.Lost > 0判断是否丢事件;丢多了说明 perf buffer 不够大或 Go 消费太慢
eBPF 结构体示例:
struct log_event {
__u64 pid;
__u64 ts;
char msg[128];
};
Go 应用写日志时如何避免被误采或干扰
你的采集程序本身也会调用 write()(比如打印 debug 日志),导致自循环采集。最简单办法是 PID 过滤。
- eBPF 侧加判断:
if (pid == bpf_get_current_pid_tgid() >> 32) return 0;(高 32 位是 tgid) - 不要过滤 UID/GID:容器环境 UID 常为 0,不可靠
- Go 日志库(如
zap)若启用了WriteSyncer并 flush 频繁,会产生大量小 write;建议在 eBPF 侧加时间窗口去重(比如同 PID 500ms 内只采第一条) - 避免在 eBPF 里做字符串匹配(如过滤 "DEBUG")—— verifier 不允许循环,且性能差;过滤逻辑放 Go 侧更灵活
真正难的是多线程 Go 程序:一个 goroutine 写 stdout,背后可能是多个内核线程(clone() 出的),pid 和 tgid 对不上。此时得结合 bpf_get_current_comm() 辅助识别进程名,再关联到 Go 的 runtime.GoroutineProfile() ——但这已超出纯 eBPF 范围,得两边协同。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










