总结一下:
/* Perform a checkpoint --- either during shutdown, or on-the-fly
* 创建checkpoint —— 不管是在shutdown过程还是在运行中
* 如果不是shutdown过程,那么我们会创建一个在线检查点(online checkpoint)。这是一个非常特别的操作类型和WAL记录,因为检查点动作(实际上)发生在一段时间内,而在逻辑上只发生在一个LSN上。
* WAL记录(redo ptr,即前面介绍的重做点)的逻辑位置早于或等于物理位置。在replay WAL时,我们通过它的物理位置定位检查点,然后读取redo ptr,实际上开始replay是在更早的逻辑位置。由于我们不向逻辑位置的WAL写任何东西,因此这个位置可以是任意类型的WAL记录。
* 以上机制的目的是,让我们在checkpoint时可以继续工作。导致的问题是,操作的时间会比较长,尤其在繁忙的系统中,该函数可能会持续数分钟。
*/
void
CreateCheckPoint(int flags)
{
bool shutdown; // 是否在shutdown过程中
CheckPoint checkPoint;//checkpoint
XLogRecPtr recptr; //XLOG Record位置
XLogSegNo _logSegNo; // 日志段号
XLogCtlInsert *Insert = &XLogCtl->Insert; //用于更新控制文件
uint32 freespace; // 空闲空间
XLogRecPtr PriorRedoPtr;// 上一个重做点(redo位置)
XLogRecPtr curInsert; // 当前插入的位置
XLogRecPtr last_important_lsn; //上一个重要的LSN
VirtualTransactionId *vxids; //虚拟事务ID
int nvxids;
/*
* 如果是end-of-recovery checkpoint或者shutdown checkpoint,将shutdown标记设为true
*/
if (flags & (CHECKPOINT_IS_SHUTDOWN | CHECKPOINT_END_OF_RECOVERY))
shutdown = true;
else
shutdown = false;
/* sanity check,如果在恢复阶段,报错创建检查点失败 */
if (RecoveryInProgress() && (flags & CHECKPOINT_END_OF_RECOVERY) == 0)
elog(ERROR, "can't create a checkpoint during recovery");
/*
* 进入临界区(critical section)前,执行初始化 InitXLogInsert。
* 通常这个操作在第一次调用RecoveryInProgress()或 LocalSetXLogInsertAllowed()时已完成,然而在创建end-of-recovery checkpoint时,LocalSetXLogInsertAllowed 会在下面的临界区中完成调用,但InitXLogInsert不能在临界区中调用。
*/
InitXLogInsert();
/*
* Prepare to accumulate statistics.
*/
MemSet(&CheckpointStats, 0, sizeof(CheckpointStats));
CheckpointStats.ckpt_start_t = GetCurrentTimestamp();
/*
* Use a critical section to force system panic if we have trouble.
* 启用临界区功能:当创建检查点异常,使系统直接进入panic状态
*/
START_CRIT_SECTION();
/*
* 如果shutdown标记为true,拿锁,更新控制文件信息
*/
if (shutdown)
{
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->state = DB_SHUTDOWNING;
ControlFile->time = (pg_time_t) time(NULL);
UpdateControlFile();
LWLockRelease(ControlFileLock);
}
/*
* 告知smgr准备好,要开始建检查点了。在我们决定 redo pointer(重做点)前必须执行这个操作。
* 注意,如果后面判断不需要建检查点,smgr也不会执行任何回滚操作。
*/
SyncPreCheckpoint();
/* Begin filling in the checkpoint WAL record,内存申请,开始填充检查点信息 */
MemSet(&checkPoint, 0, sizeof(checkPoint));
checkPoint.time = (pg_time_t) time(NULL);
/*
* For Hot Standby, derive the oldestActiveXid before we fix the redo
* pointer. This allows us to begin accumulating changes to assemble our
* starting snapshot of locks and transactions. 对于从库的非shutdown检查点,在修改redo pointer前,计算出最旧的活跃事务id(oldestActiveXid),这让我们可以记录变化,用于组装锁和事务的开始快照。否则,先直接记一个invalid事务id即可。
*/
if (!shutdown && XLogStandbyInfoActive())
checkPoint.oldestActiveXid = GetOldestActiveTransactionId();
else
checkPoint.oldestActiveXid = InvalidTransactionId;
/*
* 在获取WAL插入锁前(GetLastImportantRecPtr函数也会获取WAL锁), 获取最近一次重要的XLOG记录位置
*/
last_important_lsn = GetLastImportantRecPtr();
/*
* We must block concurrent insertions while examining insert state to
* determine the checkpoint REDO pointer. 获取WAL插入排他锁,在检查插入状态以确定检查点REDO pointer时,必须阻塞并发插入操作
*/
WALInsertLockAcquireExclusive();
curInsert = XLogBytePosToRecPtr(Insert->CurrBytePos);
/*
* 如果不是shutdown或强制检查点,并且没有活跃WAL请求检查点,跳过检查点创建。避免在系统空闲时,创建重复的检查点。
*/
if ((flags & (CHECKPOINT_IS_SHUTDOWN | CHECKPOINT_END_OF_RECOVERY |
CHECKPOINT_FORCE)) == 0)
{
if (last_important_lsn == ControlFile->checkPoint) // 没有重要的修改操作
{
WALInsertLockRelease();
END_CRIT_SECTION();
ereport(DEBUG1,
(errmsg_internal("checkpoint skipped because system is idle")));
return;
}
}
下面是一些时间线和全页写相关的设置
/*
* 在任何进程被允许写入WAL之前,必须先创建 end-of-recovery检查点。为了允许我们写检查点记录,临时启用XLogInsertAllowed(也确保了在这里和AdvanceXLInsertBuffer函数中会用到的ThisTimeLineID已经初始化)
*/
if (flags & CHECKPOINT_END_OF_RECOVERY)
LocalSetXLogInsertAllowed();
checkPoint.ThisTimeLineID = ThisTimeLineID;
if (flags & CHECKPOINT_END_OF_RECOVERY)
checkPoint.PrevTimeLineID = XLogCtl->PrevTimeLineID;
else
checkPoint.PrevTimeLineID = ThisTimeLineID;
checkPoint.fullPageWrites = Insert->fullPageWrites;
把本次检查点开始时的日志插入点(Insert->CurrBytePos)记录到RedoRecPtr变量中,这样在故障恢复时,就可以以此为起点。这两句在比较前面,这里重新列一下,比较方便看:
WALInsertLockAcquireExclusive();
curInsert = XLogBytePosToRecPtr(Insert->CurrBytePos);
// 如果当前页面没有空闲空间,推进到下一页面
freespace = INSERT_FREESPACE(curInsert);
if (freespace == 0)
{
// 是否为日志段中第一个页面
if (XLogSegmentOffset(curInsert, wal_segment_size) == 0)
curInsert += SizeOfXLogLongPHD;
else
curInsert += SizeOfXLogShortPHD;
}
// 记录本次检查点开始的LSN
checkPoint.redo = curInsert;
/*
* Here we update the shared RedoRecPtr for future XLogInsert calls; this
* must be done while holding all the insertion locks.更新RedoRecPtr值,为后续XLogInsert调用做准备,这步操作必须在持有所有insertion locks时完成
*/
RedoRecPtr = XLogCtl->Insert.RedoRecPtr = checkPoint.redo;
/*
* Now we can release the WAL insertion locks, allowing other xacts to
* proceed while we are flushing disk buffers.将日志写入磁盘缓存后,释放WAL insertion locks,允许其他事务继续工作
*/
WALInsertLockRelease();
/* Update the info_lck-protected copy of RedoRecPtr as well,加锁,更新控制文件RedoRecPtr信息 */
SpinLockAcquire(&XLogCtl->info_lck);
XLogCtl->RedoRecPtr = checkPoint.redo;
SpinLockRelease(&XLogCtl->info_lck);
下面是 log_checkpoints 的处理和 checkPoint 结构体信息的填充。
/*
* 如果启用了log_checkpoints参数(检查点完成时记录到pg日志中),则开始记录日志。将它推迟到现在的原因是:如果前面决定跳过检查点,这里则不记录任何信息。
*/
if (log_checkpoints)
LogCheckpointStart(flags, false);
/* Update the process title */
update_checkpoint_display(flags, false, false);
/*
* Get the other info we need for the checkpoint record.
* 获取checkpoint其他字段的信息
* 我们不需要在检查点中保存oldestClogXid,它只在截断clog的短时间内起作用,
* 如果数据库在此期间崩溃,我们将重新截断clog并修复oldestClogXid。
*/
LWLockAcquire(XidGenLock, LW_SHARED);
checkPoint.nextXid = ShmemVariableCache->nextXid;
checkPoint.oldestXid = ShmemVariableCache->oldestXid;
checkPoint.oldestXidDB = ShmemVariableCache->oldestXidDB;
LWLockRelease(XidGenLock);
LWLockAcquire(CommitTsLock, LW_SHARED);
checkPoint.oldestCommitTsXid = ShmemVariableCache->oldestCommitTsXid;
checkPoint.newestCommitTsXid = ShmemVariableCache->newestCommitTsXid;
LWLockRelease(CommitTsLock);
LWLockAcquire(OidGenLock, LW_SHARED);
checkPoint.nextOid = ShmemVariableCache->nextOid;
if (!shutdown)
checkPoint.nextOid += ShmemVariableCache->oidCount;
LWLockRelease(OidGenLock);
MultiXactGetCheckptMulti(shutdown,
&checkPoint.nextMulti,
&checkPoint.nextMultiOffset,
&checkPoint.oldestMulti,
&checkPoint.oldestMultiDB);
/*
* 填充完checkpoint字段信息后,确保所有shmem disk buffers和commit-log buffers都已经被刷到磁盘中。
* 刷盘的IO操作可能会因为各种原因失败。当刷盘失败时,检查点创建会失败,但没有理由强制要求系统panic。因此,在刷盘前要退出临界区。
*/
END_CRIT_SECTION();
…
CheckPointGuts(checkPoint.redo, flags);
/*
* 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.更新控制文件中的unloggedLSN值。
*/
SpinLockAcquire(&XLogCtl->ulsn_lck);
ControlFile->unloggedLSN = XLogCtl->unloggedLSN;
SpinLockRelease(&XLogCtl->ulsn_lck);
UpdateControlFile();
LWLockRelease(ControlFileLock);
/* Update shared-memory copy of checkpoint XID/epoch */
SpinLockAcquire(&XLogCtl->info_lck);
XLogCtl->ckptFullXid = checkPoint.nextXid;
SpinLockRelease(&XLogCtl->info_lck);
下面是一些收尾准备工作
/*
* We are now done with critical updates; no need for system panic if we
* have trouble while fooling with old log segments.
*/
END_CRIT_SECTION();
/*
* Let smgr do post-checkpoint cleanup (eg, deleting old files).告知smgr做检查点后的清理工作,例如删除旧文件
*/
SyncPostCheckpoint();
/*
* Update the average distance between checkpoints if the prior checkpoint exists.
如果前一个检查点存在,更新检查点之间的平均距离
*/
if (PriorRedoPtr != InvalidXLogRecPtr)
/* 估算两次checkpoint之间产生的xlog量,如果上次估算量比这次实际产生的要小,则将估算值更新为这次产生的量。否则,按照指定算法增加CheckPointDistanceEstimate */
UpdateCheckPointDistanceEstimate(RedoRecPtr - PriorRedoPtr);
/* 获取redo点的日志段号,作为最旧的需要保留的_logSegNo */
XLByteToSeg(RedoRecPtr, _logSegNo, wal_segment_size);
/* 根据max_slot_wal_keep_size和wal_keep_size两个参数设置,再次调整最旧的需要保留的_logSegNo */
KeepLogSeg(recptr, &_logSegNo);
/* 如果_logSegNo是已经过时的复制槽,需要重新计算 */
if (InvalidateObsoleteReplicationSlots(_logSegNo))
{
/*
* Some slots have been invalidated; recalculate the old-segment
* horizon, starting again from RedoRecPtr.
*/
XLByteToSeg(RedoRecPtr, _logSegNo, wal_segment_size);
KeepLogSeg(recptr, &_logSegNo);
}
/* 前面_logSegNo是最旧的需要保留的段号,因此减1则是最新的可以删除的段号 */
_logSegNo--;
/* 删除已无用的日志文件 */
RemoveOldXlogFiles(_logSegNo, RedoRecPtr, recptr);
收尾工作
/*
* Make more log segments if needed. (Do this after recycling old log segments, since that may supply some of the needed files.) 如果有需要,则创建更多日志段,回收旧日志段后再执行这一步(可以复用旧空间)。
*/
if (!shutdown)
PreallocXlogFiles(recptr);
/*
* Truncate pg_subtrans if possible. 如果不是恢复阶段,则清空pg_subtrans,因为检查点之前的事务都已经提交了,不再需要保留。
*/
if (!RecoveryInProgress())
TruncateSUBTRANS(GetOldestTransactionIdConsideredRunning());
/* Real work is done; log and update stats. 记录检查点完成日志 */
LogCheckpointEnd(false);
/* Reset the process title */
update_checkpoint_display(flags, false, true);
TRACE_POSTGRESQL_CHECKPOINT_DONE(CheckpointStats.ckpt_bufs_written,
NBuffers,
CheckpointStats.ckpt_segs_added,
CheckpointStats.ckpt_segs_removed,
CheckpointStats.ckpt_segs_recycled);
}
① vscode 设置断点函数 CreateCheckPoint
② 跟踪checkpointer进程
③ psql执行checkpoint命令
刚进来的位置跟之前是一样的
会直接运行到新断点(有点慢,要等一会),然后点击单步调试,进入函数中查看。
判断是否为shutdown检查点,设置标记。是否在recover过程中,若是则报错。
获取最旧的活跃事务id(oldestActiveXid),这里不是从库,先直接记一个invalid事务id即可。
Checkpoint skipped机制
如果不是shutdown或强制检查点,并且没有活跃WAL请求检查点,跳过检查点创建。避免在系统空闲时,创建重复的检查点。
检查点结构体中时间线和全页写的设置
确定redo点
释放WALInsertLock,允许其他事务继续工作
获取SpinLock,更新控制文件RedoRecPtr信息,然后释放
如果启用了log_checkpoints参数(检查点完成时记录到pg日志中),则开始记录日志
获取checkpoint其他字段的信息
退出临界区
刷盘函数
…省略一部分从库相关代码
更新控制文件信息
一些收尾工作
删除无用的日志文件
如果前一个检查点存在,更新检查点之间的平均距离
计算并删除旧日志文件(最近一次检查点之前的日志文件均可以删除)
如果有需要,则创建更多日志段,回收旧日志段后再执行这一步(可以复用旧空间)
如果不是恢复阶段,则清空pg_subtrans,因为检查点之前的事务都已经提交了,不再需要保留。
记录检查点完成日志,以及一些收尾工作
函数执行完毕~
主要负责将脏页刷入磁盘。除了共享缓冲区中的脏页,还包括其他需要落盘的数据。
/*
* Flush all data in shared memory to disk, and fsync
*
* This is the common code shared between regular checkpoints and
* recovery restartpoints.
*/
static void
CheckPointGuts(XLogRecPtr checkPointRedo, int flags)
{
CheckPointRelationMap(); // 保证在检查点开始前,所有Relation Map都已刷盘
CheckPointReplicationSlots(); // 把日志复制使用的slot刷入磁盘
CheckPointSnapBuild(); // 移除无用的快照信息
CheckPointLogicalRewriteHeap();
CheckPointReplicationOrigin(); // 逻辑日志的Origin信息
/* Write out all dirty data in SLRUs and the main buffer pool */
TRACE_POSTGRESQL_BUFFER_CHECKPOINT_START(flags);
CheckpointStats.ckpt_write_t = GetCurrentTimestamp(); //填充记录本次检查点自身信息的CheckpointStats,设置检查点写入时间
CheckPointCLOG(); // 刷新事务提交日志
CheckPointCommitTs();// 刷新事务提交时间日志
CheckPointSUBTRANS(); // 刷新子事务日志
CheckPointMultiXact(); // 刷新元组事务状态日志信息
CheckPointPredicate(); // SSI判断中需要记录的已提交事务信息
CheckPointBuffers(flags); // 刷入主缓冲区中的脏页
/* Perform all queued up fsyncs */
TRACE_POSTGRESQL_BUFFER_CHECKPOINT_SYNC_START();
CheckpointStats.ckpt_sync_t = GetCurrentTimestamp(); //设置检查点sync开始时间
ProcessSyncRequests();
CheckpointStats.ckpt_sync_end_t = GetCurrentTimestamp();//设置检查点sync结束时间
TRACE_POSTGRESQL_BUFFER_CHECKPOINT_DONE();
/* We deliberately delay 2PC checkpointing as long as possible */
CheckPointTwoPhase(checkPointRedo); // 两阶段提交时将其相关事务信息落盘
}
参考
《PostgreSQL技术内幕:事务处理深度探索》第4章
PostgreSQL 源码解读(115)- 后台进程#3(checkpointer进程#2)_ITPUB博客