在 Linux/BSD 上获取运行中的 PostgreSQL 后端的堆栈跟踪
Linux 和 BSD
Linux 和 BSD 系统通常使用 GNU 编译器集合 和 GNU 调试器 ("gdb")。获取进程的堆栈跟踪非常简单。
(如果你想要更多信息,而不是仅仅是堆栈跟踪,请查看 开发者常见问题解答,它涵盖了交互式调试)。
安装外部符号
(从 ports 安装的 BSD 用户可以跳过此步骤)
在许多 Linux 系统上,调试信息与程序二进制文件分开存储。它通常在安装软件包时不会安装,因此如果你想调试程序(例如,获取堆栈跟踪),你需要安装调试信息软件包。不幸的是,这些软件包的名称根据你的发行版而有所不同,安装过程也是如此。
GNOME Wiki 上维护了一些与 PostgreSQL 无关的通用说明 这里.
在 Debian 上
http://wiki.debian.org/HowToGetABacktrace
Debian Squeeze (6.x) 用户还需要从 backports 安装 gdb 7.3,因为 Squeeze 中提供的 gdb 不理解较新 PostgreSQL 构建中使用的 PIE 可执行文件。
在 Ubuntu 上
首先,按照 Ubuntu wiki 条目中的说明进行操作 DebuggingProgramCrash.
完成启用调试信息包的使用后,你需要使用该 wiki 文章中链接的 list-dbgsym-packages.sh
脚本获取所需调试包的列表。仅仅安装 postgresql 的调试包是不够的。
在按照 Ubuntu wiki 中的说明操作后,将脚本下载到桌面,打开终端并运行
$ sudo apt-get install $(sudo bash Desktop/list-dbgsym-packages.sh -t -p $(pidof -s postgres))
在 Fedora 上
所有 Fedora 版本:FedoraProject.org - StackTraces
其他发行版
一般来说,你需要至少安装 PostgreSQL 服务器和客户端的调试符号包,以及可能存在的任何通用软件包,以及 libc 的调试符号包。建议添加 PostgreSQL 使用的其他库的调试符号,以防你遇到的问题出现在这些库中或与之相关。
收集堆栈跟踪
如何判断堆栈跟踪是否有效
阅读本节内容,并在使用以下说明收集信息时牢记于心。确保你收集的信息真正有用将为你和其他人节省时间和麻烦。
要获取有效的堆栈跟踪,必须有可用的调试符号。如果你没有安装所需的符号,回溯将包含很多像这样的条目
#1 0x00686a3d in ?? () #2 0x00d3d406 in ?? () #3 0x00bf0ba4 in ?? () #4 0x00d3663b in ?? () #5 0x00d39782 in ?? ()
... 在没有访问你的系统(以及即使有访问也很少有用)的情况下,这些信息对调试完全无用。如果你看到类似以上的结果,你需要安装调试符号包,甚至用调试功能重新编译 postgresql。**不要费心收集此类回溯,它们没有用。**
有时你会得到回溯,其中只包含函数名及其所在的可执行文件,而不是源代码文件名和行号或参数。这样的输出将包含类似这样的行
#11 0x00d3afbe in PostmasterMain () from /usr/lib/postgresql/8.4/bin/postgres
这不是理想的情况,但比什么都没有好。安装调试信息包应该提供更详细的堆栈跟踪,包含行号和参数信息,例如
#9 0xb758d97e in PostmasterMain (argc=5, argv=0xb813a0e8) at postmaster.c:1040
... 这对追踪问题最为有用。请注意,它引用了源文件和行号,而不仅仅是可执行文件名。
识别要连接的后端
你需要知道要连接的 postgresql 后端的进程 ID。如果你对使用大量 CPU 的后端感兴趣,它可能会出现在 top
中。如果你与你感兴趣的后端有当前连接,请使用 select pg_backend_pid()
获取其进程 ID。否则,pg_catalog.pg_stat_activity
和/或 pg_catalog.pg_locks
视图可能有助于识别感兴趣的后端;请参阅这些视图中的 "procpid" 列。
将 gdb 附加到后端
一旦你知道了要连接的进程 ID,请运行
sudo gdb -p pid
其中 "pid" 是后端的进程 ID。GDB 将暂停你指定的进程的执行,并在显示后端当前正在运行的调用后将你带入交互模式((gdb)
提示符),例如
0xb7c73424 in __kernel_vsyscall () (gdb)
你需要告诉 gdb 将会话日志保存到一个文件中,因此在 gdb 提示符下输入
(gdb) set pagination off (gdb) set logging file debuglog.txt (gdb) set logging on
gdb 现在将所有输入和输出保存到一个文件中,即 debuglog.txt
,该文件位于你启动 gdb 的目录中。
此时,后端的执行仍然暂停。它甚至可能阻碍其他后端,因此我建议你使用 "cont" 命令告诉它恢复正常执行
(gdb) cont Continuing.
后端现在正在正常运行,就像 gdb 没有连接到它一样。
获取跟踪
好了,gdb 已连接,你可以获取有效的堆栈跟踪。
除了以下说明,你还可以查看 开发者常见问题解答 上关于使用 gdb 与 postgresql 后端的几个有用提示。
从运行中的后端获取代表性跟踪
如果你担心某个案例执行查询花费的时间过长,使用过多的 CPU,或者似乎陷入无限循环,你将需要**反复**中断其执行,获取堆栈跟踪,然后让它恢复执行。收集几个堆栈跟踪有助于更好地了解它在哪些地方花费了时间。
你可以使用 ^C(Ctrl+C)中断后端并返回到 gdb 命令行。在 gdb 命令行,你可以使用 "bt" 命令获取回溯,然后使用 "cont" 命令恢复正常的后端执行。
收集了几个回溯后,在 gdb 交互式提示符下分离然后退出 gdb
(gdb) detach Detaching from program: /usr/lib/postgresql/8.3/bin/postgres, process 12912 (gdb) quit user@host:~$
另一种方法是使用 gcore
程序保存运行程序的一系列核心转储,而不会中断其执行。这些核心转储可以在你方便的时候检查,为你提供更多信息,而不仅仅是回溯,因为你不用在思考和键入时阻塞后端的执行。
从错误报告处获取跟踪
如果你试图找出意外错误的原因,最有用的是在你让后端继续执行之前在 errfinish 处设置断点
(gdb) b errfinish Breakpoint 1 at 0x80ced0: file elog.c, line 414. (gdb) cont Continuing.
现在,在你连接的 psql 会话中,运行任何需要激发错误的查询。当它发生时,后端将在 errfinish 处停止执行。使用 bt 收集回溯,然后 quit(或者,如果要再次执行,则使用 cont)。
在 errfinish 处设置断点将捕获不仅仅是 ERROR 报告的生成,还包括 NOTICE、LOG 和任何其他不被 client_min_messages 或 log_min_messages 抑制的消息。你可能需要调整这些设置,以避免不得不继续执行一堆无关的消息。
从可重复崩溃的后端获取跟踪
如果 gdb 检测到程序崩溃,它将自动中断程序的执行。因此,在你将 gdb 附加到预期崩溃的后端后,你只需让它像往常一样继续执行,并执行使后端崩溃所需的任何操作。
当后端崩溃时,gdb 将把你带入交互模式。在 gdb
提示符下,你可以输入 bt
命令获取崩溃的堆栈跟踪,然后输入 cont
继续执行。当 gdb 报告进程已退出时,使用 quit
命令。
或者,你可以像下面解释的那样收集核心文件,但如果你在后端崩溃之前知道要将 gdb 附加到哪个后端,这可能比它更麻烦。
从随机崩溃的后端获取跟踪
从不知道为什么崩溃的后端、不知道是什么导致后端崩溃,也不知道哪些后端会在何时崩溃的后端获取堆栈跟踪要困难得多。为此,你通常需要启用核心文件的生成,核心文件是程序崩溃时由操作系统生成的程序状态的可调试转储。
启用核心转储
在 Linux 系统上,你可以检查进程是否启用了核心文件生成,方法是检查 /proc/$pid/limits,其中 $pid 是感兴趣的进程 ID。 "Max core file size" 应该是非零值。
通常,在 PostgreSQL 启动脚本顶部添加 "ulimit -c unlimited" 并重新启动 postgresql 就可以启用核心转储收集。确保你的 PostgreSQL 数据目录中有足够的可用空间,因为核心转储将写入到该目录,并且由于 Pg 使用共享内存,它们可能相当大。暂时减小 postgresql.conf 中的 shared_buffers 大小 可能会有所帮助。这样可以避免核心转储导致系统长时间(可能几分钟)无响应,这种情况在 shared_buffers 大于几个 GB 时可能会发生。显著减小 shared_buffers 通常不会使服务器速度变得无法忍受,因为 PostgreSQL 会更多地使用文件系统缓存。
在 Linux 系统上,更改核心转储使用的文件名格式也很有必要,这样核心转储就不会相互覆盖。/proc/sys/kernel/core_pattern
文件控制这一点。我建议使用 core.%p.sig%s.%ts
,它会记录进程的 PID、导致其结束的信号以及生成核心转储的时间戳。请参考 man 5 core
。要应用设置更改,只需运行 echo core.%p.sig%s.%ts | sudo tee -a /proc/sys/kernel/core_pattern
。
你可以通过启动一个 `psql` 会话、使用上面给出的说明找到其后端 PID,然后使用 "kill -ABRT pidofbackend"(其中 pidofbackend 是 postgres 后端的 PID,而不是 psql 的 PID)将其杀死,来测试核心转储是否已启用。你应该会在你的 postgresql 数据目录中看到一个核心文件。
调试核心转储
启用核心转储后,你需要等到看到后端崩溃。操作系统会生成一个核心转储,你就可以将 gdb 附加到它以收集堆栈跟踪或其他信息。
如果你想获得有用的回溯和其他调试信息,你需要告诉 gdb 生成核心转储的可执行文件。为此,在调用 gdb 时,只需指定 postgres 可执行文件路径和核心文件路径,如下所示。如果你不知道 postgres 可执行文件的位置,可以通过检查运行中的 postgres 实例的 /proc/$pid/exe 来获取它。例如
$ for f in `pgrep postgres`; do ls -l /proc/$f/exe; done lrwxrwxrwx 1 postgres postgres 0 2010-04-19 10:30 /proc/10621/exe -> /usr/lib/postgresql/8.4/bin/postgres lrwxrwxrwx 1 postgres postgres 0 2010-04-19 10:51 /proc/11052/exe -> /usr/lib/postgresql/8.4/bin/postgres lrwxrwxrwx 1 postgres postgres 0 2010-04-19 10:51 /proc/11053/exe -> /usr/lib/postgresql/8.4/bin/postgres lrwxrwxrwx 1 postgres postgres 0 2010-04-19 10:51 /proc/11054/exe -> /usr/lib/postgresql/8.4/bin/postgres lrwxrwxrwx 1 postgres postgres 0 2010-04-19 10:51 /proc/11055/exe -> /usr/lib/postgresql/8.4/bin/postgres
... 从上面我们可以看到,我(Ubuntu)系统上的 postgres 可执行文件是 /usr/lib/postgresql/8.4/bin/postgres
。
知道可执行文件路径和核心文件位置后,只需使用它们作为参数运行 gdb,即 gdb -q /path/to/postgres /path/to/core
。现在你就可以像调试正常运行的 postgres 一样调试它,如上面的部分所述。
调试核心转储 - 例子
例如,我刚刚使用 kill -ABRT
强制 postgres 后端崩溃,在我的 Ubuntu 系统上的数据目录 /var/lib/postgresql/8.4/main
中有一个名为 core.10780.sig6.1271644870s
的核心文件。我使用 /proc 发现我系统上 postgres 的可执行文件是 /usr/lib/postgresql/8.4/bin/postgres
。
现在可以轻松地针对它运行 GDB 并请求堆栈跟踪
$ sudo -u postgres gdb -q -c /var/lib/postgresql/8.4/main/core.10780.sig6.1271644870s /usr/lib/postgresql/8.4/bin/postgres Core was generated by `postgres: wal writer process '. Program terminated with signal 6, Aborted. #0 0x00a65422 in __kernel_vsyscall () (gdb) bt #0 0x00a65422 in __kernel_vsyscall () #1 0x00686a3d in ___newselect_nocancel () from /lib/tls/i686/cmov/libc.so.6 #2 0x00e68d25 in pg_usleep () from /usr/lib/postgresql/8.4/bin/postgres #3 0x00d3d406 in WalWriterMain () from /usr/lib/postgresql/8.4/bin/postgres #4 0x00bf0ba4 in AuxiliaryProcessMain () from /usr/lib/postgresql/8.4/bin/postgres #5 0x00d3663b in ?? () from /usr/lib/postgresql/8.4/bin/postgres #6 0x00d39782 in ?? () from /usr/lib/postgresql/8.4/bin/postgres #7 <signal handler called> #8 0x00a65422 in __kernel_vsyscall () #9 0x00686a3d in ___newselect_nocancel () from /lib/tls/i686/cmov/libc.so.6 #10 0x00d37bee in ?? () from /usr/lib/postgresql/8.4/bin/postgres #11 0x00d3afbe in PostmasterMain () from /usr/lib/postgresql/8.4/bin/postgres #12 0x00cdc0dc in main () from /usr/lib/postgresql/8.4/bin/postgres
这个例子展示了一个不包含函数参数的堆栈跟踪。你的系统上可能存在或不存在函数参数,这取决于你无法控制的模糊细节,比如 Postgres 是否最初是构建为省略帧指针、DWARF 版本等。总的来说,在主流 Linux 平台上获取回溯的情况自从这个例子回溯最初添加以来有了显著改善。如今,最好使用 "bt full" 而不是 "bt",因为这可以提供更多信息(崩溃期间本地/堆栈变量的值)。一般来说,你能提供的调试信息越多越好。
如果你没有安装正确的符号,或者向 gdb 指定了错误的可执行文件,或者根本没有指定可执行文件,你就会看到一个无用的回溯,就像下面这个
$ sudo -u postgres gdb -q -c /var/lib/postgresql/8.4/main/core.10780.sig6.1271644870s Core was generated by `postgres: wal writer process '. Program terminated with signal 6, Aborted. #0 0x00a65422 in __kernel_vsyscall () (gdb) bt #0 0x00a65422 in __kernel_vsyscall () #1 0x00686a3d in ?? () #2 0x00d3d406 in ?? () #3 0x00bf0ba4 in ?? () #4 0x00d3663b in ?? () #5 0x00d39782 in ?? () #6 <signal handler called> #7 0x00a65422 in __kernel_vsyscall () #8 0x00686a3d in ?? () #9 0x00d3afbe in ?? () #10 0x00cdc0dc in ?? () #11 0x005d7b56 in ?? () #12 0x00b8fad1 in ?? ()
如果你看到类似的东西,就不要费心发送它。如果你没有把可执行文件路径搞错,你可能需要安装 PostgreSQL 的调试符号(或者甚至重新编译 PostgreSQL,启用调试),然后重试。
跟踪创建集群时的故障
如果你在尝试使用 initdb 创建数据库集群时遇到崩溃,这可能会留下一个核心转储,你可以使用 gdb 按照上面描述的方法进行分析。例如,如果出现断言错误,应该会这样。你可能需要给 initdb 提供 --no-clean 选项,以防止它删除新数据目录和核心文件。
另一种查找引导时间错误的方法是手动将引导命令输入引导模式或单用户模式,数据目录保留来自 initdb --no-clean 的目录。如果没有任何导致核心转储的 PANIC,而是 FATAL 或 ERROR,例如,这将会有所帮助。很容易将 GDB 附加到这样的后端。
此外,尝试使用未修补的主分支创建数据目录,然后使用修补过的后端触发崩溃,而不是 initdb。
从 GDB 中转储页面映像
在社区邮件列表中报告问题时,发布包含一个 原始页面映像 的文件有时很有用。表和索引都由 8KiB 大小的块/页面组成,可以将其视为数据存储的基本单元。当数据完整性可疑时,这尤其有用,例如当由于破坏数据的错误导致断言失败时。GDB 使从交互式会话中轻松完成此操作(尽管核心转储可能 在转储共享内存方面存在问题)。
示例
Breakpoint 1, _bt_split (rel=0x7f555b6f3460, itup_key=0x55d03a745d40, buf=232, cbuf=0, firstright=366, newitemoff=216, newitemsz=16, newitem=0x55d03a745d18, newitemonleft=true) at nbtinsert.c:1205 1205 { (gdb) n 1215 Buffer sbuf = InvalidBuffer; (gdb) 1216 Page spage = NULL; (gdb) 1217 BTPageOpaque sopaque = NULL; (gdb) 1227 int indnatts = IndexRelationGetNumberOfAttributes(rel); (gdb) 1228 int indnkeyatts = IndexRelationGetNumberOfKeyAttributes(rel); (gdb) 1231 rbuf = _bt_getbuf(rel, P_NEW, BT_WRITE); (gdb) 1244 origpage = BufferGetPage(buf); (gdb) 1245 leftpage = PageGetTempPage(origpage); (gdb) 1246 rightpage = BufferGetPage(rbuf); (gdb) 1248 origpagenumber = BufferGetBlockNumber(buf); (gdb) 1249 rightpagenumber = BufferGetBlockNumber(rbuf); (gdb) dump binary memory /tmp/dump_block.page origpage (origpage + 8192)
页面 "origpage" 的内容现在被转储到文件 "/tmp/dump_block.page" 中,该文件的大小将正好为 8192 字节。这适用于任何出现 "Page" C 类型的地方(它实际上是 bufpage.h 中定义的 typedef -- 一个未修饰的 "Page" 实际上是一个 char 指针)。"Page" 变量是指向页面映像的原始指针,通常是存储在 shared_buffers 中的权威/当前页面。
pg_hexedit
还要注意,Postgres 十六进制编辑器工具 pg_hexedit 可以快速 在 GDB 中可视化页面映像,并使用直观的标签和注释。当最初不清楚哪些页面映像是有趣的,或者当需要随时间推移从同一块中捕获页面的多个映像时,例如在运行测试用例时,使用 pg_hexedit 可能更容易。
contrib/pageinspect 页面转储
当使用 GDB 不方便,并且不需要获取在崩溃时正好为当前的页面映像时,可以使用 contrib/pageinspect 以更轻量级的方式将任意页面转储到文件。例如,以下交互式 shell 会话将索引 'pgbench_pkey' 中块 42 的当前页面映像转储到文件。
$ psql -c "create extension pageinspect" CREATE EXTENSION $ psql -XAtc "SELECT encode(get_raw_page('pgbench_pkey', 42),'base64')" | base64 -d > dump_block_42.page
这假设可以使用 psql 以超级用户身份连接,并且 base64 程序位于用户的 $PATH 中。GNU coreutils 包通常包含 base64,因此它已经在大多数 Linux 安装中可用。请注意,可能需要安装名为 "postgresql-contrib" 或类似的系统包,才能安装 pageinspect 扩展。
通常,遵循此过程的最简单方法是首先成为 postgres 操作系统用户(例如,通过 "su postgres")。
在 GDB 下启动 Postgres
使用 GDB 调试像 PostgreSQL 这样的多进程应用程序在历史上一直非常痛苦。值得庆幸的是,通过最近的 7.x 版本,“下属”(GDB 术语,指多个被调试的进程)已经得到了极大的改进。
注意!这仍然很脆弱,因此不要指望在生产环境中使用它。
# Stop server
pg_ctl -D /path/to/data stop -m fast
# Launch postgres via gdb
gdb --args postgres -D /path/to/data
现在,在 GDB shell 中,使用以下命令设置环境
# We have scroll bars in the year 2012!
set pagination off
# Attach to both parent and child on fork
set detach-on-fork off
# Stop/resume all processes
set schedule-multiple on
# Usually don't care about these signals
handle SIGUSR1 noprint nostop
handle SIGUSR2 noprint nostop
# Make GDB's expression evaluation work with most common Postgres Macros (works with Linux).
# Per https://postgresql.ac.cn/message-id/[email protected],
# have many Postgres macros work if these are defined (useful for TOAST stuff,
# varlena stuff, etc):
macro define __builtin_offsetof(T, F) ((int) &(((T *) 0)->F))
macro define __extension__
# Ugly hack so we don't break on process exit
python gdb.events.exited.connect(lambda x: [gdb.execute('inferior 1'), gdb.post_event(lambda: gdb.execute('continue'))])
# Phew! Run it.
run
要获得进程列表,运行 info inferior
。要切换到另一个进程,运行 inferior NUM
。
使用 rr 记录和重放框架记录 Postgres
PostgreSQL 13 可以使用 rr 调试记录器 进行调试。本节描述了一些使用 rr 调试 Postgres 的有用工作流程。它主要写给 Postgres 黑客,尽管 rr 也可在报告错误时使用。
版本兼容性
提交 fc3f4453a2bc95549682e23600b22e658cb2d6d7 解决了一个问题,该问题使得在早期 Postgres 版本中难以使用 rr,因此在这些版本中可能会出现问题。此外,较旧/LTS Linux 操作系统版本附带的早期版本的 rr 可能不支持 Postgres 使用的系统调用,例如 sync_file_range()
。所有这些问题可能都有相当简单的解决方法(例如,你可以使用 --wal_writer_flush_after=0 --backend_flush_after=0 --bgwriter_flush_after=0 --checkpoint_flush_after=0
启动 Postgres)。
Postgres 设置
使用 rr 记录 postgres 会话的脚本可能包含以下示例代码段
rr record -M /code/postgresql/$BRANCH/install/bin/postgres \
-D /code/postgresql/$BRANCH/data \
--log_line_prefix="%m %p " \
--effective_cache_size=1GB \
--random_page_cost=4.0 \
--work_mem=4MB \
--maintenance_work_mem=64MB \
--fsync=off \
--log_statement=all \
--log_min_messages=DEBUG5 \
--max_connections=50 \
--shared_buffers=32MB
这里的大多数细节都是比较随意的。总体思路是尽可能使日志输出详细,并保持服务器使用的内存量较低。
使用 "rr record" 运行服务器时,使用 "make installcheck" 运行服务器相当实用,记录整个执行过程。这比仅对 Postgres 的常规调试版本运行测试要慢不了多少。例如,它仍然比 Valgrind 快得多。重放记录似乎是拥有高端机器很有帮助的地方。
日志中的事件编号
测试完成后,以通常的方式停止 Postgres(例如,Ctrl + C)。记录保存到大多数 Linux 发行版上的 $HOME/.local/share/rr/
目录中。rr 在这个父目录中为每个不同的记录创建了一个目录。rr 还维护一个指向最新记录目录的符号链接(latest-trace
),这通常在重放记录时使用。注意不要不小心留下了太多记录。它们可能相当大。
记录/Postgres 终端的输出如下所示(使用示例 "rr record" 食谱时)
[rr 1786705 1241867]2020-04-04 21:55:05.018 PDT 1786705 DEBUG: CommitTransaction(1) name: unnamed; blockState: STARTED; state: INPROGRESS, xid/subid/cid: 63992/1/2 [rr 1786705 1241898]2020-04-04 21:55:05.019 PDT 1786705 DEBUG: StartTransaction(1) name: unnamed; blockState: DEFAULT; state: INPROGRESS, xid/subid/cid: 0/1/0 [rr 1786705 1241902]2020-04-04 21:55:05.019 PDT 1786705 LOG: statement: CREATE TYPE test_type_empty AS (); [rr 1786705 1241906]2020-04-04 21:55:05.020 PDT 1786705 DEBUG: CommitTransaction(1) name: unnamed; blockState: STARTED; state: INPROGRESS, xid/subid/cid: 63993/1/1 [rr 1786705 1241936]2020-04-04 21:55:05.020 PDT 1786705 DEBUG: StartTransaction(1) name: unnamed; blockState: DEFAULT; state: INPROGRESS, xid/subid/cid: 0/1/0 [rr 1786705 1241940]2020-04-04 21:55:05.020 PDT 1786705 LOG: statement: DROP TYPE test_type_empty; [rr 1786705 1241944]2020-04-04 21:55:05.021 PDT 1786705 DEBUG: drop auto-cascades to composite type test_type_empty [rr 1786705 1241948]2020-04-04 21:55:05.021 PDT 1786705 DEBUG: drop auto-cascades to type test_type_empty[] [rr 1786705 1241952]2020-04-04 21:55:05.021 PDT 1786705 DEBUG: MultiXact: setting OldestMember[2] = 9 [rr 1786705 1241956]2020-04-04 21:55:05.021 PDT 1786705 DEBUG: CommitTransaction(1) name: unnamed; blockState: STARTED; state: INPROGRESS, xid/subid/cid: 63994/1/3
方括号中的每行日志部分来自 rr(因为我们在记录时使用了 -M
)-- 第一个数字是 PID,第二个是事件编号。你可能不会关心 PID,因为事件编号本身就可以明确地识别特定后端的特定“事件”(即使存在多个线程或进程,rr 记录也是单线程的)。假设你想要到达 CREATE TYPE test_type_empty AS ()
查询 -- 你可以使用以下选项重放记录,以到达查询的末尾
$ rr replay -M -g 1241902
这样重放记录会将你带到 Postgres 后端打印执行示例查询结束时的日志消息的地方 -- 你会得到一个 gdb 调试服务器(rr 实现了一个 gdb 后端)和一个交互式 gdb 会话。这并不完全是你感兴趣的执行点,但已经很接近了。你可以轻松地在你感兴趣的精确函数处设置断点,然后 reverse-continue
通过向后执行到达那里。
你也可以使用 fork 选项找到特定后端启动的点。因此,对于 PID 1786705,它将看起来像这样
$ rr replay -M -f 1786705
(不要尝试使用类似的-p
选项,因为当pid已经被exec
时,该选项会启动一个调试服务器。)
请注意,使用“tee”等标准工具保存录制输出似乎存在一些问题[1]。通过执行“自动驾驶”回放,您可能会获得日志输出(包含这些事件编号),例如:
$ rr replay -M -a &> rr.log
现在您有一个日志文件,可以搜索合适的事件编号作为起点。这在运行“make installcheck”或自定义测试套件时可能是一个实际的必要条件,因为可能存在数兆字节的日志输出。不过,通常您无需以这种方式生成日志。执行自动驾驶回放可能需要几分钟时间,因为rr会以低于实时的速度回放所有已记录的内容。
使用GDB命令在录制中来回跳转
当您大致了解错误在rr录制中出现的位置和时间后,您需要使用gdb实际调试问题。通常,自然的方法是来回跳转录制以跟踪已知出现故障的后台中的问题。
连接到gdb后,您可以使用gdb的“when”命令检查当前事件编号,这在确定相对于“make check”的高级输出(假设使用了-M
选项以获得事件编号)已到达执行的哪一点时非常有用。
(rr) when Current event: 379377
由于事件编号由进程/线程共享,并且在录制期间始终按顺序执行,因此事件编号是推理录制进行程度的通用方式,无论是在进程内部还是跨进程。我们不局限于将调试器附加到恰好是Postgres后端的进程。
rr还支持gdb的checkpoint
、restart
和delete
检查点命令;请参见GDB文档的相关部分。这些很有用,因为它们允许gdb以比“事件编号”更精细的粒度直接跟踪执行中的有趣点;当发生系统调用时,会创建新的事件编号,这对于实际定位特定后台/进程中的问题时可能过于粗粒度。
观察点和反向执行
由于rr支持反向调试,因此观察点更加有用。请注意,您通常应该使用watch -l expr
而不是仅使用watch expr
。如果没有-l,反向执行通常非常慢或明显有错误,因为gdb会在程序通过不同范围执行时尝试重新评估表达式。
调试tap测试
rr在调试诸如tap测试之类的东西时非常出色,在这些测试中存在复杂的脚手架,这些脚手架可能会运行多个Postgres服务器。您可以运行整个“rr record make check”,而无需担心脚手架的工作方式。一旦您拥有有用的PID(或事件编号)来处理,那么在感兴趣的后端中获得交互式调试会话就不会花费太多时间。您可以在完成录制“make check”执行后,从./tmp_check/log
目录中出现的日志中获取感兴趣的后端的PID。从那里,您可以通过将相关PID作为-f
参数传递来启动“rr replay”。
“make check”会话的示例回放
$ rr replay -M -f 2247718 [rr 2246854 304]make -C ../../../src/backend generated-headers [rr 2246855 629]make[1]: Entering directory '/code/postgresql/patch/build/src/backend' [rr 2246855 631]make -C catalog distprep generated-header-symlinks [rr 2246856 984]make[2]: Entering directory '/code/postgresql/patch/build/src/backend/catalog' *** SNIP -- Remaining "make check" output omitted for brevity *** -------------------------------------------------- ---> Reached target process 2247718 at event 379377. -------------------------------------------------- Reading symbols from /usr/bin/../lib/rr/librrpreload.so... Reading symbols from /lib/x86_64-linux-gnu/libpthread.so.0... Reading symbols from /usr/lib/debug/.build-id/0b/4031a3ab06ec61be1546960b4d1dad979d15ce.debug... *** SNIP *** (No debugging symbols found in /usr/lib/x86_64-linux-gnu/libicudata.so.66) Reading symbols from /lib/x86_64-linux-gnu/libnss_files.so.2... Reading symbols from /usr/lib/debug//lib/x86_64-linux-gnu/libnss_files-2.31.so... 0x0000000070000002 in ?? () (rr) bt #0 0x0000000070000002 in ?? () #1 0x00007f0d2c25c3b6 in _raw_syscall () at raw_syscall.S:120 #2 0x00007f0d2c2582ff in traced_raw_syscall (call=call@entry=0x681fffa0) at syscallbuf.c:229 #3 0x00007f0d2c259978 in sys_fcntl (call=<optimized out>) at syscallbuf.c:1291 #4 syscall_hook_internal (call=0x681fffa0) at syscallbuf.c:2855 #5 syscall_hook (call=0x681fffa0) at syscallbuf.c:2987 #6 0x00007f0d2c2581da in _syscall_hook_trampoline () at syscall_hook.S:282 #7 0x00007f0d2c25820a in __morestack () at syscall_hook.S:417 #8 0x00007f0d2c258225 in _syscall_hook_trampoline_48_3d_00_f0_ff_ff () at syscall_hook.S:428 #9 0x00007f0d2b5a9f15 in arch_fork (ctid=0x7f0d297bee50) at arch-fork.h:49 #10 __libc_fork () at fork.c:76 #11 0x00005620ae898e53 in fork_process () at fork_process.c:62 #12 0x00005620ae8aab39 in BackendStartup (port=0x5620b0c1f600) at postmaster.c:4187 #13 0x00005620ae8a6d29 in ServerLoop () at postmaster.c:1727 #14 0x00005620ae8a64c2 in PostmasterMain (argc=4, argv=0x5620b0bf19e0) at postmaster.c:1400 #15 0x00005620ae7a8247 in main (argc=4, argv=0x5620b0bf19e0) at main.c:210
调试竞争条件
rr可用于隔离难以重现的竞争条件错误。rr录制/执行的单线程性质似乎使得更难以重现涉及并发执行的错误。但是,使用rr的混乱模式选项(通过使用rr记录的-h
参数)似乎增加了成功重现问题的可能性。它可能仍然需要几次尝试,但您只需要一次幸运。
打包录制
rr pack可用于以相当稳定的格式保存录制——它将所需文件复制到跟踪中。
$ rr pack
如果您想保存录制超过一两天,这可能很有用。由于录制的每一个细节(例如指针、PID)都是稳定的,因此您可以将录制视为一个完全自包含的东西。