暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

探索 PostgreSql 中的 checkpoint 机制

3153

作者简介

施博文,本科生,目前就读于南京信息工程大学,个人兴趣集中在数据库领域。

Checkpoint 是 PostgreSql 中一个非常重要的概念,本文将详细的介绍什么是 checkpoint,为什么要有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 日志中反应的不同?
带着这个问题,我们来看接下来的部分。
为什么要有 checkpoint
在解释这个问题之前,我们先来补充一点背景。
PostgreSQL如何写数据
以一条 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 日志,它能干啥?
PostgreSQL故障恢复
这一章我们就来讲讲 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 。之前我们说过,checkpoint 是 WA L 日志中的位点,那么这个位点的意义是什么呢:
该位点之前所有 shared buffer 中的脏页均被刷入到存储
那么这个位点又是怎么产生的呢?
没错,是由 checkpoint 这个操作产生的。尽管说起来有些绕口,checkpoint 一会是名词一会是动词,不过我们可以这么理解:checkpoint 操作会往 WAL 日志里写 checkpoint 位点。
底下我们来看 checkpoint 操作的过程
1. checkpoint 操作首先记录下 checkpoint 的开始位置,记录为 redo point(重做位点)
2. checkpoint 将 shared buffer 中的数据刷到磁盘里面去
3. 这时候数据库又来了一条 SQL insert 3
4. 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 日志可以被清理回收
checkpoint 在代码中的实现
我们以 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
          复制
          checkpoint 什么时候运行
          这部分不属于本文的重点内容,因此就大概提一下。
          1. 系统会定期运行 checkpoint,这个时间间隔可以通过参数设置;
          2. 手动运行 checkpoint,直接进行一次 checkpoint 操作。
          总结
          留了几个小问题给大家,希望大家自己阅读 PG 源码找到答案
          1. 我们知道 PG 使用 MVCC 机制,那么重启的时候,redo point 位置的 snapshot 从哪里获得呢?
          2. 对于 redo point 之后的记录,可能有的部分已经被刷到磁盘里了(不是checkpoint刷的),那么怎么保 redo 做的不会重复呢
          请点击文章底部“阅读原文”查看原文内容。



          PostgreSQL中文社区欢迎广大技术人员投稿
          投稿邮箱:press@postgres.cn

           
          文章转载自PostgreSQL中文社区,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

          评论