施博文,本科生,目前就读于南京信息工程大学,个人兴趣集中在数据库领域。
Checkpoint 是 PostgreSql 中一个非常重要的概念,本文将详细的介绍什么是 checkpoint,为什么要有checkpoint,以及 checkpoint 的运作原理。大家在玩游戏的时候经常会需要存档,而这个存档点就是由 checkpoint 单词翻译而来的。在Postgresql 官方文档中(https://www.postgresql.org/docs/13/sql-checkpoint.html),checkpoint 的定义如下:
A checkpoint is a point in the write-ahead log sequence at which all data files have been updated to reflect the information in the log. All data files will be flushed to disk.
翻译成中文就是:checkpoint 是 WAL(write-ahead log) 日志中的一个位点,在这个点位之前数据库中的所有数据都和 WAL 日志中反映的信息相同。说起来有些绕口,并且有些困惑:WAL 日志中反应的信息是什么?数据库中的所有数据为什么会和WAL 日志中反应的不同?以一条 SQL 语句为例:INSERT INTO tbl VALUES(1);1. 将 INSERT 1 这个操作写入 WAL 日志中(会有插入的表名,这里省略)2. 修改 shared buffer 中该页的信息(如果该页不在buffer中,则从磁盘去取)3.background 进程会在某个时刻将 shared buffer 中的数据刷到磁盘(图中红色标出)。但是这并不是立刻发生的,而是一个异步操作。这也就回答了上述中的第一个问题,WAL 日志反应的信息:WAL 日志可以看成是 redo log,将所有的操作原样记载在 WAL 日志中。但实际上 WAL 日志是物理日志,记录的是对某个文件某个块的修改。但是这时候又衍生出了几个问题:为什么要有 WAL 日志,它能干啥?还是刚才那张图,假如 background 进程正在将 shared buffer 中的数据刷到磁盘,还没刷完的时候你电脑坏了。这时候你上一条 INSERT INTO tbl VALUES(1)的 SQL 仿佛失效了。过了一会,你的电脑重启了,为了让你上一条 SQL 不白写,PG 会进入恢复模式,惊喜的发现 WA L 日志里面记录了上一条 SQL 的 redo 信息,遂直接重放,数据又重新回到了数据库中~这时候我们就明白了 WAL 日志作为 redo log 的作用。但是又出现了两个问题:1. 我怎么知道数据库什么时候崩的,WAL 日志应该从哪开始重放呢?2. WAL 日志就这么一直写下去,子子孙孙无穷匮也,磁盘不炸了吗?终于,我们引入了主角:checkpoint 。之前我们说过,checkpoint 是 WA L 日志中的位点,那么这个位点的意义是什么呢:该位点之前所有 shared buffer 中的脏页均被刷入到存储没错,是由 checkpoint 这个操作产生的。尽管说起来有些绕口,checkpoint 一会是名词一会是动词,不过我们可以这么理解:checkpoint 操作会往 WAL 日志里写 checkpoint 位点。1. checkpoint 操作首先记录下 checkpoint 的开始位置,记录为 redo point(重做位点)2. checkpoint 将 shared buffer 中的数据刷到磁盘里面去3. 这时候数据库又来了一条 SQL insert 34. checkpoint 刷脏结束,redo point 之前的数据均已被刷到磁盘存储(数据1和2)5. 这时候在 WAL 日志里面记录 checkpoint 位点(红色),表明 checkpoint 操作结束。checkpoint 位点会记录相关信息,比如 redo point 的值(从哪开始重做)6. 将最新的 checkpoint 位点记录在 pg_control 文件中这时候假如开始数据库恢复,那么数据库会从 pg_control 文件中找到最新的 checkpoint 位置,再从checkpoint 找到 redo point 的位置,开始重放日志。不难看出,1 和 2 这两个数据在 checkpoint 中已经持久化到磁盘存储,WAL 日志中也只有 INSERT 3 操作需要重放。至此,我们就回答了上一小节的最后一个问题:WAL 日志应该从哪开始重放呢。还剩下一个问题,WAL 日志一直写下去吗,不清理吗?我们注意到,checkpoint 的时候已经将 redo point 之前的所有数据都落盘了,那 redo point 之前的所有WAL 日志都已经没有用了(下次宕机的时候这部分数据已经被持久化了,不属于要恢复的数据),可以请理了。因此,checkpoint 的第二个作用就是用于标记这个位点之前的 WAL 日志都可以被回收了。最后,我们来总结一下 checkpoint 的两大作用(重要的事情说三遍):1. checkpoint中记录了 redo point,标记 redo point 之前的数据均已刷脏,完成持久化存储2. 标记 redo point 之前的 WAL 日志可以被清理回收我们以 PostgreSQL 最新的版本 13.2 为例,简要的讲述一下代码里 checkpoint 的实现方式。为了降低理解的难度,下面的代码会做一些简化,去掉加锁等内容。首先,我们定位到 xlog.c文件的 CreateCheckPoint 函数,顾名思义,这个函数就是用来完成一次checkpoint 操作的。按照,我们的逻辑,checkpoint 应该首先创造一个 redo point。curInsert = XLogBytePosToRecPtr(Insert->CurrBytePos);
.......
.......
freespace = INSERT_FREESPACE(curInsert);
if (freespace == 0)
{
if (XLogSegmentOffset(curInsert, wal_segment_size) == 0)
curInsert += SizeOfXLogLongPHD;
else
curInsert += SizeOfXLogShortPHD;
}
checkPoint.redo = curInsert;
复制
上述代码的作用就是找出当前的最后一个 XLOG record 位置,并计算出下一个合法的 XLOG record 位置。接着在 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)
{
CheckPointCLOG();
CheckPointCommitTs();
CheckPointSUBTRANS();
CheckPointMultiXact();
CheckPointPredicate();
CheckPointRelationMap();
CheckPointReplicationSlots();
CheckPointSnapBuild();
CheckPointLogicalRewriteHeap();
CheckPointBuffers(flags); /* performs all required fsyncs */
CheckPointReplicationOrigin();
/* We deliberately delay 2PC checkpointing as long as possible */
CheckPointTwoPhase(checkPointRedo);
}
复制
刷脏操作结束之后,就应该把 checkpoint 作为一条日志写入 WAL 了。/*
* Now insert the checkpoint record into XLOG.
*/
XLogBeginInsert();
XLogRegisterData((char *) (&checkPoint),sizeof(checkPoint));
recptr = XLogInsert(RM_XLOG_ID, shutdown ? XLOG_CHECKPOINT_SHUTDOWN :
XLOG_CHECKPOINT_ONLINE);
XLogFlush(recptr);
复制
到这里已经在 WAL 日志中写下了 checkpoint 这条记录,最后要做的就是在 pg_control 文件中更新checkpoint 位置的相关信息。/*
* Update the control file.
*/
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;
ControlFile->unloggedLSN = XLogCtl->unloggedLSN;
UpdateControlFile(); // 更新 control file
复制
1. 系统会定期运行 checkpoint,这个时间间隔可以通过参数设置;2. 手动运行 checkpoint,直接进行一次 checkpoint 操作。留了几个小问题给大家,希望大家自己阅读 PG 源码找到答案1. 我们知道 PG 使用 MVCC 机制,那么重启的时候,redo point 位置的 snapshot 从哪里获得呢?2. 对于 redo point 之后的记录,可能有的部分已经被刷到磁盘里了(不是checkpoint刷的),那么怎么保证 redo 做的不会重复呢PostgreSQL中文社区欢迎广大技术人员投稿
投稿邮箱:press@postgres.cn