eBPF无法直接解析SQL,因其仅可观测read/write/sendto等底层系统调用,而Go数据库驱动常通过缓冲、连接池或零拷贝路径绕过可捕获的syscall,导致数据碎片化或缺失;应改用tcp_connect/tcp_sendmsg等稳定tracepoint,结合用户态协议解析实现可靠SQL追踪。

为什么直接 hook 数据库进程的系统调用不靠谱
eBPF 本身不能直接解析 SQL 或识别“数据库请求”,它看到的只是 read、write、sendto 这类底层系统调用。如果你在用户态用 libpq(PostgreSQL)或 mysqlclient,它们多数走 socket,但 Go 的 database/sql 默认使用纯 Go 驱动(如 github.com/lib/pq 或 github.com/go-sql-driver/mysql),这些驱动内部不一定会触发可被 eBPF 可靠捕获的 syscall —— 尤其是启用连接池、复用 net.Conn、或走 io.Copy + bufio 缓冲时,实际 write 可能被延迟合并,甚至绕过内核(如 AF_UNIX 或某些零拷贝路径)。
所以指望靠 tracepoint:syscalls:sys_enter_write 拦住所有 SQL 是不现实的,你大概率只看到零碎、错位、甚至完全缺失的数据包。
推荐路径:从 Go 程序的 TCP 连接生命周期切入
Go 应用连数据库几乎都走 TCP(包括本地 127.0.0.1:5432),而 eBPF 对 tcp_connect、tcp_sendmsg、tcp_recvmsg 的 tracepoint 支持稳定,且上下文里能拿到 sk(socket)、PID、目标 IP/端口,足够做关联分析。
- 用
tracepoint:tcp:tcp_connect捕获新连接建立,记录pid、saddr、daddr、dport和sk地址,存入 eBPF map(如bpf_map_type::BPF_MAP_TYPE_HASH) - 在
tracepoint:tcp:tcp_sendmsg中查该sk是否属于已知 DB 连接;若是,把iov[0].iov_base前 N 字节(比如 256)copy 到 perf event 输出(注意用bpf_probe_read_kernel安全读取) - Go 程序侧需确保未禁用
SOCK_STREAM的 Nagle 算法(即没设SetNoDelay(true)),否则小 SQL 包可能被合并,影响 payload 提取精度
示例关键逻辑(eBPF C):
立即学习“go语言免费学习笔记(深入)”;
在 Golang 中使用 samber/hot 进行内存缓存,支持 LRU、LFU、TinyLFU、W‑TinyLFU、S3FIFO、ARC、TwoQueue、SIEVE、FIFO 等淘汰算法,提供 TTL、缓存加载器及分片功能。
SEC("tracepoint/tcp/tcp_sendmsg")
int trace_tcp_sendmsg(struct trace_event_raw_tcp_sendmsg *ctx) {
struct sock *sk = ctx->sk;
u64 sk_ptr = (u64)sk;
struct db_conn_info *info = bpf_map_lookup_elem(&db_conns, &sk_ptr);
if (!info || info->dport != 5432) return 0;
char buf[256] = {};
bpf_probe_read_kernel(buf, sizeof(buf), (void *)ctx->iov[0].iov_base);
// 后续通过 perf_submit 发给用户态
}
Go 用户态程序怎么配合 eBPF 解析 SQL?
eBPF 只负责“截包”和“打标”,真正的协议解析必须放在用户态:Go 程序用 libbpf-go 加载 eBPF 程序后,从 perf event reader 拿到原始字节流,再按协议还原。PostgreSQL 使用 Frontend/Backend 协议(长度前缀 + 类型字节),MySQL 是 packet length + sequence id + payload,两者都**不能直接当 UTF-8 字符串打印**——你需要:
- 检查前 1~2 字节是否匹配协议 magic(如 PostgreSQL startup packet 以
0x00000004开头) - 跳过认证交互阶段(StartupMessage、AuthenticationOK),专注捕获
Query('Q')或Parse('P')消息 - 对 MySQL,过滤
COM_QUERY(0x03)或COM_STMT_PREPARE(0x16)命令码 - 避免把 TLS 握手数据(如 ClientHello)误判为 SQL —— 检查目标端口+payload 是否含明文 ASCII 字符(如
SELECT)可作简单启发式过滤
别试图在 eBPF 里做字符串匹配:bpf_strncmp 不可用,bpf_probe_read_kernel 后手动循环比较又易超指令数限制。
真正容易被忽略的点:Go runtime 的 goroutine 调度干扰
Go netpoller 会把多个 goroutine 的 socket I/O 复用到少数几个 OS 线程上,导致同一个 PID 下大量不同 DB 连接的 tcp_sendmsg 事件混在一起。eBPF map 里仅存 sk → pid 映射不够,你还得在用户态维护 sk → app_context(比如 DB URL、query timeout、trace ID)——这个映射只能靠 Go 主动上报(例如在 sql.Open 后调用一个 ebpf.RegisterConn(sk_fd) 的 syscall 辅助函数),或者用 uprobe hook net.(*conn).Write 获取更上层上下文。
后者更可靠但有代价:Go 1.20+ 的 net.Conn.Write 是内联函数,符号不稳定;建议改 hook internal/poll.(*FD).Write,它的 symbol 名在各版本中相对固定,且参数含 *FD 结构体指针,可通过偏移提取 fd 和所属 goroutine 标识。

















