PostgreSQL的checkpoint簡析

語言: CN / TW / HK


作者:楊向博


一、Checkpoint簡介


官方文檔對於checkpoint的描述:


Checkpoints are points in the sequence of transactions at which it is guaranteed that the heap and index data files have been updated with all information written before that checkpoint.
At checkpoint time, all dirty data pages are flushed to disk and a special checkpoint record is written to the log file. (The change records were previously flushed to the WAL files.) 
In the event of a crash, the crash recovery procedure looks at the latest checkpoint record to determine the point in the log (known as the redo record) from which it should start the REDO operation.
Any changes made to data files before that point are guaranteed to be already on disk.
Hence, after a checkpoint, log segments preceding the one containing the redo record are no longer needed and can be recycled or removed. (When WAL archiving is being done, the log segments must be archived before being recycled or removed.)


簡單來説,checkpoint就是一個事務順序的記錄點,checkpoint主要是進行刷髒頁,redo時會參考checkpoint進行日誌回放。除了刷髒之外還會更新一些位點信息,清理一些不再需要的wal。


下圖分為part1-4,4個部分描述checkpoint的觸發條件,以及觸發後進行的操作等。



二、Checkpoint的觸發條件


如圖Part1:


在PostgreSQL中Checkpoint是由checkpointer進程執行的,大致的邏輯是這樣子的。Checkpointer進程的主流程是一個無條件的for循環,在未觸發checkpoint時一直在WaitLatch中sleep,也就是在epoll_wait中觀察list鏈表,查看是否有事件句柄已經就緒(某個條件在觸發checkpoint);


如果已經存在就緒事件,則wake up(通過SetLatch中write pipe的方式wake up),執行checkpoint。


哪些條件會觸發checkpoint呢?


Checkpoint是由一些flag來觸發的,這些flag並不只是單獨作用,大多情況下是根據場景多個flag進行或運算組合為ckpt_flags


根據觸發方式flag可以分為兩種:


1、checkpointer進程本身通過checkpoint_timeout觸發


#define CHECKPOINT_CAUSE_TIME    0x0100    /* Elapsed time */


2、其他進程向checkpointer發送信號觸發:


#define CHECKPOINT_IS_SHUTDOWN    0x0001    /* Checkpoint is for shutdown */
主要場景:數據庫shutdown時


其它進程調用RequestCheckpoint向checkpointer進程發送SIGINT信號觸發


如圖Part2:


Step1:修改共享內存CheckpointerShmem->ckpt_flags,傳入對應的flags

Step2:向checkpointer進程發送SIGINT信號,喚醒進程


#define CHECKPOINT_END_OF_RECOVERY    0x0002    /* Like shutdown checkpoint, but  issued at end of WAL recovery */
主要場景:startup進程StartupXlog完成時


#define CHECKPOINT_IMMEDIATE    0x0004    /* Do it without delays */
主要場景:當postgres為standalone backend模式請求checkpoint時;Basebackup執行備份時


#define CHECKPOINT_FORCE        0x0008    /* Force even if no activity */
主要場景:手動執行checkpoint命令;standby實例進行promote時


#define CHECKPOINT_FLUSH_ALL    0x0010    /* Flush all pages, including those belonging to unlogged tables */
主要場景:drop database或者create database後


#define CHECKPOINT_CAUSE_XLOG    0x0040    /* XLOG consumption */
主要場景:wal新增數量大於等於CheckPointSegments – 1時,默認參數下大致是42。

在9.5後CheckPointSegments不再是一個單獨參數,根據max_wal_size_mb和checkpoint_completion_target參數聯動。
CalculateCheckpointSegments函數中計算CheckPointSegments = max_wal_size_mb/(wal_segment_size/(1024*1024))/(1.0 + CheckPointCompletionTarget)
                      = 1024 / (16777216/(1024*1024))/1.5 ≈ 43
XLogCheckpointNeeded函數中判斷新增wal數量大於等於CheckPointSegments – 1, 滿足時函數返回true,表示需要進行checkpoint。
有時checkpoint比較頻繁會提示需要增大max_wal_size,根據計算公式,被除數max_wal_size越大,則CheckPointSegments越大,checkpoint的間隔就越大。


三、Checkpoint會做什麼


如圖Part4:


表示的是checkpoint觸發後,createcheckpoint實際的工作內容


1、Flush Dirty Pages,刷髒,這裏不展開了;

2、Update some points,更新XlogCtl和ControlFile,並持久化至pg_control文件;


    /*
     * Update the control file.
     */
    LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
    if (shutdown)
        ControlFile->state = DB_SHUTDOWNED;
    ControlFile->checkPoint = ProcLastRecPtr;
    ControlFile->checkPointCopy = checkPoint;
    ControlFile->time = (pg_time_t) time(NULL);
    /* crash recovery should always recover to the end of WAL */
    ControlFile->minRecoveryPoint = InvalidXLogRecPtr;
    ControlFile->minRecoveryPointTLI = 0;
    /*
     * Persist unloggedLSN value. It's reset on crash recovery, so this goes
     * unused on non-shutdown checkpoints, but seems useful to store it always
     * for debugging purposes.
     */
    SpinLockAcquire(&XLogCtl->ulsn_lck);
    ControlFile->unloggedLSN = XLogCtl->unloggedLSN;
    SpinLockRelease(&XLogCtl->ulsn_lck);
    /*更新pg_control文件*/
    UpdateControlFile();
    LWLockRelease(ControlFileLock);


3、Remove old wal,計算兩次checkpoint間的wal數量進行回收重用,並清理不再需要的wal


/*
     * Update the average distance between checkpoints if the prior checkpoint
     * exists.
     */
    if (PriorRedoPtr != InvalidXLogRecPtr)
     /*根據ptr偏移量,預估出兩次checkpoint間產生的wal量CheckPointDistanceEstimate*/
        UpdateCheckPointDistanceEstimate(RedoRecPtr - PriorRedoPtr);
    /*
     * Delete old log files, those no longer needed for last checkpoint to
     * prevent the disk holding the xlog from growing full.
     */
    XLByteToSeg(RedoRecPtr, _logSegNo, wal_segment_size);
/*根據min{wal_keep_segments, min(replication_slot.restart_lsn)}計算出_logSegNo,比_logSegNo早的日誌後續將會被清理掉*/
    KeepLogSeg(recptr, &_logSegNo);
    _logSegNo--;
/*首先根據CheckPointDistanceEstimate 結合一套公式,計算出開始回收重用的recycleSegNo,從這個日誌開始回收重用(wal_recycle默認開啟,主要是保留日誌並rename為新的序列號,回收一個序列號加一)*/
/*然後將_logSegNo之前並已經歸檔(如果開啟歸檔)的wal都清理掉*/
    RemoveOldXlogFiles(_logSegNo, RedoRecPtr, recptr);


這裏就能解釋,為什麼在沒有任何異常的情況下,wal實際保留個數總是大於wal_keep_segments,在remove old wal時已經recycle了一部分了。


四、checkpoint skipped機制


每次Checkpoint都會進行刷髒、清理wal?


如圖Part3:


並不是,當system idle時觸發checkpoint,會進入checkpoint skipped邏輯,函數中直接return,跳過刷髒、清理wal等步驟;這裏將這個機制描述為checkpoint skipped。


System idle:這裏可以理解為上次到本次checkpoint之間沒有wal寫入


看下這裏的邏輯:


/*
     * If this isn't a shutdown or forced checkpoint, and if there has been no
     * WAL activity requiring a checkpoint, skip it.  The idea here is to
     * avoid inserting duplicate checkpoints when the system is idle.
     */
    if ((flags & (CHECKPOINT_IS_SHUTDOWN | CHECKPOINT_END_OF_RECOVERY |
                  CHECKPOINT_FORCE)) == 0)
    {
        if (last_important_lsn == ControlFile->checkPoint)
        {
            WALInsertLockRelease();
            LWLockRelease(CheckpointLock);
            END_CRIT_SECTION();
            ereport(DEBUG1,
                    (errmsg('
checkpoint skipped because system is idle')));
            return;
        }
    }


在CreateCheckPoint時,如果checkpointFlag不是CHECKPOINT_FORCE(手動執行checkpoint)或者CHECKPOINT_IS_SHUTDOWN,當滿足last_important_lsn == ControlFile->checkPoint時,則直接return(當日志級別大於等於DEBUG1會打印checkpoint skipped信息),不進行後續操作。


着重來看if條件的左右值:


1. last_important_lsn


last_important_lsn = WALInsertLocks[lockno].l.lastImportantAt


在寫wal時,XLogInsertRecord函數中更新lastImportantAt為wal開始寫入的location


WALInsertLocks[lockno].l.lastImportantAt = StartPos;


2. ControlFile->checkPoint


共享內存成員ControlFile->checkPoint的更新位於checkpoint skipped代碼塊之後,在完成checkpoint操作後,會更新ControlFile並進行持久化(寫入pg_control文件)


ControlFile->checkPoint = ProcLastRecPtr;


這裏兩個變量等值成立的條件大概又可能是什麼?controlfile.checkpoint讀取的是上次checkpoint完成後的值,wal寫入點是當前正在寫wal的位置,那麼就是説wal寫入點一直未更新,也就是説數據庫未進行寫操作。


當兩次checkpoint間沒有寫操作時,刷髒和清理wal都是不需要的,看起來checkpoint skipped機制是比較合理的。


不過,在特定場景下,還是有些隱患的,需要手動維護下。


特殊場景:


實例持續大併發數據寫入,wal歸檔速度相對較慢,一段時間後停止寫入。這時可能會發現wal累積的比較多,甚至遠超於保留策略範圍,導致磁盤容量告急。由於後續沒有寫wal的操作,因此每次checkpoint_timeout觸發checkpoint後,會進入checkpoint skipped機制,一直不會清理wal,哪怕是歸檔已經完成。


這個時候就需要手動做一次checkpoint,也就是CHECKPOINT_FORCE的方式觸發,是不會進入checkpoint skipped機制的。


五、如何記錄checkpoint


打開checkpoint日誌,設置log_checkpoints=on;


當觸發checkpoint時pglog中會記錄兩條信息:


一條記錄觸發的flag,由LogCheckpointStart函數完成。


/*
 * Log start of a checkpoint.
 */
static void
LogCheckpointStart(int flags, bool restartpoint)
{
    elog(LOG, '%s starting:%s%s%s%s%s%s%s%s',
         restartpoint ? 'restartpoint' : 'checkpoint',
         (flags & CHECKPOINT_IS_SHUTDOWN) ? ' shutdown' : '',
         (flags & CHECKPOINT_END_OF_RECOVERY) ? ' end-of-recovery' : '',
         (flags & CHECKPOINT_IMMEDIATE) ? ' immediate' : '',
         (flags & CHECKPOINT_FORCE) ? ' force' : '',
         (flags & CHECKPOINT_WAIT) ? ' wait' : '',
         (flags & CHECKPOINT_CAUSE_XLOG) ? ' wal' : '',
         (flags & CHECKPOINT_CAUSE_TIME) ? ' time' : '',
         (flags & CHECKPOINT_FLUSH_ALL) ? ' flush-all' : '');
}


另外一條記錄checkpoint做了什麼,刷了多少髒塊,新增/清理/回收了多少wal等,由LogCheckpointEnd函數完成。


/*
 * Log end of a checkpoint.
 */
static void
LogCheckpointEnd(bool restartpoint)
{
    /* ............*/
    elog(LOG, '%s complete: wrote %d buffers (%.1f%%); '
         '%d WAL file(s) added, %d removed, %d recycled; '
         'write=%ld.%03d s, sync=%ld.%03d s, total=%ld.%03d s; '
         'sync files=%d, longest=%ld.%03d s, average=%ld.%03d s; '
         'distance=%d kB, estimate=%d kB',
         restartpoint ? 'restartpoint' : 'checkpoint',
         CheckpointStats.ckpt_bufs_written,
         (double) CheckpointStats.ckpt_bufs_written * 100 / NBuffers,
         CheckpointStats.ckpt_segs_added,
         CheckpointStats.ckpt_segs_removed,
         CheckpointStats.ckpt_segs_recycled,
         write_secs, write_usecs / 1000,
         sync_secs, sync_usecs / 1000,
         total_secs, total_usecs / 1000,
         CheckpointStats.ckpt_sync_rels,
         longest_secs, longest_usecs / 1000,
         average_secs, average_usecs / 1000,
         (int) (PrevCheckPointDistance / 1024.0),
         (int) (CheckPointDistanceEstimate / 1024.0));
}


例如這次由CHECKPOINT_CAUSE_XLOG觸發的checkpoint記錄:


2021-10-10 20:55:08.044 CST,,,10801,,615da38b.2a31,41,,2021-10-06 21:24:27 CST,,0,LOG,00000,'checkpoint starting: wal',,,,,,,,,''
2021-10-10 20:55:18.058 CST,,,10801,,615da38b.2a31,42,,2021-10-06 21:24:27 CST,,0,LOG,00000,'checkpoint complete: wrote 5776 buffers (35.3%); 0 WAL file(s) added, 0 removed, 41 recycled; write=9.147 s, sync=0.565 s, total=10.013 s; sync files=7, longest=0.333 s, average=0.080 s; distance=691976 kB, estimate=691976 kB',,,,,,,,,''


如果開啟了log_checkpoints,日誌中並未記錄checkpoint信息,大概率是觸發了checkpoint skipped機制,可以將log_min_messages配置為debug1,觀察日誌是否打印’checkpoint skipped because system is idle’。


六、checkpoint是否正常


1、可以通過系統函數查看執行時間等


Nick postgres=# select * from pg_control_checkpoint();
-[ RECORD 1 ]--------+-------------------------
checkpoint_lsn       | 18/39FD6C88
redo_lsn             | 18/39FD6C50
redo_wal_file        | 000000010000001800000039
timeline_id          | 1
prev_timeline_id     | 1
full_page_writes     | t
next_xid             | 0:1927987
next_oid             | 51061
next_multixact_id    | 1
next_multi_offset    | 0
oldest_xid           | 479
oldest_xid_dbid      | 1
oldest_active_xid    | 1927987
oldest_multi_xid     | 1
oldest_multi_dbid    | 1
oldest_commit_ts_xid | 0
newest_commit_ts_xid | 0
checkpoint_time      | 2021-10-10 22:15:33+08


2、pg_controldata 工具解析pg_control文件,根據結果分析


3、pstack,gdb,strace觀察checkpointer進程是否正常



預告 | 2021 PG亞洲大會12月與您相約
PG ACE計劃的正式發佈
三期PostgreSQL國際線上沙龍活動的舉辦
六期PostgreSQL國內線上沙龍活動的舉辦

中國PostgreSQL分會與騰訊雲戰略合作協議簽訂

中國PostgreSQL分會與美創科技戰略合作協議簽訂
中國PostgreSQL分會與中軟國際戰略合作協議簽訂
中國PostgreSQL分會“走進”北京大學
中國PostgreSQL分會“走進”深圳大學
PGFans社區核心用户點亮計劃

PostgreSQL 14.0 正式發佈

深度報告:開源協議那些事兒

從“非主流”到“潮流”,開源早已值得擁有

Oracle中國正在進行新一輪裁員,傳 N+6 補償

PostgreSQL與MySQL版權比較

新聞|Babelfish使PostgreSQL直接兼容SQL Server應用程序

四年三冠,PostgreSQL再度榮獲“年度數據庫”

中國PostgreSQL分會入選工信部重點領域人才能力評價機構


更多新聞資訊行業動態技術熱點,請關注中國PostgreSQL分會官方網站

https://www.postgresqlchina.com

中國PostgreSQL分會生態產品

https://www.pgfans.cn

中國PostgreSQL分會資源下載站

https://www.postgreshub.cn


點贊在看分享收藏

本文分享自微信公眾號 - 開源軟件聯盟PostgreSQL分會(kaiyuanlianmeng)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閲讀的你也加入,一起分享。