postgresql源码学习(32)—— 检查点④-核心函数CreateCheckPoint

67 篇文章 52 订阅
34 篇文章 3 订阅

一、 CreateCheckPoint会做什么?

  • 最主要的其实就是前面提到的检查点的3大作用。
  • 由于很多检查点相关的信息是记录在控制文件中的,因此该函数还要负责更新控制文件中检查点信息。
  • 如果数据库中没有重要的更新,则代表当前数据库空闲,可以跳过检查点。

总结一下:

  • 脏页刷入
  • 删除旧WAL:创建检查点后此位置之前的WAL可以删除
  • 故障恢复起点
  • 更新控制文件中检查点信息
  • Checkpoint skipped机制

二、 CreateCheckPoint函数

1. 准备工作

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

2. Checkpoint skipped机制

/*
* 如果不是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;

3. 确定故障恢复起点(redo点)

       把本次检查点开始时的日志插入点(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();
…

4. 脏页刷盘函数

	CheckPointGuts(checkPoint.redo, flags);

5. 更新控制文件信息

/*
	 * 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();

6. 删除无用的日志文件

/*
     * 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);
}

三、 代码调试

1. 调试方法

① vscode 设置断点函数 CreateCheckPoint

② 跟踪checkpointer进程

  • ps -ef|grep checkpointer
  • vscode跟踪5513进程

 ③ psql执行checkpoint命令

刚进来的位置跟之前是一样的

 ④ 手动加一个断点,点击继续

会直接运行到新断点(有点慢,要等一会),然后点击单步调试,进入函数中查看。

2. 调试过程

判断是否为shutdown检查点,设置标记。是否在recover过程中,若是则报错。

  • 初始化 InitXLogInsert
  • 准备收集统计信息
  • 进入临界区(critical section) :当创建检查点异常,使系统直接进入panic状态

  • 非shutdown检查点,跳过if
  • 告知smgr准备好,要开始建检查点了(SyncPreCheckpoint)
  • 开始填充检查点WAL记录

获取最旧的活跃事务id(oldestActiveXid),这里不是从库,先直接记一个invalid事务id即可。

  • 获取最近一次重要的XLOG记录位置
  • 获取WAL插入排他锁,及当前插入的位置curInsert(redo点),获取curInsert必须阻塞其他插入操作。

Checkpoint skipped机制

如果不是shutdown或强制检查点,并且没有活跃WAL请求检查点,跳过检查点创建。避免在系统空闲时,创建重复的检查点。

检查点结构体中时间线和全页写的设置

确定redo点

  • 如果当前页面没有空闲空间,推进到下一页面,计算当前插入位置curInsert
  • 将故障恢复起点(redo点)设置为curInsert
  • 更新RedoRecPtr值,为后续XLogInsert调用做准备

释放WALInsertLock,允许其他事务继续工作

获取SpinLock,更新控制文件RedoRecPtr信息,然后释放

如果启用了log_checkpoints参数(检查点完成时记录到pg日志中),则开始记录日志

获取checkpoint其他字段的信息

退出临界区

刷盘函数

省略一部分从库相关代码

更新控制文件信息

一些收尾工作

删除无用的日志文件

如果前一个检查点存在,更新检查点之间的平均距离

计算并删除旧日志文件(最近一次检查点之前的日志文件均可以删除)

如果有需要,则创建更多日志段,回收旧日志段后再执行这一步(可以复用旧空间)

如果不是恢复阶段,则清空pg_subtrans,因为检查点之前的事务都已经提交了,不再需要保留。

记录检查点完成日志,以及一些收尾工作

函数执行完毕~

四、 CheckPointGuts函数

主要负责将脏页刷入磁盘。除了共享缓冲区中的脏页,还包括其他需要落盘的数据。

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

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hehuyi_In

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值