当 PostgreSQL 慢在数据库之外:用 bpftrace 打通 SQL 与内核观测

本页目录

前言

排查 PostgreSQL 问题时,我们通常会先看这些东西:

大多数问题,到这里基本能有个方向。

但有些场景会比较尴尬:

这时候,bpftrace 就很有价值。

一句话概括:

bpftrace 可以帮助我们从操作系统和进程行为层面,看见 PostgreSQL 后端进程到底在做什么。

它不是替代 EXPLAIN、pg_stat_activity、pg_stat_statements 的工具,而是当数据库内部视角不够时,继续往下看系统调用、内核 tracepoint、用户态函数、调度、I/O、网络等行为。

bpftrace 是什么

bpftrace 可以理解为 Linux 上的“动态观测脚本工具”。

它基于 eBPF,可以在不改代码、不重启进程的情况下,临时观察内核、系统调用、用户态函数、调度、I/O、网络等行为。

官方对它的定位是:

bpftrace 是 Linux 的高级 tracing 语言,利用 eBPF 提供动态观测能力。

它的语法有点像 awk + C。你可以把一段小脚本挂到某个事件上,当事件发生时执行统计、打印、聚合等动作。

比如统计 PostgreSQL 进程退出 read() 系统调用的次数:

Shell
sudo bpftrace -e '
tracepoint:syscalls:sys_exit_read
/comm == "postgres"/
{
  @[pid] = count();
}'

这个例子很简单,但它说明了 bpftrace 的核心价值:

临时挂上去,观察一段时间,拿到证据,然后摘下来。

用完就走,不留感情包袱。

bpftrace 和 strace / perf / gdb 有什么区别?

工具 更适合做什么 特点
strace 看系统调用明细 直观,但输出量大,开销可能明显
perf CPU 采样、火焰图、热点函数 适合性能剖析,但脚本化表达能力不如 bpftrace
gdb 调试进程、断点、查看变量 强大但侵入性更高,线上慎用
bpftrace 临时观测、聚合统计、延迟分布、Top N 可在内核侧聚合,适合短时间定向分析

bpftrace 的一个优势是:很多数据可以直接在内核侧聚合,最后只把统计结果输出到用户态。

所以它很适合做:

bpftrace 能观察什么?

bpftrace 支持多类 probe:

Probe 类型 说明 PostgreSQL 场景
tracepoint 内核预定义稳定事件 系统调用、调度、块设备 I/O、网络
kprobe/kretprobe 内核函数入口/返回 深入观察内核路径
uprobe/uretprobe 用户态函数入口/返回 跟踪 PostgreSQL 内部函数
usdt 用户态静态探针 PostgreSQL Dynamic Tracing 探针
profile/interval 采样或定时事件 CPU 采样、周期性输出

PostgreSQL 自身也支持 Dynamic Tracing。官方文档中提到,PostgreSQL 可以在构建时开启 DTrace/USDT 相关探针,这样就能观察 query、transaction、lock、lwlock、buffer 等内部事件。

这意味着,如果 PostgreSQL 编译时带了相关探针,我们可以直接从 bpftrace 里观察数据库内部行为。

用 bpftrace 分析 PostgreSQL,能做什么?

1. 看 PostgreSQL 后端进程在做哪些系统调用

比如一个 backend 慢,到底是不是频繁 read()、write()、fsync()、openat()?

可以统计 PostgreSQL 进程的系统调用:

Shell
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_*
/comm == "postgres"/
{
  @[probe] = count();
}'

这类分析适合判断:

2. 分析 fsync / fdatasync 延迟

PostgreSQL 的事务提交、checkpoint、WAL flush 都可能涉及同步刷盘。

如果怀疑慢在存储层,可以看 fsync() 延迟分布:

Shell
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_fsync
/comm == "postgres"/
{
  @start[tid] = nsecs;
}

tracepoint:syscalls:sys_exit_fsync
/@start[tid]/
{
  @fsync_latency_us = hist((nsecs - @start[tid]) / 1000);
  delete(@start[tid]);
}'

它可以回答一个很关键的问题:

PostgreSQL 慢,是不是因为 fsync 延迟突然变高?

如果直方图里大量 fsync 落在几十毫秒甚至几百毫秒,就应该继续向存储、文件系统、云盘、RAID、虚拟化层排查。

3. 看 read/write I/O 延迟

例如观察 PostgreSQL 进程的 read() 延迟:

Shell
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_read
/comm == "postgres"/
{
  @start[tid] = nsecs;
}

tracepoint:syscalls:sys_exit_read
/@start[tid]/
{
  @read_latency_us = hist((nsecs - @start[tid]) / 1000);
  delete(@start[tid]);
}'

这可以用于判断:

4. 看 PostgreSQL 是否被调度出去

有时 SQL 慢不是数据库内部问题,而是 OS 调度、CPU 争抢、容器 cgroup 限制、虚拟化抖动导致 PostgreSQL 后端拿不到 CPU。

可以观察 postgres 进程的调度切换:

Shell
sudo bpftrace -e '
tracepoint:sched:sched_switch
/args->prev_comm == "postgres"/
{
  @[args->prev_state] = count();
}'

这类分析适合排查:

5. 看锁等待 / futex 等待

PostgreSQL 内部很多等待最终可能表现为 futex 等系统调用,尤其是 latch、进程间同步、锁等待相关场景。

可以粗略观察:

Shell
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_futex
/comm == "postgres"/
{
  @[pid, comm] = count();
}'

如果 futex 调用异常频繁,可以结合 PostgreSQL 侧继续看:

SQL
select pid, wait_event_type, wait_event, query
from pg_stat_activity
where wait_event is not null;

这时 bpftrace 负责从 OS 层证明“确实在等”,PostgreSQL 视图负责告诉你“等的是什么”。

6. 分析 TCP / 网络问题

对于 PostgreSQL 主从复制、客户端连接、连接池、Greenplum interconnect、YMatrix 分布式通信,网络问题经常不是 SQL 层能直接解释的。

bpftrace 可以观察:

比如统计 postgres 的 sendto():

Shell
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_sendto
/comm == "postgres"/
{
  @[pid] = count();
}'

对于 Greenplum/YMatrix 这类分布式数据库,还可以结合 segment 进程观察是否某些 segment 网络发送异常频繁,或者某类进程出现网络阻塞。

7. 分析用户态 PostgreSQL 函数

如果 PostgreSQL 二进制没有 strip,或者有符号信息,可以用 uprobe 跟踪 PostgreSQL 内部函数。

例如查看某个函数是否被频繁调用:

Shell
sudo bpftrace -e '
uprobe:/usr/pgsql-16/bin/postgres:ExecProcNode
{
  @[comm] = count();
}'

也可以观察函数耗时:

Shell
sudo bpftrace -e '
uprobe:/usr/pgsql-16/bin/postgres:some_function
{
  @start[tid] = nsecs;
}

uretprobe:/usr/pgsql-16/bin/postgres:some_function
/@start[tid]/
{
  @latency_us = hist((nsecs - @start[tid]) / 1000);
  delete(@start[tid]);
}'

这里的难点是:你需要知道 PostgreSQL 内部函数名,并且二进制要保留符号。

如果 PostgreSQL 被 strip,或者没有 debuginfo,uprobe 能看到的东西就会少很多。这种情况下,可能更适合用 perf,或者在带 debuginfo/debug build 的环境里分析。

PostgreSQL 排障中,bpftrace 应该放在哪一层?

我会把 bpftrace 放在这个位置:

TEXT
SQL 层现象
  ↓
pg_stat_activity / pg_stat_statements / wait_event
  ↓
确认是 CPU、I/O、锁、网络、调度、文件系统哪一类问题
  ↓
bpftrace 从 OS / 内核 / 进程行为层面验证
  ↓
再决定是调 SQL、调参数、调存储、调网络、调 OS,还是改架构

举几个典型问题。

问题 1:SQL 偶发慢,但执行计划没变

可以看:

问题 2:commit 延迟突然变高

重点看:

问题 3:CPU 很高,但不知道 PostgreSQL 在忙什么

可以用:

问题 4:数据库看起来空闲,但查询还是慢

可以查:

一个实用的排查链路

当 PostgreSQL 慢时,不要一上来就改参数。

可以按这个链路看:

TEXT
1. SQL 层
   pg_stat_activity / wait_event / pg_stat_statements

2. 进程层
   top / pidstat / perf / bpftrace

3. 系统调用层
   read / write / fsync / futex / sendto / recvfrom

4. 内核层
   block I/O / scheduler / TCP / page cache

5. 根因定位
   SQL、锁、存储、网络、CPU 调度、内存、cgroup、文件系统

bpftrace 主要补的是第 2 到第 4 层。

它不是告诉你 SQL 应该怎么改,而是告诉你 PostgreSQL 后端进程在操作系统层面到底发生了什么。

使用 bpftrace 的注意事项

第一,bpftrace 通常需要 root 权限,或者需要具备 BPF 相关 capability。

第二,生产环境使用时一定要控制范围,尽量加过滤条件:

TEXT
/comm == "postgres"/
/pid == 12345/

不要一上来全系统无差别抓取。

第三,优先使用稳定探针。tracepoint 通常比 kprobe 更稳定,因为 kprobe 依赖内核函数名,跨内核版本可能变化。

第四,用户态函数分析依赖符号信息。PostgreSQL 如果被 strip,或者没有 debuginfo,uprobe 能看到的东西会少很多。

第五,少在高频路径里疯狂 printf()。更推荐用 count()、sum()、hist() 做聚合,最后统一输出结果。

第六,bpftrace 适合“短时间、针对性、统计型”的线上观测,不适合长期替代 Prometheus、pg_stat_statements、日志系统这类常规监控。

总结

对 PostgreSQL 来说,bpftrace 不是替代 EXPLAIN、pg_stat_activity、pg_stat_statements 的工具,而是它们的补充。

它最适合回答这类问题:

PostgreSQL 现在到底是在 CPU 上跑,在等磁盘,在等锁,在等网络,在被调度器切走,还是卡在某个内核路径里?

所以,在数据库专家手里,bpftrace 的价值不是“会写几个命令”,而是把数据库现象和操作系统行为打通:

  1. 从 SQL 慢,追到 backend 进程;
  2. 从 backend 进程,追到系统调用;
  3. 从系统调用,追到 I/O、调度、网络和内核路径。

这才是 bpftrace 在 PostgreSQL 排障里的真正价值。

参考资料

  1. bpftrace 官方网站
  2. bpftrace GitHub
  3. bpftrace About
  4. PostgreSQL Dynamic Tracing 文档

讨论与补充

在 GitHub 查看讨论

支持作者

感谢支持 PostgreSQL Howto 的长期维护。

微信赞赏码

读者交流群

二维码若已过期,可通过公众号 PostgreSQL 学徒或邮件联系。

PostgreSQL 学徒读者交流群二维码