postgres.c是PostgreSQL后端的重要源代码文件,负责管理查询的整体流程。本文以PostgreSQL-12.2为例,首先对postgres.c的功能、包含的函数及函数间的调用关系进行简单概述;然后详细介绍其中的入口函数PostgresMain,并且对于PostgresMain函数涉及的通信协议将做进一步展开说明。
PostgreSQL是典型的C/S的模式,服务器后台有一个主进程(postmaster),默认监听5432端口,等待连接请求。在多用户模式下,当客户端的连接请求进来时,postmaster进程会fork一个服务进程(postgres)为客户端服务。postgres.c位于src/backend/tcop/目录下,是服务进程postgres的入口文件,负责管理查询的整体流程。
postgres.c中总共有56个函数定义,其中,
PostgresMain 函数最为关键,它是postgres.c的入口函数。因此,下面本文将围绕 PostgresMain 函数进行详细介绍。
PostgresMain函数1的工作流程如下图,以下依次介绍其中的主要步骤:初始化相关变量和进程状态(2.1节)、配置参数(2.2节)、设置信号处理和信号屏蔽(2.3节)、初始化postgres的运行环境(2.4节)、创建内存上下文并设置查询取消跳跃点(2.5节)及循环等待处理查询(2.6节)。
进入PostgresMain函数后,首先对相关变量进行初始化,如下图。
postgres进程有两种启动方式:一种是多用户模式,在postmaster的监控下,动态地被postmaster创建为用户服务;另一种是单用户模式,由–single选项直接启动,不经过postmaster,为单一用户提供服务,这种模式通常用于initdb初始化阶段及修复系统表等。如下图所示,在单用户模式下,postgres进程调用InitStandaloneProcess函数初始化启动进程环境;而在多用户模式下,该操作由postmaster进程完成。此操作完成后,postgres设置进程状态为InitProcessing。
PostgresMain函数的配置参数分为三个步骤:
读配置文件重新设置参数,如下图所示。
其中,步骤1)和步骤3)在多用户模式下(即IsUnderPostmaster为true时)已由postmaster完成,在单用户模式下分别调用函数InitializeGUCOptions、SelectConfigFiles完成。步骤2)调用函数process_postgres_switches完成。
postgres中信号及相应的信号处理函数对应的代码片段、各信号处理函数的功能,如下图、下表。
信号 | 信号处理函数 | 功能 |
---|---|---|
SIGHUP | PostgresSigHupHandler | 当配置文件发生变化时,产生SIGHUP信号。服务进程收到此信号后,设置ConfigReloadPending为真,重新读取配置文件。 |
SIGINT | StatementCancelHandler | 收到SIGINT信号后调用此函数,终止正在进行的查询操作。若此时进程正在退出(proc_exit_inprogress为真),则什么也不做;否则,将标志位InterruptPending和QueryCancelPending设置为真,表明准备处理查询取消中断。 |
SIGTERM | die | 用于终止当前事务。若此时进程正在退出,则什么也不做;否则,将标志位InterruptPending和ProcDiePending设置为真,表明准备处理进程退出中断。在单用户模式下,调用ProcessInterrupts退出。 |
SIGQUIT | quickdie / die | 首先屏蔽其他信号,然后结束正在进行的工作并退出。 |
SIGALRM | handle_sig_alarm | 处理SIGALRM信号,由等待锁的进程超时引发,如果存在死锁则将自己从锁等待队列中退出。 |
SIGPIPE | SIG_IGN | 忽略对应的信号。 |
SIGUSR1 | procsignal_sigusr1_handler | 处理用户自定义信号。 |
SIGUSR2 | SIG_IGN | 忽略对应的信号。 |
SIGFPE | FloatExceptionHandler | 调用floating-point exception函数报浮点数异常错误。 |
SIGCHLD | SIG_DFL | SIGCHLD信号由postmaster进程接收,将信号重置为0。 |
在信号处理函数设置完成后,postgres进程开始初始化运行环境。如下图,postgres进程首先调用checkDataDir函数检查DataDir变量,确保其是一个合法的、有访问权限的数据目录路径,然后调用ValidatePgVersion函数检查PG_VERSION文件中的版本信息与当前程序版本兼容。在路径与版本检查通过后,调用ChangeToDataDir函数(该函数实质调用系统调用函数chdir)将当前工作目录转到DataDir字符串表示的目录,以方便postmaster进程及其他后台进程使用相对路径访问数据目录。
路径设置完成后,postgres进程调用CreateDataDirLockFile函数,该函数又调用CreateLockFile函数创建锁文件postmaster.pid,从而保证当前只有一个postmaster在运行且没有任何独立后台进程运行。然后,调用LocalProcessControlFile函数读取控制文件,调用InitializeMaxBackends函数初始化最大后台进程数。
在多用户模式下,上述工作均由postmaster进程完成。接下来,postgres进程开始进行三个初始化,如下图,分别为BaseInit、InitProcess和InitPostgres。
BaseInit函数(定义于src/backend/utils/init/postinit.c)第一步调用InitCommunication函数创建共享内存和信号量并进行初始化;第二步调用DebugFileOpen函数初始化input/output/debugging文件描述符;第三步调用InitFileAccess函数初始化文件访问;第四步调用InitSync函数初始化跟踪文件同步的数据结构;第五步调用smgrinit函数初始化或关闭存储管理器;最后调用InitBufferPoolAccess函数初始化共享缓冲池。
InitProcess函数(定义于src/backend/storage/lmgr/proc.c)用于在共享内存中为每个后台进程创建PGPROC数据结构。在EXEC_BACKEND情况下,该工作由SubPostmasterMain完成。
InitPostgres函数(定义于src/backend/utils/init/postinit.c)的输入包含数据库和用户两部分:数据库由数据库名或数据库OID参数指定;用户由用户名或用户OID指定。如果传入的参数是数据库OID,该函数还将把对应的数据库名返回给调用者。在bootstrap模式不需要参数。InitPostgres函数首先完成PGPROC结构的填充并将其添加到ProcArray,从而使得其对其他后台进程可见。随后开始一系列的初始化操作,包含初始化后端共享缓冲池(InitBufferPoolBackend)、XLOG访问(InitXLOGAccess)、关系Cache(RelationCacheInitialize和RelationCacheInitializePhase2)、系统表Cache(InitCatalogCache)、查询计划Cache(InitPlanCache)、Portal管理器(EnablePortalManager)以及状态收集器(pgstat_initialize)等。
如下图,postgres首先创建一个名为MessageContext的内存上下文,用于存储前端发送过来的消息中的查询命令及在查询过程中产生的中间数据。每次PostgresMain进入下一次循环(处理完客户端的查询请求)时,该内存上下文将被重置。
创建完成后,postgres调用sigsetjmp函数设置跳跃点,如下图。每当客户端取消查询请求或发生错误时,将从PG_exception_stack(指向跳跃点的指针)处退出当前事务并重新开始查询。
完成上述工作,postgres进入查询处理的主循环,如下图。
第一次进入循环时,服务进程首先会释放上次查询时的内存,并为新的查询分配内存,准备好查询执行环境。然后调用ReadyForQuery函数给客户端发送消息告诉客户端它已经准备好接收查询了。接下来服务进程调用ReadCommand函数开始从客户端接收消息,如下图。
ReadCommand函数中有一个条件判断语句,如下图,若是在多用户模式(通过网络连接)下,则调用SocketBackend函数读取客户端请求;若是在单用户模式(服务端和客户端合为一体)下,则调用InteractiveBackend函数从交互式命令行读取客户端请求。
最后ReadCommand函数返回消息的消息类型,postgres进程使用switch选择结构根据不同的消息类型进行相应的处理。在此阶段涉及PostgreSQL的通信协议23,下面对其进行展开说明。
PostgreSQL使用一种基于消息的协议完成服务端与客户端之间的通信,至今已有三个版本的通信协议。其中,通信协议2.0版本在PostgreSQL-6.4提出(1999年左右),通信协议3.0版本在PostgreSQL-7.4提出(2003年)。目前普遍使用的是通信协议3.0版本(其他版本的协议依然支持),因此本文主要介绍PostgreSQL通信协议的3.0版本。
首先介绍通信过程中涉及的数据结构:共享缓冲区PqSendBuffer(用于接收服务端发送给客户端的消息)和PqRecvBuffer(用于接收客户端发送给服务端的消息)。它们的定义位于src/backend/libpq/pqcomm.c,如下图。可以看到:它们都是大小为8192字节的缓冲区,其中PqRecvBuffer的大小是固定的,而PqSendBuffer可以通过pq_putmessage_noblock函数进行扩展。
此外,这两个共享缓冲区上的操作依赖于字符指针。如下图,全局变量PqSendPointer是一个字符指针,表示在PqSendBuffer中此地址前的消息正在等待发送给客户端,新加入的消息则放在PqSendPointer所在的位置。具体的操作函数如internal_putbytes。
如下图,字符指针PqRecvLength表示下一个发送过来的消息在缓冲区PqRecvBuffer中存放的位置,PqRecvPointer指向位置之前的消息已被处理,PqRecvPointer和PqRecvLength之间的消息还未处理。具体的操作函数如pq_getbyte、pq_getbytes等。
对通信相关的数据结构有大致的了解后,下面介绍通信协议的阶段划分及相应的消息格式。如下图,PostgreSQL的通信协议分为两阶段:启动(startup)阶段和正常(normal)阶段。在启动阶段,客户端向服务端发送连接请求,服务端确认后反馈状态信息,从而创建数据库连接;然后进入正常阶段,客户端发送查询请求到服务端,服务端执行命令并返回查询结果给客户端。
上述两阶段都是通过消息流进行的,但消息格式略有差异。在正常阶段,消息的第一个字节标识消息类型,接下来四个字节标识消息的长度(该长度包括这四个字节,但不包括消息类型字节),具体的消息内容由消息类型决定。而在启动阶段,消息以长度标识开始,随后是协议版本号,最后是键值对形式的连接信息,如用户名、数据库名及GUC参数等。
本文主要介绍PostgreSQL通信协议正常阶段的两种子协议:简单查询协议(3.1节)和扩展查询协议(3.2节)。
本节将围绕简单查询协议的通信过程、简单查询的处理函数exec_simple_query以及追踪调试三方面展开。
简单查询协议(Simple query protocol)的通信过程如下:
客户端通过Query消息发送一个SQL查询给服务端;服务端收到该消息后,调用exec_simple_query函数进行处理,然后回复查询结果。查询结果包含两部分内容:结构与数据。结构通过RowDescription消息传递,包括列名、类型OID、长度等;数据通过DataRow消息传递,每个DataRow消息中包含一行数据。
最后服务端发送CommandComplete和ReadyForQuery消息。
CommandComplete表示当前命令执行完成。客户端的一条查询请求可能包含多条SQL命令(作为一个事务),每个SQL命令执行完都会回复一条CommandComplete消息;如果其中有SQL命令执行失败,整个事务都会回滚。
在查询请求执行结束后,服务端会回复一条ReadyForQuery消息,告知客户端可以发送新的请求。ReadyForQuery消息中包含事务状态信息,目前包含I、T、E三种事务状态类型。I表示不处于事务中;T表示处于事务中;E表示在一个失败的事务中。该信息对psql、pgbouncer等很有用,出现于通信协议3.0版本。对于通信协议2.0版本,libpq通过字符串比较来跟踪事务状态。
上述消息格式与消息流如下图。
简单查询处理函数exec_simple_query的核心代码如下图,主要步骤如下:
1) 根据查询语句,调用pg_parse_query进行词法分析和语法分析,生成语法解析树;
2) 根据语法解析树,调用pg_analyze_and_rewrite进行语义分析和查询重写,生成查询树;
3) 根据查询树,调用pg_plan_queries进行查询优化,生成执行计划;
4) 根据执行计划,调用PortalRun执行查询。
如下图,使用psycopg2连接PostgreSQL数据库,追踪“select * from
student;”语句发现:psycopg2默认使用的是简单查询协议。
如下图,使用psql连接PostgreSQL数据库,追踪同样的语句发现:psql也默认使用简单查询协议。
扩展查询协议(Extended query
protocol)将查询的处理流程分为若干步骤,以达到执行计划复用的目的。每一步都由单独的服务端消息进行确认(但是服务端消息可以连续发送,无需等待)。
扩展查询协议允许查询中带参数,如:select * from student where id =
$var;不允许一个查询中包含多条SQL命令,如:select * from company;
delete from company;。
下面将分别介绍扩展查询协议通常包括的五个阶段:Parse(3.2.1节)、Bind(3.2.2节)、Describe(3.2.3节)、Execute(3.2.4节)和Sync(3.2.5节)。
Parse阶段主要对查询语句进行解析并保存,本节将从Parse阶段的通信过程及相应的处理函数exec_parse_message两方面进行介绍。
(1)通信过程:
客户端首先向服务端发送一个Parse消息,如下图,该消息可以包含参数占位符以及每个参数的类型,还可以指定语句的名字:若不指定名字,即为一个未命名语句(unnamed
statement),该语句会在生成下一个未命名语句时予以销毁;若指定名字,则必须在下次发送Parse消息前将其显式销毁。
服务端收到该消息后,调用exec_parse_message函数进行处理。
(2)exec_parse_message函数:
exec_parse_message函数的核心代码如下图,主要进行语法分析、语义分析和重写,同时会创建一个Plan
Cache的结构,用于缓存后续的执行计划。当查询语句是一个命名语句时,该函数还会将其作为预备语句(prepared
statement)保存。
Bind阶段主要进行参数绑定,本节将从Bind阶段的通信过程及相应的处理函数exec_bind_message两方面进行介绍。
(1)通信过程:
经过Parse阶段之后,客户端发送Bind消息。如下图,Bind消息含有参数值、参数格式及返回列的格式等信息。
(2)exec_bind_message函数:
exec_bind_message函数的核心代码如下图,除了绑定参数外,该函数获取Parse阶段缓存的执行计划及预备语句,并为解析过的语句创建一个Portal用于后续执行。
Describe阶段属于一个插曲,确保客户端对正在返回的数据有所了解。
本节将从Describe阶段的通信过程、相应的处理函数exec_describe_statement_message、以及exec_describe_portal_message三方面进行介绍。
(1)通信过程:
客户端可以发送格式如上图所示的Describe消息请求一个statement或portal的描述信息。statement的描述信息包含两部分:ParameterDescription和RowDescription;portal的描述信息则只有RowDescription。
服务端收到该消息后,会对客户端的请求加以判断:如果是statement的描述信息,则调用exec_describe_statement_message函数进行处理;如果是portal的描述信息,则调用exec_describe_portal_message函数进行处理。
(2)exec_describe_statement_message函数:
exec_describe_statement_message函数的核心代码如下图,从pq_beginmessage_reuse函数到pq_endmessage_reuse函数,进行参数描述;然后调用SendRowDescriptionMessage函数进行结果描述。
(3)exec_describe_portal_message函数:
exec_describe_portal_message函数的核心代码如下图,主要发送RowDescription消息。
Execute阶段主要完成查询的执行,本节将从Execute阶段的通信过程及相应的处理函数exec_execute_message两方面进行介绍。
(1)通信过程:
portal创建之后,客户端发送Execute消息请求执行,如上图。Execute消息中可以指定返回的行数,若指定为0,则表示返回所有行。
服务端收到该消息后,调用exec_execute_message函数进行处理。
(2)exec_execute_message函数:
exec_execute_message函数的核心代码如下图,主要调用PortalRun执行查询语句,执行结果通过DataRow消息返回给客户端,执行完成后发送CommandComplete。
Sync阶段标志扩展查询协议的结束,本节将从Sync阶段的通信过程及相应的处理函数finish_xact_command两方面进行介绍。
(1)通信过程:
Sync消息格式如上图,服务端收到该消息后,如果事务是隐式的,服务端将关闭事务,并以ReadyForQuery消息响应。
(2)finish_xact_command函数:
finish_xact_command函数的核心代码如下图,主要调用CommitTransactionCommand函数完成事务的提交。
扩展查询协议完整的消息流如下图所示:
如下图,使用PgJDBC连接PostgreSQL数据库,执行“select * from
company;”语句发现:PgJDBC默认使用的是扩展查询协议。
然后,将代码中的数据库连接字符串加上参数loggerLevel=TRACE以追踪带参数语句“select
id from company where name = ?”的查询过程。如下图,追踪日志可以显示服务端和客户端的通信过程,可以看到:客户端依次进行了sendParse、sendBind、sendDescribePortal、sendExecute、sendSync操作;服务端对这些操作依次给予了ParseComplete、BindComplete、RowDescription、DataRow等响应。
此处需要注意的是,客户端和服务端的操作是按照严格的次序一一对应进行的,但是为了优化网络传输,客户端的所有请求一次性发送到服务端,服务端依次处理完成后又一次性返回给客户端,避免多次发送数据,从而降低通信开销。
本文主要围绕postgres.c中的入口函数PostgresMain展开,对两种通信子协议(简单查询协议和扩展查询协议)及相关函数(exec_simple_query、exec_parse_message、exec_bind_message、exec_describe_statement_message、exec_describe_portal_message、exec_execute_message和finish_xact_command)做了进一步介绍。客户端发送的全部消息类型及服务端对应的处理操作可以总结如下:
客户端消息类型 | 服务端操作 |
---|---|
Q | exec_simple_query:处理简单查询 |
P | exec_parse_message:扩展查询的Parse阶段,解析语句并保存 |
B | exec_bind_message:扩展查询的Bind阶段,绑定参数 |
E | exec_execute_message:扩展查询的Execute阶段,执行 |
F | HandleFunctionRequest:fastpath函数调用 |
C | DropPreparedStatement / drop_unnamed_stmt / PortalDrop /* Close */ |
D | exec_describe_statement_message / exec_describe_portal_message /* describe */ |
H | pq_flush:输出所有缓存内容 |
S | finish_xact_command |
X | /* normal shutdown */ |
EOF | /* normal shutdown */ |
d | /* copy data */ |
c | /* copy done */ |
f | /* copy fail */ |
彭智勇, 彭煜玮.
PostgreSQL数据库内核分析[M]. 机械工业出版社, 2012. ↩︎
https://www.postgresql.org/docs/12/protocol.html ↩︎
https://www.pgcon.org/2014/schedule/attachments/330_postgres-for-the-wire.pdf ↩︎