使用GDB和VS Code调试PostgreSQL实操
使用GDB和VS Code调试PostgreSQL实操
如果你想使用gdb调试PG, 首先要在编译PG软件时加上调试选项, 安装符号表等相关的包. 下面是一个详细的例子.
以下内容翻译自: https://sbaziotis.com/databases/building-and-debugging-postgres.html
介绍
这篇文章比较短,介绍了如何从源代码构建 Postgres,以及如何以各种方式进行调试。我相信这些信息肯定散落在互联网的各个角落,但由于我花了数周时间才弄清楚这一切 —— 主要是通过反复试验 —— 所以我想把所有内容都整理到一起。
从源代码构建
我建议从源代码构建 Postgres,这样你就可以添加debug信息,从而更好地了解数据库正在发生的事情。我个人使用自定义 Docker 镜像来安装PG(便于调试)。在这种情况下,它非常有用,因为我们需要 root 权限。从一个安装所有必需组件的简单 Ubuntu 镜像开始:
FROM ubuntu:22.04
WORKDIR /app
RUN apt-get update && apt-get install -y \
build-essential pkg-config libicu-dev \
g++ make bison flex libreadline-dev \
zlib1g-dev git llvm-dev clang \
&& rm -rf /var/lib/apt/lists/*
您看到的大多数软件包都是构建 Postgres 基础所必需的。我们还需要llvm-dev启用 JIT 编译,该功能现在在 Docker 镜像等环境中默认启用。完成所有必要设置后,下载 Postgres 源代码;在本文中,我使用的是 17.5版本。我没有在发行版中从 FTP 服务器下载源代码( https://www.postgresql.org/ftp/source/v17.5/ ),而是从 Github 下载。这样,我始终可以轻松地撤消所做的所有更改(指使用git命令回滚代码版本)。因此,让我们首先获取源代码,克隆一个发行版的单个分支,并且只克隆最后一次提交(因为我们只需要能够回溯到最后一次提交):
git clone https://github.com/postgres/postgres --branch REL_17_5 --depth 1
假设我们在 /app/postgres 中克隆它。我们进入到postgres目录,然后:
./configure --with-llvm
我们需要这个--with-llvm标志,否则 JIT 编译将不可用。假设此操作成功,下一步是使用以下命令构建 Postgres:
make -j4
然后我们执行以下命令。正如我之前所说,我们需要 sudo 访问权限。最简单的方法是以 root 身份登录,如下所示。如果您无法执行 su,但您的用户仍在 sudoers 中,您可以随时在每个命令前添加 sudo 。此外,您不必添加名为 postgres 的新用户 。您可以执行所有其他命令并使用您的用户(例如 stef )。唯一的限制是,在这种情况下,您的用户不应该是 root ,因为出于安全原因,Postgres 不能以 root 用户身份启动。
su
make install
adduser postgres
mkdir -p /usr/local/pgsql/data
chown postgres /usr/local/pgsql/data
我们首先获取 root 权限,这是后续步骤所需的。首先安装我们构建的程序。然后,创建postgres用户,因为 Postgres 无法以 root 身份运行(出于安全原因)。在此步骤中,系统会要求您设置密码并输入一些其他信息。通常情况下,您需要一个安全可靠的密码,但在本例中,您可以使用类似 "a" 的简单密码。最后,我们创建 Postgres 用于存储所有文件的目录,并指定postgres为其所有者,因为postgres用户将启动 Postgres 进程。接下来,我们将执行以下命令:
su - postgres
/usr/local/pgsql/bin/initdb -D /usr/local/pgsql/data
现在我们使用postgres用户来启动 Postgres 实例。到目前为止,我们一直遵循Postgres 文档中 关于如何从源代码构建和运行 Postgres 的方法。但是,现在我们将偏离这个方法,因为我发现这种方法更容易迭代。我们将直接以普通可执行文件的形式启动 Postgres,而不是将其作为后台进程启动。因此,在我们登录的同一个终端中,我们执行以下postgres命令:
/usr/local/pgsql/bin/postgres -D /usr/local/pgsql/data
现在,我们让这个终端继续运行。这样做的好处是它会显示终端上的任何日志,这对于迭代很有用。现在,在一个新的终端中,我们首先为实验创建一个数据库:
/usr/local/pgsql/bin/createdb test
现在我们可以使用以下命令连接到该数据库。如果您之前没有创建 postgres用户,则应省略-U postgres。
/usr/local/pgsql/bin/psql -U postgres -d test
此时,设置基本完成。我们可以开始执行查询了。但我的迭代周期是这样的。首先,我有3 个终端:(1)postgres终端,(2)psql终端,(3)make终端。在 postgres终端中,我以postgres用户身份登录并运行postgres 可执行文件。psql终端是面向用户的终端,我们将在本文的其余部分使用它。在这里,我们只执行查询,将数据插入数据库等。在这里,我以 Docker root身份登录。最后,只有在进行make install更改时才会使用make终端。因此,迭代如下所示:对 Postgres 代码进行更改后,我首先在postgres终端 (1) 中使用 Ctrl+C 停止,在make终端 (3) 中按下make install ,然后在 postgres终端(1) 中重新启动,并在 psql终端(2) 中执行查询以查看更改。
使用 GDB 进行调试
调试 Postgres 并非易事,因为它使用许多线程,并且涉及多个可执行文件(例如postgres和psql)。但这里有一个对我有用的方法。首先,你需要使用调试信息编译 Postgres。首先:
./configure --enable-cassert --enable-debug CFLAGS="-O0 -g3 -ggdb -fno-omit-frame-pointer"
如果您之前已经build过 Postgres,请先执行以下操作:
make clean
make distclean
然后:
make -j4
sudo make install
现在按照我们上面讨论的方式先停库, 再启动您的 Postgres 后端:
/usr/local/pgsql/bin/postgres -D /usr/local/pgsql/data
然后,在新的终端中启动您的psql连接数据库:
/usr/local/pgsql/bin/psql -d test
在做其他任何事情之前,请先执行以下操作:
SELECT pg_backend_pid(); -- 得到当前psql连接的数据库会话对应的backend process pid
然后,在另一个终端中,像这样启动gdb:
sudo gdb /usr/local/pgsql/bin/postgres <PID>
这应该会给你一个类似这样的输出:
Reading symbols from /usr/local/pgsql/bin/postgres...
Attaching to program: /usr/local/pgsql/bin/postgres, process 235825
Reading symbols from /lib/x86_64-linux-gnu/libz.so.1...
(No debugging symbols found in /lib/x86_64-linux-gnu/libz.so.1)
...
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
0x000073c17c325e5a in epoll_wait (epfd=6, events=0x58839c925e00, maxevents=1, timeout=-1) at ../sysdeps/unix/sysv/linux/epoll_wait.c:30
30 ../sysdeps/unix/sysv/linux/epoll_wait.c: No such file or directory.
不必担心 epoll_wait.c 的错误。这只是因为后端当前处于空闲状态。另外,也不必担心所有“No debugging symbols found”的消息。但是,您应该确保:
Reading symbols from /usr/local/pgsql/bin/postgres...
成功了,这意味着你的系统缺失某些符号表, 可安装即可:
(No debugging symbols found in /lib/x86_64-linux-gnu/libz.so.1)
接下来,执行以下操作:
handle SIGPIPE SIGUSR1 SIGUSR2 SIGALRM nostop noprint pass
来处理这些问题。最后,添加断点。如果您在 Postgres 的核心部分添加了断点,则应立即添加。例如:
(gdb) b exec_simple_query
Breakpoint 1 at 0x588373babda3: file postgres.c, line 1018.
但是,如果您在代码的 PL/SQL 部分添加断点(例如 exec stmt execsql https://github.com/postgres/postgres/blob/5e2f3df49d4298c6097789364a5a53be172f6e85/src/pl/plpgsql/src/pl_exec.c#L4216 ),则gdb可能会找不到任何调试符号:
(gdb) b exec_stmt_execsql
Function "exec_stmt_execsql" not defined.
Make breakpoint pending on future shared library load? (y or [n])
发生这种情况是因为 PL/SQL 模块是一个 SQL 扩展。这意味着它是作为共享库构建和加载的。假设您目前在 GDB 会话中尚未发出任何涉及 PL/pgSQL 的查询,则plpgsql lib库尚未被加载。因此,您可以将其设置为待处理状态。
为了确保其有效,请添加一个exec_simple_query断点,然后键入短码 c 或:
(gdb) continue
现在,在运行的psql终端中,发出一些简单的命令,例如:
test=# SELECT 1;
这看起来好像要花很长时间,因为希望在终端运行 gdb时,你能命中断点。需要注意的是,如果你需要将整个SQL文件作为输入进行调试。可这样: 启动psql, 获得pid, 启动gdb, 然后在psql中使用\i SQLFILE即可.
在 VS Code 中调试
正如你所料,在 VS Code 中调试稍微困难一些,但同样,这里有一个对我有用的方法。我们将使用gdbserver,因为正如我们之前所见,要成功进行调试会话,gdb需要从 sudo 开始 。在终端中执行此操作很简单,但在 VS Code 中执行此操作很麻烦。因此,我们将在终端中使用sudo启动一个 GDB 服务器,然后让 VS Code 与其通信。这比听起来容易得多。首先,确保 gdbserver已安装 ,例如:
sudo apt-get install gdbserver
然后,像之前一样启动psql并获取 PID。在第三个终端中执行以下操作:
sudo gdbserver --attach :1234 <PID>
# 注: 有个疑问, 难道这里不用象gdb一样指定postgres的进程吗, 否则怎么知道用什么符号表进行解释呢?
# sudo gdb /usr/local/pgsql/bin/postgres <PID>
# 继续往下看, 原来配置在 launch.json 里面
您应该看到:
Attached; pid = 246440
Listening on port 1234
在 VS Code 中打开 Postgres 源代码,并在任意位置添加一个断点,只需确保这个断点会被我们稍后执行的查询命中即可。转到“Run and Debug”选项卡,并像使用 VS Code 进行任何调试会话一样创建一个断点文件launch.json。使用以下内容:
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach via gdbserver",
"type": "cppdbg",
"request": "launch",
"program": "/usr/local/pgsql/bin/postgres",
"cwd": "${workspaceFolder}",
"MIMode": "gdb",
"miDebuggerPath": "/usr/bin/gdb",
"miDebuggerServerAddress": "localhost:1234",
"setupCommands": [
{ "text": "-enable-pretty-printing" },
{ "text": "set pagination off" },
{ "text": "handle SIGALRM SIGUSR1 SIGPIPE nostop noprint pass" }
]
}
]
}
现在,点击“Run”。你可能会看到一些错误,但不用担心。在运行的gdbserver终端中,你应该会看到类似以下内容:
Remote debugging from host 127.0.0.1, port 55466
最后,打开psql终端并发出查询。希望 VS Code 能够在发出查询后在断点处中断。对我来说,当我停止调试器时,它会停止psql进程,原因我不清楚,但这对我来说没什么大不了的。
PG单用户模式下的调试
如果你不知道什么是单用户模式, 可先阅读以下文章.
《PostgreSQL 把数据库所有的superuser都变成普通账号后, 怎么找回超级账号? - 单用户模式改pg_authid》
《PostgreSQL 单用户模式修复 系统索引损坏 - single user mode, reindex system》
《PostgreSQL single-user mode 单用户模式修复 database is not accepting commands to avoid wraparound data loss in database "...." 问题 - usage,like Repair Database》
“单用户模式”顾名思义,它在单个线程中运行,具有单个用户、单个数据库和单个可执行文件。所有操作都在一个终端中进行。您可以使用以下命令启动它:
/usr/local/pgsql/bin/postgres --single -D /usr/local/pgsql/data test
请注意,我们不仅需要传递 --single 参数,在本例中还需要传递要使用的数据库test。将进入如下终端:
backend>
这种模式的好处在于所有操作都集中在一个可执行文件postgres中。可执行文件psql本身不参与其中。这让我们几乎可以用 GDB 调试一个简单的可执行文件。唯一的问题是如何发出一个.sql文件来运行,这在调试时很有用,因为我们不想每次迭代都重新输入代码。问题在于,在这种模式下,我们只能通过直接输入代码或……通过管道来运行代码。长话短说,我使用以下脚本进行了短暂的快速迭代:
gdb -ex 'set breakpoint pending on' \
-ex 'b exec_stmt_execsql' \
-ex 'run --single -D /usr/local/pgsql/data test < ./regression-sl.sql' \
-ex 'layout src' \
--args /usr/local/pgsql/bin/postgres
你应该小心处理你通过管道传入的文件。在单用户模式下,Postgres 会逐行读取文件,一些正常工作的操作在这里可能无法正常工作。例如,下面这样就无法正常工作:
CREATE FUNCTION func() RETURNS VOID AS $$
BEGIN
SELECT SUM(o_totalprice) FROM orders;
END
$$
LANGUAGE PLPGSQL;
相反,您应该在一行中定义它,如下所示:
CREATE FUNCTION func() RETURNS VOID AS $$BEGIN SELECT SUM(o_totalprice) FROM orders; END$$ LANGUAGE plpgsql;
在PG代码中添加打印件
软件社区多年来一直争论着 Printf 与调试器调试孰优孰劣。我对这场争论不感兴趣,因为我觉得两者在不同情况下都很有用。我只在调试 Postgres 时发表我的看法。和我做的大多数事情一样,我对 Postgres 所做的大部分工作都与性能有关。要弄清楚一个查询是否比另一个查询更快,您不能使用调试版本,因为您无法获得准确的数字。因此,大多数时候我使用优化版本(即 编译PG时使用-O选项, 关闭调试选项等),并且只在这里和那里添加打印以大致查看采用了哪些代码路径。只有当我真的需要深入研究程序所采用的代码路径时,我才会切换到调试版本。简而言之,打印至少在某些时候是有用的,所以让我们学习如何添加它们。
Postgres 是用 C 语言编写的,您可能认为只需在代码周围添加 printfs 即可。然而,这样做的问题是,打印语句会在运行的postgres终端中出现,这并不理想,因为 postgres可执行文件默认会记录所有其他通常对迭代无用的信息,并使输出变得混乱。您可以改为使用 elog(INFO, "..."),例如:
elog(INFO, "STEF44: ncols: %d\n", ncols);