暂无图片
暂无图片
12
暂无图片
暂无图片
暂无图片

Postgres 之一条SQL的奇幻历程

原创 大表哥 2023-01-16
2214

image.png

大家好, 今天给大家分享的是 Postgres 之一条SQL的奇幻历程。

简简单单的说就是在客户端连上了PG实例之后,从执行一条SQL 按下键盘上的回车键到客户端返回给你结果,这之间到底发生了什么?
本文是从源码的角度来debug 跟踪一下SQL执行的整体的流程。本人非C语言的开发者,难免有不当之处,望请各位大佬专家指点更正!

我们的实验环境是 linux + pg 13.8 源代码 + 调试工具 CLion:

为了debug 方便,我们直接将 CLion 安装在了 linux 的server 上,并且这台server 源码方式安装了 postgres 13.8的版本:

在CLion 中的debug 窗口中,启动 postgres 实例, 传入的参数是 数据文件的data 路径:

Image.png

debug 方式启动之后, 我们可以看到在输出 log 命令台上,像是 后台主进程 postgres 启动成功:

Image.png

PG 数据库是一款多进程的程序在后台运行, 所以我们在debug的时候,需要 debug PG 主程序 master 进程 fork 出来的子进程:

我们可以看到 主进程号是 104804

Image.png

主进程和子进程的树形图: 这些子进程就是我们熟悉 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语言的基础的同学可以研究一下。

Image.png

我们尝试用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();

Image.png

客户端尝试登陆:

/u01/postgres/pg13/bin/psql -h /tmp -p 5432

我们可以看到 fork 出来的 子进程 是 pid : 122846

Image.png

我们去PG 的实例上验证一下: 查询 pg_backend_pid()

postgres=# select pg_backend_pid(); pg_backend_pid ---------------- 122846 (1 row)

我们观察linux 操作系统进程树上 多个子进程 122846

Image.png

接下来,我们需要 attach 这个子进程122846 进行 debug, 看看我们执行一条SQL后台是如何处理的?

我们的开发工具colin 对于 attached fork的进程操作很简单: Run-> Attach to Process…

Image.png

接下来我们在处理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:

Image.png

dehug 到源代码中:

Image.png

接下来,我们可以一步一步的debug : 进入到重要的函数入口 exec_simple_query(query_string);

Image.png

进入函数 exec_simple_query(query_string) 后, 我们来到了关键的第一个步骤, AST 语法解析树:

Image.png

接着一路debug 下一步到另外2个关键的函数:

pg_analyze_and_rewrite 函数尝试重写 SQL
pg_plan_queries 函数得到SQL的执行计划

Image.png

在SQL语句经过了 pg_analyze_and_rewrite 和 pg_plan_queries 2个阶段后, 进入实际执行的阶段
入口的函数是 /src/tcop/postgres.c 中的函数 PortalRun

Image.png

接下来会调用执行器 ExecutorRun --> 根据执行计划运行正确的执行器 ExecSeqScan (本SQL为全表扫描的执行器):

具体调用的图示:

Image.png

返回的结果集 tuple -》 slot = ExecProcNode(planstate);

Image.png

最后把返回的结果集slot,发送到我们的PSQL执行的客户端:

Image.png

客户端返回结果集:

Image.png

最后我们来总结一下:

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 😃 !

最后修改时间:2023-01-16 18:51:04
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论