大家好, 今天给大家分享的是 Postgres 之一条SQL的奇幻历程。
简简单单的说就是在客户端连上了PG实例之后,从执行一条SQL 按下键盘上的回车键到客户端返回给你结果,这之间到底发生了什么?
本文是从源码的角度来debug 跟踪一下SQL执行的整体的流程。本人非C语言的开发者,难免有不当之处,望请各位大佬专家指点更正!
我们的实验环境是 linux + pg 13.8 源代码 + 调试工具 CLion:
为了debug 方便,我们直接将 CLion 安装在了 linux 的server 上,并且这台server 源码方式安装了 postgres 13.8的版本:
在CLion 中的debug 窗口中,启动 postgres 实例, 传入的参数是 数据文件的data 路径:
debug 方式启动之后, 我们可以看到在输出 log 命令台上,像是 后台主进程 postgres 启动成功:
PG 数据库是一款多进程的程序在后台运行, 所以我们在debug的时候,需要 debug PG 主程序 master 进程 fork 出来的子进程:
我们可以看到 主进程号是 104804
主进程和子进程的树形图: 这些子进程就是我们熟悉 PG启动后的后台 backgroud 进程 :
checkpointer,
background writer,
walwriter,
autovacuum launcher,
stats collector,
logical replication launcher
INFRA [postgres@ljzdcmongo004 ~]# pstree -p 104804
postgres(104804)─┬─postgres(104808)
├─postgres(104809)
├─postgres(104810)
├─postgres(104811)
├─postgres(104812)
└─postgres(104813)
主进程 对应的源码文件函数是 PostgresMain()(路径在 src/postmaster/postmaster.c)
/*
* Postmaster main entry point
*/
void
PostmasterMain(int argc, char *argv[])
{
int opt;
int status;
char *userDoption = NULL;
bool listen_addr_saved = false;
int i;
char *output_config_variable = NULL;
InitProcessGlobals();
PostmasterPid = MyProcPid;
IsPostmasterEnvironment = true;
...
子进程初始化启动的代码 : PostmasterMain() 函数中
/*
* Remove old temporary files. At this point there can be no other
* Postgres processes running in this directory, so this should be safe.
*/
RemovePgTempFiles();
/*
* Initialize stats collection subsystem (this does NOT start the
* collector process!)
*/
pgstat_init();
/*
* Initialize the autovacuum subsystem (again, no process start yet)
*/
autovac_init();
/*
* If enabled, start up syslogger collection subprocess
*/
SysLoggerPID = SysLogger_Start();
/*
* We're ready to rock and roll...
*/
StartupPID = StartupDataBase();
Assert(StartupPID != 0);
StartupStatus = STARTUP_RUNNING;
pmState = PM_STARTUP;
/* Some workers may be scheduled to start now */
maybe_start_bgworkers();
status = ServerLoop();
/*
* ServerLoop probably shouldn't ever return, but if it does, close down.
*/
ExitPostmaster(status != STATUS_OK);
abort();
PG源码的注释还是很有意思: 当数据库基本上所有的进程启动成功后, 注释写到: We’re ready to rock and roll… 我们准备好摇滚了!!!
RemovePgTempFiles() – 删除一些临时文件
pgstat_init(); – fork 收集统计信息的子进程
autovac_init(); – fork 出 auto vacumm 的子进程
SysLoggerPID = SysLogger_Start(); – fork 出收集日志信息的子进程
maybe_start_bgworkers(); --fork 出辅助的后台worker 进程
status = ServerLoop(); – 这个很重要,是postmaster 的监听程序: 1)监听了所有后台fork 出来的 background process , 如果挂了,直接拉起来 2) 监听了网络过来的连接,并负责fork 出来子进程来处理客户端请求的SQL
基本上所有的background process 的源码文件也都在 路径 src/postmaster/ 文件夹下面: 有C语言的基础的同学可以研究一下。
我们尝试用PSQL 在客户端建立一个连接 ,代码应该是由 ServerLoop() 这个函数负责 fork 出来一个子进程来处理SQL语句的请求:
监听的本质上就是一个 for(;; ) 的死循环,处理来自客户端的socket 请求:
/*
* Main idle loop of postmaster
*
* NB: Needs to be called with signals blocked
*/
static int
ServerLoop(void)
{
fd_set readmask;
int nSockets;
time_t last_lockfile_recheck_time,
last_touch_time;
last_lockfile_recheck_time = last_touch_time = time(NULL);
nSockets = initMasks(&readmask);
for (;;)
{
fd_set rmask;
int selres;
time_t now;
/*
* Wait for a connection request to arrive.
*
* We block all signals except while sleeping. That makes it safe for
* signal handlers, which again block all signals while executing, to
* do nontrivial work.
*
* If we are in PM_WAIT_DEAD_END state, then we don't want to accept
* any new connections, so we don't call select(), and just sleep.
*/
...
...
}
}
我们现在用PSQL 在 Linux server 上建议一个连接, 并在函数中ServerLoop()中的 BackendStartup(port) 打上断点:
BackendStartup(port) 这个函数负责 fork 出子进程处理 pid = fork_process();
客户端尝试登陆:
/u01/postgres/pg13/bin/psql -h /tmp -p 5432
我们可以看到 fork 出来的 子进程 是 pid : 122846
我们去PG 的实例上验证一下: 查询 pg_backend_pid()
postgres=# select pg_backend_pid();
pg_backend_pid
----------------
122846
(1 row)
我们观察linux 操作系统进程树上 多个子进程 122846
接下来,我们需要 attach 这个子进程122846 进行 debug, 看看我们执行一条SQL后台是如何处理的?
我们的开发工具colin 对于 attached fork的进程操作很简单: Run-> Attach to Process…
接下来我们在处理SQL的入口打上断点:/src/tcop/postgres.c 中的函数 PostgresMain
官方的函数注释很规范,果然是学院派的代表典范!!!
/* ----------------------------------------------------------------
* PostgresMain
* postgres main loop -- all backends, interactive or otherwise start here
*
* argc/argv are the command line arguments to be used. (When being forked
* by the postmaster, these are not the original argv array of the process.)
* dbname is the name of the database to connect to, or NULL if the database
* name should be extracted from the command line arguments or defaulted.
* username is the PostgreSQL user name to be used for the session.
* ----------------------------------------------------------------
*/
void
PostgresMain(int argc, char *argv[],
const char *dbname,
const char *username)
具体的断点的打到 firstchar = ReadCommand(&input_message); 这行上, 我们看一下接收到的SQL 语句:
我们执行SQL:
dehug 到源代码中:
接下来,我们可以一步一步的debug : 进入到重要的函数入口 exec_simple_query(query_string);
进入函数 exec_simple_query(query_string) 后, 我们来到了关键的第一个步骤, AST 语法解析树:
接着一路debug 下一步到另外2个关键的函数:
pg_analyze_and_rewrite 函数尝试重写 SQL
pg_plan_queries 函数得到SQL的执行计划
在SQL语句经过了 pg_analyze_and_rewrite 和 pg_plan_queries 2个阶段后, 进入实际执行的阶段
入口的函数是 /src/tcop/postgres.c 中的函数 PortalRun
接下来会调用执行器 ExecutorRun --> 根据执行计划运行正确的执行器 ExecSeqScan (本SQL为全表扫描的执行器):
具体调用的图示:
返回的结果集 tuple -》 slot = ExecProcNode(planstate);
最后把返回的结果集slot,发送到我们的PSQL执行的客户端:
客户端返回结果集:
最后我们来总结一下:
SQL执行和源码中函数的简单对照关系的流程:
顺序步骤描述 | 调用函数以及源码文件 |
---|---|
建立客户端连接,fork出处理SQL请求的子进程 | ServerLoop()–> BackendStartup() 源码文件: src/backend/postmaster/postmaster.c |
进入处理客户端发送SQL语句的总入口 | PostgresMain()–>ReadCommand()–>exec_simple_query() 源码文件: src/backend/tpoc/postgres.c |
AST 语法树解析成 node | PostgresMain()–>exec_simple_query() raw_parsetree_list = raw_parser(query_string) parsetree_list =pg_parse_query(query_string); 源码文件: src/backend/tpoc/postgres.c |
SQL 进行重写优化 | PostgresMain()–>exec_simple_query() querytree_list = pg_analyze_and_rewrite(parsetree, query_string,NULL, 0, NULL); 源码文件: src/backend/tpoc/postgres.c |
SQL 生成执行计划树 | PostgresMain()–>exec_simple_query() plantree_list = pg_plan_queries(querytree_list, query_string,CURSOR_OPT_PARALLEL_OK, NULL); 源码文件: src/backend/tpoc/postgres.c |
启动Portal ,执行SQL语句 | PostgresMain()–>exec_simple_query()–> (void) PortalRun()–> PortalRunSelect() --> ExecutorRun()–>standard_ExecutorRun()–>ExecutePlan() -->ExecProcNode()–>ExecSeqScan() 源码文件: src/backend/tpoc/postgres.c src/backend/tpoc/pgquery.c src/backend/executor/execMain.c src/backend/executor/nodeSeqScan.c |
查询SQL的结果返回给客户端 | PostgresMain()–>exec_simple_query()–> dest->receiveSlot(slot, dest) 源码文件: src/backend/tpoc/postgres.c src/backend/executor/execMain.c |
Have a fun 😃 !