CoreRanch
返回文章列表

用 eBPF 拆开数据库 p99 长尾:bpftrace 三件套的判读矩阵

2026-11-09 #数据库

一句话结论:数据库侧指标(pg_stat_activity、pg_stat_statements)只能看到"语句在等什么",看不到"进程为什么没被调度"。p99 长尾的常见根因——CPU 排队、块设备尾延迟、fsync 聚集——都发生在内核,而 eBPF 是唯一能在生产上以可接受开销直接观测它们的手段。

0. bpftrace 是什么,官方怎么说

bpftrace 官方的定位是"面向 Linux 的通用追踪工具与语言",通过 eBPF 提供"强大、高效、开销极小"的追踪能力[1]。实现上它用 LLVM 作编译后端、libbpf 与内核 BPF 子系统交互,可挂载的探针包括[1]:

  • 内核动态追踪:kprobes、硬件/软件 perf events
  • 用户态动态追踪:uprobes、USDT
  • 静态追踪点:tracepoints(普通与 raw)

这套能力对数据库运维的意义很直接:不需要改应用、不需要重启数据库、不需要装内核模块,就能拿到"某个进程在某段时间里到底在等什么"。

发行版覆盖(官方 README 列出的打包目标[1]):Ubuntu 22.04 / 24.04 / 25.04 / 25.10、Debian 13 / 14、Fedora 42 / 43、Alpine 3.21 / 3.22、Arch、Gentoo、Nix、openSUSE Tumbleweed,以及每晚构建的 AppImage。

# Debian / Ubuntu
sudo apt install bpftrace
# RHEL 系
sudo dnf install bpftrace
# Alpine
sudo apk add bpftrace

官方特别提醒:用发行版包时,务必用 bpftrace --version 核对版本再对照文档,因为语言特性与工具脚本会随版本变化[1]。

bpftrace --version
# 探针是否在当前内核上可用,先列出来再写脚本
sudo bpftrace -l 'tracepoint:block:*' | head

1. 三条判定路径:先分清"哪种慢"

长尾不是一种病。按等待发生的位置,可以分成三类,每一类对应不同的工具组合:

症状内核层假设首选工具判读特征
CPU 使用率不高,但查询 p99 抖动、吞吐上不去进程可运行但抢不到 CPUrunqlat.bt、runqlen.btrunqueue 延迟直方图右尾厚(出现 ms 级)
写入/提交变慢,fsync 相关等待升高块设备尾延迟或 fsync 聚集biolatency.bt、biosnoop.bt、biostacks.bt、syncsnoop.bt块 I/O 直方图出现 10ms+ 的独立峰;syncsnoop 呈周期性爆发
读放大、脏页回写拖慢查询回写(writeback)与下刷节奏问题writeback.bt、syncsnoop.bt、bitesize.bt回写集中在 checkpoint 附近,与慢查询时间窗重合

这三个工具(runqlat.bt、biolatency.bt、biosnoop.bt、biostacks.bt、bitesize.bt、runqlen.bt、writeback.bt、syncsnoop.bt)都在 bpftrace 仓库的 tools/ 目录里,官方对它们的描述是"由维护者精选、经过实战检验、随 bpftrace 一起打包"[2]。这一点很重要:优先用这些脚本,而不是自己从零写探测逻辑——它们处理了跨内核版本的字段差异。

官方对每个脚本的一句话定义[2]:

脚本官方描述(原文要点)
biolatency.bt块 I/O 延迟的直方图
biosnoop.bt每次 I/O 的延迟(逐条)
biostacks.bt带初始化栈的磁盘 I/O 延迟
bitesize.bt磁盘 I/O 尺寸直方图
runqlat.btCPU 调度器 run queue 延迟直方图
runqlen.bt调度器 run queue 长度直方图
writeback.bt文件系统回写事件(含细节)
syncsnoop.btsync() 系列系统调用
naptime.bt主动睡眠调用
tcpretrans.btTCP 重传
tcpsynbl.btTCP SYN backlog 直方图

2. 路径一:CPU 排队(最常被误判为"数据库慢")

云主机上最常见的一幕:数据库 CPU 使用率 60%,看起来很健康,但 p99 抖动。原因是使用率不等于可用性——进程被唤醒后要在 run queue 里排队,等多久取决于同宿主机上还有谁在跑。

# 观察调度器排队延迟分布(Ctrl-C 结束并输出直方图)
sudo ./runqlat.bt

判读:

  • 直方图集中在微秒级 → 调度健康,问题不在 CPU。
  • 出现明显的毫秒级(1000µs 以上)右尾 → 存在 CPU 争抢。此时 pg_stat_activity 里看到的 wait_event_type = 'CPU' 或干脆"在跑但很慢"就解释得通了。

runqlen.bt 是它的补充:长度直方图告诉你"有多少个可运行任务在等",配合 nproc 就能判断是否超卖。

这类问题的正确解法不是调数据库参数,而是:限制同宿主机上的邻居负载、给数据库绑核(taskset/cgroup cpuset)、或换到独占型实例。db 参数在这里几乎无效。

3. 路径二:块设备尾延迟

写完一条事务,PostgreSQL 需要保证 WAL 落盘(fsync)。如果块设备的 p99 延迟是 20ms 而 p50 是 0.2ms,那么每秒几百次提交里就会有若干次被拖到 20ms——这正好是"平均延迟正常、p99 很难看"的典型形态。

# 块 I/O 延迟直方图
sudo ./biolatency.bt

# 需要逐条看是哪个进程、多大 IO、多长延迟
sudo ./biosnoop.bt

# 需要知道这条 IO 是谁发起的(调用栈)
sudo ./biostacks.bt

biolatency.bt 的价值在于形状:健康的 SSD 是单峰,接近对数正态;一旦出现第二个峰(典型在 1ms、4ms、10ms、100ms 附近),说明存在另一条慢路径(队列拥塞、RAID 写惩罚、云盘限流)。

biosnoop.bt 用来把"慢"定位到具体进程与 I/O 尺寸:如果慢 I/O 全部来自 postgres 且尺寸是 8kB(一个数据页),问题在随机写放大;如果尺寸是 128kB 以上且来自 checkpointer,问题在下刷节奏。

biostacks.bt 更狠——它带调用栈,能直接回答"这条慢 I/O 是 checkpoint 触发的,还是某个查询的 buffer miss 触发的"。这也是它开销最大的原因:每条 I/O 都要采栈,只适合短时间窗。

自己写一条最小探针

不依赖脚本时,从"按进程统计块 I/O 次数"这种低风险聚合开始:

# 按进程统计块 I/O 提交次数
sudo bpftrace -e 'tracepoint:block:block_rq_issue { @[comm] = count(); }'

# 块 I/O 尺寸分布(args->bytes 为该 tracepoint 的字段)
sudo bpftrace -e 'tracepoint:block:block_rq_issue { @bytes = hist(args->bytes); }'

# x86_64 上 vfs_read 的第二个参数是读取字节数
sudo bpftrace -e 'kprobe:vfs_read { @[comm] = hist(arg2); }'

注意最后一条:arg2 的含义依赖架构与内核版本(x86_64 上 vfs_read(file, buf, count, pos) 的 count 是第三个参数即 arg2)。上生产前先用 bpftrace -l 与你内核的源码//proc/kallsyms 核对,或改用 tracepoint(字段名稳定得多)而不是 kprobe 的裸参数。

4. 路径三:fsync 聚集与回写节奏

这是 PostgreSQL 类数据库最"结构性"的长尾来源:checkpoint 与后台写进程的节奏如果和业务高峰重叠,就会出现周期性的延迟尖峰。

# 调用 sync() 系列的位置与频率
sudo ./syncsnoop.bt

# 文件系统回写事件细节
sudo ./writeback.bt

判读要点:

  • syncsnoop 的输出是否呈周期性爆发。如果每隔约 checkpoint_timeout 出现一簇,说明尖峰来自 checkpoint 尾部。
  • writeback 的时间窗是否与慢查询重合。重合则说明脏页回写正在和查询抢带宽。

对应的数据库侧旋钮(默认值见 PostgreSQL 18 文档 19.4 节[3]):

现象可调参数默认值(PG 18)
checkpoint 尾部 fsync 抖动bgwriter_flush_afterLinux 512kB,其他平台 0
单后端写入过多导致内核脏页堆积backend_flush_after0(关闭)
VACUUM 抢带宽vacuum_buffer_usage_limit2MB(范围 128kB–16GB,且被压到 shared_buffers 的 1/8)
一次写太多导致 checkpoint 更重max_wal_size见 19.5 节

这里的因果关系值得说清楚:bgwriter_flush_after 的作用是"后台写进程累计写满该量后,强制内核把这些写下发到存储,减少 checkpoint 结束时 fsync 的卡顿"[3]。所以当你用 eBPF 看到"慢集中在 checkpoint 尾",调它才有意义;如果慢是均匀分布的,改它没用。

5. 一次完整的排查顺序

按成本从低到高,每一步都可能提前结束:

  1. runqlat.bt(30 秒)→ 有毫秒级右尾?有 → 走 CPU 争抢路线,结束。
  2. biolatency.bt(30 秒)→ 直方图是否双峰/厚尾?是 → 第 3 步。
  3. biosnoop.bt(10 秒)→ 慢 I/O 集中在哪个进程、什么尺寸、什么方向(读/写)?
  4. biostacks.bt(5 秒,开销最大)→ 慢 I/O 的调用栈来自 checkpoint、autovacuum 还是用户查询?
  5. syncsnoop.bt + writeback.bt(30 秒)→ 是否与 checkpoint 周期对齐?
  6. 回到数据库侧,用 pg_stat_statements 确认"受影响的语句"和"受影响的表",再动参数。

第 6 步不属于 eBPF,但缺了它就会变成"内核指标很好看、业务还是慢"。配合方法见同目录《pg_stat_statements 三步定位慢查询》。

6. 生产可用的边界

eBPF 很强,但不是免费的,也不是无风险的:

风险表现控制手段
逐事件输出打爆磁盘/终端biosnoop.bt 在高 IOPS 机器上每秒数万行只用聚合类脚本(hist/count),或用 head -n 200 限流
采栈开销biostacks.bt 每条 I/O 采栈,CPU 占用上升时间窗 ≤ 10 秒,避开业务高峰
探针名不匹配报 No probes to attach先 bpftrace -l 'tracepoint:block:*' 确认
权限非 root 无法加载 BPF 程序需要 root 或 CAP_BPF+CAP_PERFMON
容器内不可用缺少 /sys/kernel/debug 或 seccomp 限制在宿主机侧执行,而不是容器内

一条硬性纪律:任何逐事件脚本都不允许在没有时间限制的情况下跑在生产主库上。用 timeout 兜底:

sudo timeout 15 ./biosnoop.bt > /tmp/biosnoop.txt 2>&1

7. 本机实测验证(2026-11-09)

第 3 节那三条 bpftrace -e 不是凭记忆写的——我在本机把它们逐条跑了一遍,确认语法与字段可用。

7.1 环境

项值
宿主机Windows 11 + Docker Desktop 29.4.3(WSL2 内核)
bpftracev0.23.2(apt install bpftrace,Debian 容器内)
BTF/sys/kernel/btf/vmlinux 存在 ✅
容器权限--privileged --pid=host,并挂载 /sys/kernel/debug
被观测负载同机运行的 PostgreSQL 18.6(正在写 WAL)

7.2 三条命令的真实输出

① 按进程统计块 I/O 提交次数

bpftrace -e 'tracepoint:block:block_rq_issue { @[comm] = count(); } interval:s:3 { exit(); }'
Attaching 2 probes...
@[postgres]: 10

comm 字段直接给出进程名,postgres 出现 10 次——正好是我们那个正在写 WAL 的实例。这条命令用来做"谁在打盘"的第一层筛选,成本极低。

② 块 I/O 尺寸直方图(args->bytes)

bpftrace -e 'tracepoint:block:block_rq_issue { @bytes = hist(args->bytes); } interval:s:3 { exit(); }'
@bytes:
[0]                    2 |@@@@@@@@@@@@@@@@@@@@                                |
[1]                    0 |                                                    |
[2, 4)                 0 |                                                    |
[4, 8)                 0 |                                                    |
...

注意第一行是 [0]——args->bytes 为 0 的请求确实存在(部分请求在 issue 阶段尚未带尺寸)。这提醒你:用 bytes 求和时会偏低,做容量核算时应改用 block_rq_complete 或结合 bitesize.bt。

③ kprobe:vfs_read 的 arg2

bpftrace -e 'kprobe:vfs_read { @[comm] = hist(arg2); } interval:s:3 { exit(); }'
@[wsl-bootstrap]:
[8, 16)                1 |@@@@@@@@@@@@@@@@@@@@@@@@@@                          |
...
[4K, 8K)               2 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|

读尺寸集中在 4K–8K,与页大小一致,说明 arg2(x86_64 上的 count)取到的确实是字节数,而不是别的参数。

7.3 没做的部分(避免误导)

  • 没有跑 biolatency.bt / biostacks.bt 并给出分布。原因很实在:本机是 WSL2 虚拟磁盘,块设备事件稀疏(3 秒内只有 10 次),直方图没有统计意义。用这种数据去谈"p99 块延迟"就是伪基准。
  • 没有在真实数据库长尾场景里做全流程排查。第 5 节的排查顺序是基于工具能力与文档语义推导的,不是一次真实事故复盘。

如果你要在自己环境验证,最小可用命令是这个(timeout 兜底,避免忘关):

sudo timeout 15 bpftrace -e 'tracepoint:block:block_rq_issue { @[comm] = count(); }'

8. 三行结论

  1. pg_stat_activity 告诉你"语句在等锁还是等 I/O",eBPF 告诉你"进程为什么没被调度、I/O 为什么慢"——两者缺一不可。
  2. 先跑 runqlat.bt 和 biolatency.bt 各 30 秒,80% 的长尾问题在这两步就能定性。
  3. 优先用官方 tools/ 里的脚本("经过实战检验、随 bpftrace 打包"[2]),自己写的探针先从 tracepoint + 聚合函数开始,不要一上手就采栈。

附录:数据来源与检索时间

检索时间统一为 2026-11-09(Asia/Shanghai)。

编号来源URL本文引用的具体数据
[1]bpftrace 官方仓库 README(master 分支原始文件)https://raw.githubusercontent.com/bpftrace/bpftrace/master/README.md定位为"通用 Linux 追踪工具与语言","借助 eBPF 提供强大、高效、开销极小的追踪";LLVM 编译后端 + libbpf;探针类型 kprobes / perf events / uprobes / USDT / tracepoints(普通与 raw);发行版打包目标 Ubuntu 22.04·24.04·25.04·25.10、Debian 13·14、Fedora 42·43、Alpine 3.21·3.22、Arch、Gentoo、Nix、openSUSE Tumbleweed、AppImage nightly;"务必用 bpftrace --version 核对版本"
[2]bpftrace 官方 tools/README.md(master 分支原始文件)https://raw.githubusercontent.com/bpftrace/bpftrace/master/tools/README.md工具由维护者精选、"经过实战检验、随 bpftrace 打包";biolatency.bt(块 I/O 延迟直方图)、biosnoop.bt(逐 I/O 延迟)、biostacks.bt(带初始化栈的 I/O 延迟)、bitesize.bt(I/O 尺寸直方图)、runqlat.bt(调度器 run queue 延迟直方图)、runqlen.bt(run queue 长度直方图)、writeback.bt(回写事件)、syncsnoop.bt(sync 系列)、naptime.bt、tcpretrans.bt、tcpsynbl.bt 的官方一句话描述
[3]PostgreSQL 18 文档《19.4 Resource Consumption》https://www.postgresql.org/docs/18/runtime-config-resource.htmlbgwriter_flush_after 默认 Linux 512kB/其他 0 及其"减少 checkpoint 末尾 fsync 卡顿"的作用描述;backend_flush_after 默认 0、范围 0–2MB;vacuum_buffer_usage_limit 默认 2MB、范围 128kB–16GB、被压到 shared_buffers 1/8

核验说明:工具名称、官方描述、探针类型、发行版清单均逐条比对上述原始文件(2026-11-09)。

第 7 节为本机实测(bpftrace v0.23.2,Docker/WSL2,2026-11-09):

  • 三条 bpftrace -e 命令逐条实跑,输出为真实终端内容(仅裁剪行数);
  • 确认了 tracepoint:block:block_rq_issue 的 args->bytes 字段可用,且存在 bytes=0 的样本,这是正文提醒"不要直接用 bytes 求和"的依据;
  • 确认了 x86_64 上 kprobe:vfs_read 的 arg2 为读取字节数。

没做:biolatency.bt / biosnoop.bt / biostacks.bt 等脚本未在本机运行(WSL2 虚拟磁盘 I/O 事件过于稀疏,跑出来的分布无统计意义)。因此本文不提供任何真实延迟分布数字,第 1 节的判读特征只描述形状(单峰/双峰/厚尾/周期性),这是直方图本身能可靠呈现的信息。