Postgres中postmaster代码解析(中)

今天我们对postmaster的以下细节进行讨论:

backend的启动和client的连接请求的认证
客户端取消查询时的处理
接受pg_ctl的shutdown请求进行shutdown处理

2.与前端的交互

2.1backend的启动和client的连接请求的认证

关于backend的启动,其函数调用栈如下:

PostmasterMain()
   |->ServerLoop()
       |->initMasks()
       |->for(;;)
           |->select()         <--监听端口
           |->ConnCreate()     <--创建connection相关的数据结构
           |->BackendStartup() <--建立后端进程backend process
               |->PostmasterRandom()
               |->canAcceptConnections()
               |->fork_process()
               |->InitPostmasterChild()
               |->ClosePostmasterPorts()
               |->BackendInitialize()
                   |->ProcessStartupPacket()
               |->BackendRun()
                   |->PostgresMain()
            |->ConnFree()       <--释放connection相关的数据结构

简单来说,在系统调用select()中我们监听客户端的连接请求,当读到一个客户端请求时我们将为其创建相关数据结构,做一下初始化。注意此时只是监听接受了请求,这个请求是否合法(例如password是否正确)在此时是不做判断的。判断是放在BackendStartup()中的。

你可能会疑惑:BackendStartup()用来创建backend,但是创建了backend之后才做验证是不是有点晚?

我们来看BackendStartup()的处理(具体的大家看上面的调用栈和source)。

typedef struct bkend
{
	pid_t		pid;			/* process id of backend */
	long		cancel_key;		/* cancel key for cancels for this backend */
	int			child_slot;		/* PMChildSlot for this backend, if any */

	/*
	 * Flavor of backend or auxiliary process.  Note that BACKEND_TYPE_WALSND
	 * backends initially announce themselves as BACKEND_TYPE_NORMAL, so if
	 * bkend_type is normal, you should check for a recent transition.
	 */
	int			bkend_type;
	bool		dead_end;		/* is it going to send an error and quit? */
	bool		bgworker_notify;	/* gets bgworker start/stop notifications */
	dlist_node	elem;			/* list link in BackendList */
} Backend;

(backend的数据结构比较重要,我先引用在这里)

首先,程序会调用PostmasterRandom()函数产生一个cancelkey。这个cancelkey是做什么的呢?它是用来标记前端发来的cancel指令的:即前端发来一个中止当前SQL文的操作的指令时(比如你按Crtl+C), postmaster会先通过backendPID先在后端的backendList中找到对应的backend,再利用这个cancelkey来和这个backend做验证(这个在下一节会提到)。

然后调用canAcceptConnections来判断当前postmaster的状态是否可以接受连接?读者又疑惑了:前面不是已经接受连接了么?前面的select只是调用select()系统调用获取了这个连接请求(甚至连连接请求也算不上,只是收到了一个Client发来的packet,可能是startup_packet,也可能是cancel_request_packet),至于是不是能接受连接,我们有两个判断:

  • 当前数据库是不是处于可以接受连接的状态?(startup/shutdown/inconsistent recovery状态不可接受连接);
  • 当前数据连接数是不是已经满了(超过MaxConnections)

如果不满足条件。我们把将要启动的backend标记为dead_end。就是说这个后端只是用来向前端报错用的,报错之后立即退出。所以我们就不给它分配Slot了。判断可以连接了之后,我们就给它分配好slot。

继续往下走。调用fork_process()启动一个进程(当然就是用来作为backend的了)。backend启动起来了之后,我们就可以脱离postmaster,把后面的一切交给backend自己处理了。随之而来的InitPostmasterChild()就是用来初始化backend进程,将环境句柄从postmaster切换到backend。然后调用ClosePostmasterPorts()关闭此时不需要的文件描述符。

然后调用BackendInitialize做进一步的初始化。这里我们比较感兴趣的可能就是它调用了ProcessStartupPacket()获取前端发送的StartupPacket并为之分配内存,做一些简单的判断处理。验证部分放在了后面。

最后,我们调用BackendRun()真正的运行这个backend。
从代码我们可知,BackendRun()函数也只是一个壳子,他只是切换了内存上下文到TopMemoryContext并且获取postmaster的命令行上 -o参数指定的一些参数,然后将这些参数传给了PostgresMain()函数到这里,我们看出PostgresMain()函PostmasterMain()很像。都是命令的入口。而且后面我们看到PostgresMain()函数里面也有一个Loop。就是循读取客户端发来的SQL文。

InitPostgres()函数是PostgresMain()调用的一个非常重要的初始化函数,自然它的作用是做初始化。做哪些初始化呢?

我列举一些:

InitProcessPhase2()         Add my PGPROC struct to the ProcArray
SharedInvalBackendInit()    shared cache invalidation communication(inval)
ProcSignalInit()            Register the current process in the procsignal array
RegisterTimeout()           Register timeout
RelationCacheInitialize()   Initialize relationcache
InitCatalogCache()          Initialize catalog cache
InitPlanCache()             Initialize callbacks for inval

EnablePortalManager()       Portals are objects representing the execution state of a query,
                            This module provides memory management services for portals

InitializeClientEncoding()  initialize client encoding

关于PostgresMain()其它的我不多说,和PostmasterMain()很像,只不过处理的所有对象都是针对banckend的,具体看代码吧。

到这里我们可以回答为啥要创建了backend之后才做验证:

postmaster只充当一个中介的角色,不过多地涉及共享内存和其他会引起错误的操作,使得postmaster主程序更健壮和稳定。同时如果在ServerLoop里面花时间做验证我觉得也太费时间了。

哦,忘了,我们还没说client的连接请求的认证。下面是函数调用栈:

BackendRun()
    |->PostgresMain()
        |->InitPostgres()
            |->PerformAuthentication()
                |->ClientAuthentication()

PerformAuthentication()在InitPostgres中是在EnablePortalManager()之后调用的,这个时候大部分backend进程本身的初始化工作都已完毕。

这里做Client验证的入口是ClientAuthentication()了。它的主要工作如下:

hba_getauthmethod() //获取hba文件中和该条请求匹配的auth method
|
v
switch(auth_method)//根据auth_method和请求信息做出相应的处理
|
v
status == STATUS_OK ? sendAuthRequest() : auth_failed() 
                    //根据返回值决定向client发送验证packet还是拒绝请求

需要说明的是当验证失败,拒绝client的请求后,程序在这里就报错退出了。这样,这个backend就是一个dead_end的backend,他会在postmaster的指挥下退出,具体细节见后面几节内容。


2.2客户端取消查询时的中介

当我们在client(例如psql命令行)中运行一个很长的SQL查询(并不是说一定要很长的查询,只是如果时间太短的话你根本来不及cancel~)时,此时由于各种原因你想中止这条查询,于是你按下了Crtl+C键。立即在客户端上显示:

postgres=# select * from test order by id asc ;
Cancel request sent
ERROR: canceling statement due to user request

那我们来看一看postgres是如何处理这样的cancel吧。

先上图:

对应上图,我们针对涉及的进程分别列出函数调用栈:
client:
psql命令在初始化的时候调用setup_cancel_handler()在psql的MainLoop之前注册了一个信号处理函数,在收到client的SIGINT(也就是你按下Ctrl+C)后,调用handle_sigint()处理这个信号。处理成功后,打印:

Cancel request sent

(src/bin/psql/startup.c)
main()
    |->setup_cancel_handler()
        |->pqsignal(SIGINT, handle_sigint)
    |->successResult = MainLoop(stdin)

postmaster:processCancelRequest: postmaster在接收到client发来的packet后,建立一个后端进程(backend)去处理它,当发现它是一个cancel_request_packet后,调用processCancelRequest()函数处理这个packet,通过PID向对应的backend发送SIGINT信号。

PostmasterMain()
   |->ServerLoop()
       |->for(;;)
           |->BackendStartup() <--建立后端进程backend process
               |->BackendInitialize()
                   |->ProcessStartupPacket()
                       |->processCancelRequest()
                           |->signal_child(bp->pid, SIGINT)

backend:pqsignal(SIGINT, StatementCancelHandler): postgresMain()函数上注册下面这个信号处理函数,它接受postmaster发来的SIGINT信号,进行对应的处理,设置两个全局变量:

pqsignal(SIGINT, StatementCancelHandler)
{
...
		InterruptPending = true;
		QueryCancelPending = true;
...
}

而这两个全局变量又决定了CHECK_FOR_INTERRUPTS()是否生效:

#define CHECK_FOR_INTERRUPTS() \
do { \
	if (InterruptPending) \
		ProcessInterrupts(); \
} while(0)

我们进ProcessInterrupts()函数,发现他就是用来处理client的中止请求的:

ProcessInterrupts(){
...
InterruptPending = false;
...
		{
			LockErrorCleanup();
			ereport(ERROR,
					(errcode(ERRCODE_QUERY_CANCELED),
					 errmsg("canceling statement due to user request")));
		}
...		
}

报错消息就是我们上面所见的那条了。


2.3接受pg_ctl的shutdown请求

我们经常会使用pg_ctl 来控制postgres服务器,比如start,stop和reload等等。start参数就是对应着服务器的启动,这个在Postgres中postmaster代码解析(上)中我们已经讨论过。这里我们来讨论下指定stop参数的处理。

在开始讨论之前,我们先看下PMState这个枚举类型。

typedef enum
{
	PM_INIT,					/* postmaster starting */
	PM_STARTUP,					/* waiting for startup subprocess */
	PM_RECOVERY,				/* in archive recovery mode */
	PM_HOT_STANDBY,				/* in hot standby mode */
	PM_RUN,						/* normal "database is alive" state */
	PM_WAIT_BACKUP,				/* waiting for online backup mode to end */
	PM_WAIT_READONLY,			/* waiting for read only backends to exit */
	PM_WAIT_BACKENDS,			/* waiting for live backends to exit */
	PM_SHUTDOWN,				/* waiting for checkpointer to do shutdown
								 * ckpt */
	PM_SHUTDOWN_2,				/* waiting for archiver and walsenders to
								 * finish */
	PM_WAIT_DEAD_END,			/* waiting for dead_end children to exit */
	PM_NO_CHILDREN				/* all important children have exited */
} PMState;

这个枚举类型标注的是数据库当前的状态。其中PM_RUN是一个分水岭。从PM_INIT到PM_RUN,数据库逐渐从初始化状态转换为正常的运行状态。而从PM_RUN到PM_NO_CHILDREN,数据库逐渐由正常运行状态转换到可以关闭的状态。理解了这个有助于我们理解数据库的启动和关闭的时序。上面每个状态后面的注释已经能很好地解释每个状态间的转换条件了,我这里不赘述了。

对于pg_ctl的stop参数,我们有三种模式:

模式|发送的signal|signal的处理

  • | :-😐 -
    smart|SIGTERM|Wait for children to end their work, then shut down
    fast|SIGINT|Abort all children with SIGTERM (rollback active transactions and exit) and shut down when they are gone
    immediate|SIGQUIT|abort all children with SIGQUIT, wait for them to exit, terminate remaining ones with SIGKILL, then exit without attempt to properly shut down the database system.

这里我们就先以smart模式展开讨论,其他的模式其实也是类似的。

首先执行"pg_ctl stop -m smart",这个时候其实就是向postmaster发送了一个SIGTERM信号;

postmaster收到SIGTERM信号,触发pqsignal(SIGTERM, pmdie),调用pmdie()函数去处理SIGTERM信号;

pmdie
    |->SignalSomeChildren(SIGTERM,BACKEND_TYPE_AUTOVAC | BACKEND_TYPE_BGWORKER)
                                        向autovacuum和bgworker子进程转发SIGTERM信号
    |->PostmasterStateMachine()         更新数据库的状态PM_State

pmdie中的处理如上所示。pmdie调用SignalSomeChildren()向指定的进程发送SIGTERM信号,同样这些进程本身也有信号处理函数,在接收到postmaster的SIGTERM信号进行相关处理并终止。(子进程终止后会向父进程发送一个SIGCHLD信号,这是操作系统的固有处理)。PostmasterStateMachine()是一个工具函数,在postmaster很多的信号处理函数中都会调用该函数来根据数据库当前的PM_State和相关进程的死活来更新PM_State。

这个时候我们看backend进程:

backend:pqsignal(SIGTERM, die);   //die

backend进程本身的信号处理函数在收到SIGTERM信号后调用die函数进程exit处理。

话题再回到postmaster,当它收到子进程的SIGCHLD信号时,触发pqsignal(SIGCHLD, reaper),会调用reaper()函数处理子进程发来的SIGCHLD信号:

reaper()
    |->switch(PID)      根据PID类型判断子进程类型,分别进行处理
    |->PostmasterStateMachine()     更新数据库的状态PM_State

这样postmaster会一直收到子进程的SIGCHLD信号,并进行相应处理后更新PM_State。

那什么时候确定所有的子进程都结束了呢?还是看PostmasterStateMachine()函数:

PostmasterStateMachine() 
            当最后一个backend(dead_end)结束时,reaper处理子进程通过调用PostmasterStateMachine更新当前状态,
            将当前状态由PM_WAIT_DEAD_END转换为PM_NO_CHILDREN时:
            PM_WAIT_DEAD_END  -> pmState = PM_NO_CHILDREN
            说明说有子进程都已退出,postmaster调用ExitPostmaster结束自身:
            ExitPostmaster()


本节讨论就是这样,下次准备讨论:

后端process的管理
DB的shoutdown的处理
backend异常结束时的处理
BootstrapMain()的处理

先把flag立下来,免得自己忘了。欢迎大家点赞~

posted @ 2018-04-23 08:06  非我在  阅读(2869)  评论(1编辑  收藏  举报