PostgreSQL码农集散地

使用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);