pg_stat_backtrace:让 stack 抓取更 easy
老杨用 AI 做了一个玩具,可以随时随地通过 SQL 打印 PG 进程堆栈或记录至日志。分享给大家。
背景介绍
先看下这几个场景:
- 这个后端
state=active、wait_event=NULL、CPU 100% 跑了 30 分钟 — 它到底在干嘛? - 逻辑复制 apply worker 不再推进 — 是卡在
apply_handle_insert、触发器、还是heap_update? - autovacuum worker 在某张表上跑了 6 小时 — heap 扫描、索引清理、还是表截断?
- WAL sender 不发包 — 卡在
pq_putmessage、WalSndLoop、还是XLogRead? - checkpointer 慢 —
BufferSync、mdsync、还是别的地方?
我相信最简单的办法肯定是观察等待事件,抓取执行堆栈来分析。抓取 stack,基本一看函数调用链路就能知道进程目前的逻辑和行为。
特别是我们当前做 DBA agent,分析一些性能问题或者"进程死锁"等问题时 stack 这个信息源显得尤为重要,有了 stack 后 agent 就可以结合代码精准分析。
但是,不是所有人有权限登录机器来抓取 stack,并且只能临时记录到屏显无持久化日志。当前 PG 自身仅支持 error 类型的 backtrace 记录到日志,且需要事先配置 GUC backtrace_functions。
因此我想写一个扩展,可以随时通过 SQL 查看 PG 进程 stack,或者记录 stack 到 serverlog。
需求与要求
依然是奴役压榨 AI,以下是我给 AI 的需求和要求:
需求
生成一个扩展 pg_stat_backtrace,实现两个函数:
pg_get_backtrace(pid)— 查看进程 stackpg_log_backtrace(pid)— 打印 stack 到 serverlog
要求
- 切记你是一个顶级 PostgreSQL 内核专家
- 一切功能在扩展中实现,不要污染主干代码
- 不要使用 hook,shared memory,signal handle 的方案,不要影响其他正常功能
- 要做 pid 入参对账和权限管理。pid 一定要是本实例的,superuser 可打印所有 backend 及辅助进程 stack,普通用户仅可以打印自身或同 member 内进程 stack
- 代码和注释完全符合 PostgreSQL 风格
- 不存在性能隐患、安全隐患、稳定性隐患、内存泄露、非法内存访问、进程死锁等,不和其他插件冲突
- 一定遵循事实,不要存在事实错误,表述不准确的情况
- 进行回归、TAP、压测、可靠性测试等各种场景验证确保扩展高质量
- 绝对满足官方 contrib 要求
- 适配 PG14 - 19 版本
设置定时任务,AI 自行多轮验证
- 请全面检查下插件功能是否已按照需求实现
- 代码和注释是否符合 PostgreSQL 代码风格
- 是否还存在风险,重点排查性能隐患,安全隐患,稳定性隐患,内存泄露,非法内存访问,进程死锁,和其他插件冲突等等
- 是否还存在事实错误,表述不准确等
- 在 PG14 - 19 版本编译,并进行回归、TAP、压测可靠性测试等各种场景验证
- 是否可提交至官方 contrib
方案效果
最终 AI 采用了 ptrace + libunwind 的方案实现了扩展,经过 N 轮验证基本稳定可靠。
测试报告
一、PG14-19 全矩阵编译
| 版本 | 编译标志 | 警告 | .so 哈希 |
|---|---|---|---|
| pg14 | -Wall -Wextra -Wshadow -Wstrict-prototypes -Werror | 0 | a438594b |
| pg15 | 同上 | 0 | 6000d0ed |
| pg16 | 同上 | 0 | 85b21464 |
| pg17 | 同上 | 0 | a68ab5df |
| pg18 | 同上 | 0 | 3ec14d7f |
| pg19 | 同上 | 0 | 0f14931f |
6 份 .so 哈希互不相同,证明独立编译。
二、回归 + TAP 测试
| 版本 | regress | TAP(5 文件 / 87 子测试) | trace 字节数 |
|---|---|---|---|
| pg14 | ✅ All 1 passed | ✅ 87/87 PASS | 784 |
| pg15 | ✅ | ✅ 87/87 | 783 |
| pg16 | ✅ | ✅ 87/87 | 792 |
| pg17 | ✅ | ✅ 87/87 | 893 |
| pg18 | ✅ | ✅ 87/87 | 892 |
| pg19 | ✅ | ✅ 87/87 | 892 |
总计 522 个 TAP 断言 + 6 个回归套件全部通过。 trace 字节数随 PG 版本调用栈深度变化,是预期。
三、内存检查
| 测试结果 | 详情 |
|---|---|
| target 端连续 25000 次抓栈 RSS 增长 | 80 kB(一个 page,glibc arena 抖动) |
| caller 端 18000 次抓栈 | 12000 次后 0 kB 增长,libunwind 缓存预热后稳定 |
| valgrind --leak-check=full(10 次抓栈) | 0 个泄漏来自插件,1 个 definitely lost 来自 PG 自身 save_ps_display_args(与插件无关) |
四、压测可靠性测试
| 场景 | 结果 |
|---|---|
| 单机串行 10000 次(含冷启动) | 平均 2.682 ms/call |
| 单线程 100 次(含 pg_stat_statements) | 3.04 ms/call |
| 8 路并发 × 200 = 1600 次抓栈 | 0 失败,120 s 完成 |
五、异常路径验证
| 场景 | 行为 |
|---|---|
statement_timeout=100 |
超时正确 ERROR、ptrace 已 detach |
caller 中途 pg_cancel_backend |
正确 ERROR、target 仍 active 不受影响 |
| cancel 后再次抓栈同一 target | ✅ 正常返回 892 字节 trace |
六、边界值(10 个用例全部正确)
| 输入 | 行为 |
|---|---|
| NULL | NULL ✅ |
| 0 / -1 / INT32_MIN | WARNING “invalid PID” + NULL ✅ |
| INT32_MAX / 999999999 | WARNING “not a PostgreSQL server process” + NULL ✅ |
| 自身 PID | ERROR “cannot ptrace self” + hint ✅ |
| postmaster PID | ERROR “cannot ptrace the postmaster” + hint ✅ |
| 普通用户 → 超级用户 backend | ERROR “permission denied to target backend” + DETAIL ✅ |
七、代码风格
| 维度 | 结果 |
|---|---|
| 制表符缩进 | 604 行 tab,0 行空格缩进 |
| 行尾空白 | 0 |
| 行长度 ≤ 100 | 100% 符合 PostgreSQL |
| 危险函数 | 0 处 strcpy/strcat/sprintf/gets |
| ereport / errcode | 13 ERROR 级别 100% 带 errcode;2 WARNING 不带(与 postgres_fdw 等官方扩展做法一致,41% 官方 contrib 也如此) |
| PG_TRY/CATCH/END_TRY | 2/2/2 完美平衡 |
| #include 顺序 | postgres.h 第一,符合规范 |
八、ABI 兼容性 / 插件冲突
| 风险点 | 状态 |
|---|---|
_PG_init |
不存在任何 hook 注册 |
| GUC(DefineCustom*) | 0 个 |
| 共享内存(RequestAddinShmemSpace) | 0 个 |
| PG_MODULE_MAGIC/_EXT | 通过 #ifdef 兼容 PG14-19 |
| ProcArrayLock 用法 | LWLockAcquire(LW_SHARED) / LWLockRelease 微秒级,全版本稳定 API |
| BackendPidGetProcWithLock | PG14-19 全版本兼容,未触碰 PG18 ProcNumber 改造影响面 |
结论:本插件结构上不可能与任何其他扩展冲突 —— 没有全局状态、没有 hook、没有共享内存。
结果验证
pg_get_backtrace:执行 SQL 实时获取 stack
postgres=# select *, pg_get_backtrace(pid)
from pg_stat_activity
where backend_type in ('client backend','checkpointer','background writer')
and pid <> pg_backend_pid();
输出示例(client backend - idle 状态):
-[ RECORD 1 ]----+-------------------------------------------------------------
datid | 5
datname | postgres
pid | 3133583
state | idle
wait_event_type | Client
wait_event | ClientRead
backend_type | client backend
pg_get_backtrace | #0 0x00007f48d004ff57 in epoll_wait+0x17
| #1 0x000000000091416b in WaitEventSetWait+0x12b
| #2 0x00000000007882e4 in secure_read+0xd4
| #3 0x00000000007930ce in pq_recvbuf.lto_priv.0+0x8e
| #4 0x00000000009319e5 in PostgresMain+0x14c5
| #5 0x0000000000932ec5 in BackendMain+0x45
| #6 0x000000000088434d in postmaster_child_launch+0x12d
| #7 0x000000000088f2ad in ServerLoop.lto_priv.0+0x52d
| #8 0x0000000000885d19 in PostmasterMain+0xce9
| #9 0x000000000052fa80 in main+0x1d0
| #10 0x00007f48cff612a0 in __libc_start_call_main+0x80
| #11 0x00007f48cff61359 in __libc_start_main+0x89
| #12 0x0000000000530075 in _start+0x25
输出示例(background writer):
-[ RECORD 2 ]----+-------------------------------------------------------------
pid | 373780
wait_event_type | Activity
wait_event | BgwriterHibernate
backend_type | background writer
pg_get_backtrace | #0 0x00007f48d004ff57 in epoll_wait+0x17
| #1 0x000000000091416b in WaitEventSetWait+0x12b
| #2 0x000000000090b6a5 in WaitLatch+0x75
| #3 0x000000000088310d in BackgroundWriterMain+0x2ed
| #4 0x000000000088434d in postmaster_child_launch+0x12d
| #5 0x000000000088d12f in StartChildProcess.lto_priv.0+0x2f
| #6 0x0000000000885dfa in PostmasterMain+0xdca
| #7 0x000000000052fa80 in main+0x1d0
| #8 0x00007f48cff612a0 in __libc_start_call_main+0x80
| #9 0x00007f48cff61359 in __libc_start_main+0x89
| #10 0x0000000000530075 in _start+0x25
输出示例(checkpointer):
-[ RECORD 3 ]----+-------------------------------------------------------------
pid | 373779
wait_event_type | Activity
wait_event | CheckpointerMain
backend_type | checkpointer
pg_get_backtrace | #0 0x00007f48d004ff57 in epoll_wait+0x17
| #1 0x000000000091416b in WaitEventSetWait+0x12b
| #2 0x000000000090b6a5 in WaitLatch+0x75
| #3 0x0000000000883d03 in CheckpointerMain+0x503
| #4 0x000000000088434d in postmaster_child_launch+0x12d
| #5 0x000000000088d12f in StartChildProcess.lto_priv.0+0x2f
| #6 0x0000000000885e10 in PostmasterMain+0xde0
| #7 0x000000000052fa80 in main+0x1d0
| #8 0x00007f48cff612a0 in __libc_start_call_main+0x80
| #9 0x00007f48cff61359 in __libc_start_main+0x89
| #10 0x0000000000530075 in _start+0x25
pg_log_backtrace:记录 stack 到 serverlog
postgres=# select pg_log_backtrace(373776);
serverlog 输出:
2026-05-12 00:26:06.671 CST [3139292] LOG: backtrace of PID 373776
2026-05-12 00:26:06.671 CST [3139292] DETAIL:
#0 0x00007f48d004ff57 in epoll_wait+0x17
#1 0x000000000091416b in WaitEventSetWait+0x12b
#2 0x000000000090b6a5 in WaitLatch+0x75
#3 0x00000000008ed787 in IoWorkerMain+0x4c7
#4 0x000000000088434d in postmaster_child_launch+0x12d
#5 0x000000000088d12f in StartChildProcess.lto_priv.0+0x2f
#6 0x000000000088d2fb in maybe_adjust_io_workers.lto_priv.0+0x8b
#7 0x0000000000885cd8 in PostmasterMain+0xca8
#8 0x000000000052fa80 in main+0x1d0
#9 0x00007f48cff612a0 in __libc_start_call_main+0x80
#10 0x00007f48cff61359 in __libc_start_main+0x89
#11 0x0000000000530075 in _start+0x25
更多详情
GitHub 仓库:https://github.com/Nickyoung0/pg_stat_backtrace.git




