暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

pg_stat_backtrace:让stack抓取更easy

原创 NickYoung 2026-05-19
142

pg_stat_backtrace:让 stack 抓取更 easy

老杨用 AI 做了一个玩具,可以随时随地通过 SQL 打印 PG 进程堆栈或记录至日志。分享给大家。

背景介绍

先看下这几个场景:

  • 这个后端 state=activewait_event=NULL、CPU 100% 跑了 30 分钟 — 它到底在干嘛?
  • 逻辑复制 apply worker 不再推进 — 是卡在 apply_handle_insert、触发器、还是 heap_update
  • autovacuum worker 在某张表上跑了 6 小时 — heap 扫描、索引清理、还是表截断?
  • WAL sender 不发包 — 卡在 pq_putmessageWalSndLoop、还是 XLogRead
  • checkpointer 慢 — BufferSyncmdsync、还是别的地方?

我相信最简单的办法肯定是观察等待事件,抓取执行堆栈来分析。抓取 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) — 查看进程 stack
  • pg_log_backtrace(pid) — 打印 stack 到 serverlog

要求

  1. 切记你是一个顶级 PostgreSQL 内核专家
  2. 一切功能在扩展中实现,不要污染主干代码
  3. 不要使用 hook,shared memory,signal handle 的方案,不要影响其他正常功能
  4. 要做 pid 入参对账和权限管理。pid 一定要是本实例的,superuser 可打印所有 backend 及辅助进程 stack,普通用户仅可以打印自身或同 member 内进程 stack
  5. 代码和注释完全符合 PostgreSQL 风格
  6. 不存在性能隐患、安全隐患、稳定性隐患、内存泄露、非法内存访问、进程死锁等,不和其他插件冲突
  7. 一定遵循事实,不要存在事实错误,表述不准确的情况
  8. 进行回归、TAP、压测、可靠性测试等各种场景验证确保扩展高质量
  9. 绝对满足官方 contrib 要求
  10. 适配 PG14 - 19 版本

设置定时任务,AI 自行多轮验证

  1. 请全面检查下插件功能是否已按照需求实现
  2. 代码和注释是否符合 PostgreSQL 代码风格
  3. 是否还存在风险,重点排查性能隐患,安全隐患,稳定性隐患,内存泄露,非法内存访问,进程死锁,和其他插件冲突等等
  4. 是否还存在事实错误,表述不准确等
  5. 在 PG14 - 19 版本编译,并进行回归、TAP、压测可靠性测试等各种场景验证
  6. 是否可提交至官方 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

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论