Postgresql - 原始碼 - autovacuum deamon
什麼是vacuum。在這一文章中就不多闡述了。我們來看一下autovacuum的程式碼。
程式碼中的註釋資訊 ( 程式碼位置 src/backend/postmaster/autovacuum.c ):
******************************************************************
autovacuum系統是由兩種不同的過程構成的:autovacuum launcher 和 autovacuum worker。launcher是一個總是執行的過程,當autovacuum GUC引數被設定時,由 postmaster 啟動。發射裝置安排 autovacuum workers 在適當的時候開始工作。workers 是執行實際 vacuuming 的過程;他們連線到啟動器中確定的資料庫,一旦連線,他們檢查目錄以選擇要 vacuum 的表。
autovacuum launcher 不能自己啟動輔助程序,因為這樣做會導致健壯性問題(即,在特殊情況下無法關閉它們,而且,由於launcher 連線到共享記憶體,因此受到損壞,它並不像健壯的 postmaster)。所以它把這個任務留給 postmaster。
有一個 autovacuum 共享記憶體區域,其中 launcher 儲存關於它想要清空的資料庫的資訊。當它想要一個新的 worker 啟動時,它在共享記憶體中設定一個標誌並向 postmaster 傳送一個訊號。然後 postmaster 只知道它必須開始一個 worker ,所以它 fork 一個新的子程序,變成一個 worker 。這個新的程序連線到共享記憶體,在那裡它可以檢查 launcher 已經建立的資訊。
如果 fork() 在 postmaster 中失敗,則在共享記憶體區域中設定一個標誌,並向launcher傳送一個訊號。 launcher 在注意到標誌後,可以通過重新發送訊號來嘗試再次啟動 worker 。請注意,故障只能是暫時的(由於高負載、記憶體壓力、太多的程序而導致的 fork 故障,等等);更持久的問題,如連線到資料庫的故障,將在 worker 稍後檢測到,並且僅通過讓 worker 正常退出來處理。launcher 將按時間表重新啟動新的 worker 。
當 worker 完成 vacuum 時,它將SIGUSR2傳送到launcher。然後,如果 schedule 安排太緊,以至於立即需要新的 worker ,launcher 就會醒來,並且能夠啟動另一個 worker 。此時,launcher 還可以平衡各種剩餘 worker 基於成本的 vacuum 延遲特性的設定。
注意,資料庫中可以同時存在多個 worker 。它們將把當前正在 vacuuming 的表儲存在共享記憶體中,以便其他 worker 避免被阻塞,等待該表的真空鎖。他們還會在 vacuuming 每個表之前重新載入pgstats資料,以避免 vacuuming 剛剛結束的表,該表被另一個 worker 清空,因此不再在共享記憶體中被記錄。但是,有一個視窗(由pgstat延遲引起),worker 可以在該視窗上選擇已經清空的表;這是當前設計中的一個bug。
******************************************************************
autovacuum launcher的主函式入口 StartAutoVacLauncher(void) ,在postmaster.c的ServerLoop(void)函式呼叫,或reaper(SIGNAL_ARGS)函式呼叫。
主函式很精簡,主要做了三件事情, InitPostmasterChild(),ClosePostmasterPorts(false), AutoVacLauncherMain(0, NULL)
第一個函式是為postmaster的子程序初始化基本環境,要在子程序啟動前被呼叫。
第二個函式是關閉所有開啟的套接字的postmaster,這在子程序啟動過程中被呼叫,以釋放該子程序不需要的檔案描述符。
第三個函式是真正的autovacuum launcher 程序的主函數了
int StartAutoVacLauncher(void)
{
pid_t AutoVacPID;
#ifdef EXEC_BACKEND
switch ((AutoVacPID = avlauncher_forkexec()))
#else
switch ((AutoVacPID = fork_process()))
#endif
{
case -1:
ereport(LOG,
(errmsg("could not fork autovacuum launcher process: %m")));
return 0;
#ifndef EXEC_BACKEND
case 0:
/* in postmaster child ... */
InitPostmasterChild();
/* Close the postmaster's sockets */
ClosePostmasterPorts(false);
AutoVacLauncherMain(0, NULL); # 主迴圈 autovacuum launcher 程序函式
break;
#endif
default:
return (int) AutoVacPID;
}
/* shouldn't get here */
return 0;
}
/* autovacuum launcher 程序函式 */
NON_EXEC_STATIC void
AutoVacLauncherMain(int argc, char *argv[]){
......
/* Identify myself via ps */ /* 初始化 */
init_ps_display(pgstat_get_backend_desc(B_AUTOVAC_LAUNCHER), "", "", "");
......
SetProcessingMode(InitProcessing); /* 程序檢查 */
/*
* Set up signal handlers. We operate on databases much like a regular
* backend, so we use the same signal handling. See equivalent code in
* tcop/postgres.c.
*/
/* 設定 signal handlers */
pqsignal(SIGHUP, av_sighup_handler);
......
pqsignal(SIGCHLD, SIG_DFL);
/* 初始化,InitCommunitcation(), DebugFileOpen(), InitFileAccess(), smgrinit(), InitBufferPoolAccess() */
BaseInit();
/* 在記憶體中建立了PGPROC,但是除了EXEC_BACKEND,所以在這裡必須要初始化程序。 */
#ifndef EXEC_BACKEND
InitProcess();
#endif
InitPostgres(NULL, InvalidOid, NULL, InvalidOid, NULL, false);
SetProcessingMode(NormalProcessing);
/* 建立一個記憶體上下文,避免在發生錯誤或出現問題的時候有記憶體洩漏。 */
AutovacMemCxt = AllocSetContextCreate(TopMemoryContext,
"Autovacuum Launcher",
ALLOCSET_DEFAULT_SIZES);
MemoryContextSwitchTo(AutovacMemCxt);
/* 如果遇到異常,則在此做恢復處理。此程式碼是PostgresMain 錯誤恢復的精簡版本。*/
if (sigsetjmp(local_sigjmp_buf, 1) != 0)
{
......
}
/* We can now handle ereport(ERROR) */
PG_exception_stack = &local_sigjmp_buf;
/* must unblock signals before calling rebuild_database_list */
PG_SETMASK(&UnBlockSig);
/* 設定是中安全的搜尋路徑。但是Launcher沒有連線到資料庫,所以沒有影響。 */
SetConfigOption("search_path", "", PGC_SUSET, PGC_S_OVERRIDE);
/* 在autovac過程中強制關閉zero_damaged_pages,即使它被設定在postgresql.conf中。我們真的不希望非互動地應用這種危險的選項。*/
SetConfigOption("zero_damaged_pages", "false", PGC_SUSET, PGC_S_OVERRIDE);
/* 強制設定超時關閉,以避免讓這些設定阻止定期維護被執行。 */
SetConfigOption("statement_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
SetConfigOption("lock_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
SetConfigOption("idle_in_transaction_session_timeout", "0", PGC_SUSET, PGC_S_OVERRIDE);
/* 強制 default_transaction_isolation 以讀取已提交的檔案。我們不想增加可序列化模式的開銷,也不增加任何導致死鎖或延遲其他事務的風險。 */
SetConfigOption("default_transaction_isolation", "read committed",
PGC_SUSET, PGC_S_OVERRIDE);
/* 在緊急模式下,只需啟動一個worker */
if (!AutoVacuumingActive())
{
....
}
AutoVacuumShmem->av_launcherpid = MyProcPid;
/* 建立初始資料庫列表。我們希望這個列表保持不變的是,它是通過減少 next_time 來排序的。一旦一個條目被更新到更高的時間,它將被移動到前面。 */
rebuild_database_list(InvalidOid);
/* 迴圈,直到資料庫shutdown 。*/
while (!got_SIGTERM)
{
......
/* 這個迴圈與 WaitLatch 的正常使用稍有不同,因為我們希望在第一次啟動子程序之前睡眠。所以是WaitLatch ,然後是ResetLatch ,然後檢查喚醒狀態。 */
launcher_determine_sleep(!dlist_is_empty(&AutoVacuumShmem->av_freeWorkers), false, &nap);
/*等待直到 naptime 到期或者我們得到某種型別的訊號(所有的訊號處理程式將通過呼叫SetLatch喚醒)。 */
rc = WaitLatch(MyLatch,
WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
(nap.tv_sec * 1000L) + (nap.tv_usec / 1000L),
WAIT_EVENT_AUTOVACUUM_MAIN);
ResetLatch(MyLatch);
/* 當休眠的時候,程序 sinval catchup 中斷 */
ProcessCatchupInterrupt();
/* 緊急補救,當postmaster 程序死掉了。這是為了避免對所有子程序進行手工清理。 */
if (rc & WL_POSTMASTER_DEATH)
proc_exit(1);
/* the normal shutdown case */
if (got_SIGTERM)
break;
if (got_SIGHUP)
{
got_SIGHUP = false;
ProcessConfigFile(PGC_SIGHUP);
/* shutdown requested in config file? */
if (!AutoVacuumingActive())
break;
/* rebalance in case the default cost parameters changed */
LWLockAcquire(AutovacuumLock, LW_EXCLUSIVE);
autovac_balance_cost();
LWLockRelease(AutovacuumLock);
/* rebuild the list in case the naptime changed */
rebuild_database_list(InvalidOid);
}
/* 一個worker完成,或postmaster未能啟動worker */
if (got_SIGUSR2)
{
got_SIGUSR2 = false;
/* rebalance cost limits, if needed */
if (AutoVacuumShmem->av_signal[AutoVacRebalance])
{
LWLockAcquire(AutovacuumLock, LW_EXCLUSIVE);
AutoVacuumShmem->av_signal[AutoVacRebalance] = false;
autovac_balance_cost();
LWLockRelease(AutovacuumLock);
}
if (AutoVacuumShmem->av_signal[AutoVacForkFailed])
{
/* 如果主程序啟動新的worker失敗,sleep一下並重新發送。新的worker狀態仍在記憶體中。之後,我們重新啟動主迴圈。我們應該限制我們重試的次數嗎?這是沒有意義的,因為worker 之後的啟動將繼續以同樣的方式失敗。 */
AutoVacuumShmem->av_signal[AutoVacForkFailed] = false;
pg_usleep(1000000L); /* 1s */
SendPostmasterSignal(PMSIGNAL_START_AUTOVAC_WORKER);
continue;
}
}
/* 在試圖啟動一個worker之前,需要做一些檢查。首先,我們需要確保有一個worker插槽可用。第二,我們需要確保沒有其他worker在啟動時失敗。*/
current_time = GetCurrentTimestamp();
LWLockAcquire(AutovacuumLock, LW_SHARED);
can_launch = !dlist_is_empty(&AutoVacuumShmem->av_freeWorkers);
if (AutoVacuumShmem->av_startingWorker != NULL)
{
int waittime;
WorkerInfo worker = AutoVacuumShmem->av_startingWorker;
/* 當一個worker 還在啟動時(或者在啟動時失敗),我們不能啟動另一個worker,所以只需要多sleep一會兒;一旦準備好,這個worker就會再次喚醒。然而,我們只會等待 autovacuum_naptime 秒(最多60秒)。注意,無法連線到特定資料庫在這裡不是問題,因為 worker 在嘗試連線之前將自己從 startingWorker 指標中移除。postmaster發現的問題也有不同的報告和處理方式。可能導致此程式碼啟動的唯一問題是,在worker從startingWorker 指標中移除 WorkerInfo 之前,AutoVacWorkerMain 早期部分中的錯誤。*/
waittime = Min(autovacuum_naptime, 60) * 1000;
if (TimestampDifferenceExceeds(worker->wi_launchtime, current_time,
waittime))
{
LWLockRelease(AutovacuumLock);
LWLockAcquire(AutovacuumLock, LW_EXCLUSIVE);
/* 沒有其他程序可以讓worker處於啟動模式,所以如果 startingWorker 在交換鎖之後仍然為INVALID,那麼我們假設它和上面看到的一樣(所以我們不重新檢查啟動時間) */
if (AutoVacuumShmem->av_startingWorker != NULL)
{
worker = AutoVacuumShmem->av_startingWorker;
worker->wi_dboid = InvalidOid;
worker->wi_tableoid = InvalidOid;
worker->wi_sharedrel = false;
worker->wi_proc = NULL;
worker->wi_launchtime = 0;
dlist_push_head(&AutoVacuumShmem->av_freeWorkers,
&worker->wi_links);
AutoVacuumShmem->av_startingWorker = NULL;
elog(WARNING, "worker took too long to start; canceled");
}
}
else
can_launch = false;
}
LWLockRelease(AutovacuumLock); /* either shared or exclusive */
/*什麼都做不了的時候,sleep */
if (!can_launch)
continue;
/* We're OK to start a new worker */
if (dlist_is_empty(&DatabaseList))
{
/* 當列表為空的特殊情況下:立即啟動一個worker 。這覆蓋了初始情況,當沒有資料庫在 pgstats S中(因此列表是空的)。注意,launcher_determine_sleep 中的約束使我們不能太快地開始工作(最多一次是在列表為空時autovacuum_naptime)。 */
launch_worker(current_time);
}
else
{
/* 因為rebuild_database_list首先用最遠的adl_next_worker構造一個列表,所以我們從列表的尾部獲得資料庫。*/
avl_dbase *avdb;
avdb = dlist_tail_element(avl_dbase, adl_node, &DatabaseList);
/* 啟動一個worker 無論 next_worker是現在還是以前 */
if (TimestampDifferenceExceeds(avdb->adl_next_worker,
current_time, 0))
launch_worker(current_time);
}
}
/* 正常退出 */
shutdown:
ereport(DEBUG1,
(errmsg("autovacuum launcher shutting down")));
AutoVacuumShmem->av_launcherpid = 0;
proc_exit(0); /* done */
}