1. 程式人生 > >Postgresql - 原始碼 - autovacuum deamon

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 */

}