大家好, 今天给大家分享的是 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 😃 !