今天我們對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立下來,免得自己忘了。歡迎大家點贊~