Checkpoint&Redo
预备知识
概述
前面的文档中,我们讨论了很多关于XLOG问题:XLOG如何组织?如何写入log buffer?log buffer如何落盘?现在我们来阐述XLOG的用处:当数据库意外发生宕机后,如何依据XLOG来做数据恢复?在《数据库重启恢复概述》中,我们阐述过数据库几种基于日志的恢复策略,其中提到undo\redo策略是现在应用最广泛的策略。而PostgreSQL也使用的这种策略。
依据undo\redo策略,我们在数据库重启恢复时,需要对已提交事务中未落盘的数据进行redo,对未提交事务中已落盘的数据进行undo。而PostgreSQL对于undo\redo策略的实现非常简单,可以归纳为两点:
- 在数据库故障恢复时,无脑redo日志文件中的所有XLOG。所谓无脑是指不管这条XLOG对应的事务是否提交。
- 在查询时结合CLOG判断记录是否提交,如果未提交,则不可见。
可见,PostgreSQL在故障恢复时并不做undo操作,对于元组的undo是留在查询的可见性判断时进行的,所以PostgreSQL会牺牲一定的查询性能,换来无比简单的故障恢复流程。本文只讨论PostgreSQL在故障恢复时如何依据XLOG实现redo,对于CLOG及可见性的判断会在后面的文章中阐述。
Checkpoint
核心思想
在概述中我们提到PostgreSQL故障恢复时,会redo日志文件中的所有XLOG。诚然,如果一个数据库存放了从诞生之日起至今所有的XLOG,当发生故障时,然后将全部的XLOG都redo一次,一定可以恢复数据库中的数据。但是这样做既不现实也没必要。不现实是因为数据库每天都会面对大量的增删查改操作,产生大量的XLOG,所以如果想要保存是个数据库今年甚至几十年的所有XLOG需要多大的空间?没必要是因为数据库发生故障通常只有部分还没来得及落盘的数据会丢失,需要通过XLOG来恢复,而大部分的数据已经持久化到磁盘,没必要恢复,所以根本不需要将所有XLOG都redo一次。那么这里就牵扯到几个问题:
-
问题1:如何判断一条XLOG对应的数据是否落盘?
这个问题,其实在前面的文档中已经回答过了。每一条XLOG都对应一个LSN,在PostgreSQL中,将XLOG的物理偏移作为LSN。PostgreSQL中插入流程如下:
-
将元组写入数据页
-
为这个insert操作产生一条XLOG
-
将XLOG写入log buffer,返回该XLOG的LSN
-
将步骤3返回的LSN写入步骤1的数据页,作为这个页面的page LSN
那么在故障恢复时,通过对比XLOG的log LSN与页面的page LSN,就知道这个XLOG对应的数据是否已经落盘。如果log LSN <= page LSN则说明XLOG对应的数据已经落盘,该XLOG无需在页面中执行redo。否则就说明XLOG对应的数据没有落盘,XLOG需要进行redo操作。
-
-
问题2:是不是存在某个点,在这个点之前的所有XLOG都不需要做redo?
当然存在。一个感性的认识,一个数据库运行了两天,那么在第二天的时候,第一天的数据怎么都该落盘了,那么在故障恢复时这部分数据都不需要用XLOG来恢复,而这部分XLOG理所当然就不需要了,可以删了。这个特性可以极大的减少日志的体量,提高磁盘空间的利用率。
而这个点,就叫做redopoint,在故障恢复时,只需要从redopoint开始遍历XLOG直到日志文件结尾,从而大大缩短故障恢复的时间。
注意
在数据库理论中,通常将前面讲的redopoint称为checkpoint。而在PostgreSQL中checkpoint也是一条XLOG日志,里面记录了包含redopoint在内的与 恢复相关的信息。在本文后面会详细阐述checkpoint与redopoint。
-
问题3:既然redopoint这么好,那么如何确定redopoint呢?
redopoint的特性是在redopoint之前所有XLOG对应的数据都已经落盘。那么与其去找redopoint不如构建redopoint。怎么构建呢?思路非常简单,假设log buffer当前的情况如图1所示:

当前日志写入的位置为Insert->CurrBytePos,那么只要在这个时候将数据页面中的所有数据都落盘,那么在落盘完成之后,Insert->CurrBytePos之前的所有XLOG对应的数据都落盘了,Insert->CurrBytePos自然成为了redopoint。数据页落盘的时其他的事务可以正常的开始或提交。这就是PostgreSQL实现checkpoint的核心思想。
代码实现
下面,我们来看看PostgreSQL是如何实现checkpoint的。在PostgreSQL中有以下场景会触发checkpoint:
- 超级用户(其他用户不可)执行CHECKPOINT命令
- 数据库shutdown
- 数据库recovery完成
- XLOG日志量达到了触发checkpoint阈值
- 后台周期性地进行checkpoint
PostgreSQL有一个专门的后台进程用于周期性执行checkpoint,然而其他情况触发checkpoint后,也是通过向后台进程发信号的方式将checkpoint交给后台进程来完成。比如超级用户在执行了CHECKPOINT操作时,PostgreSQL会调用RequestCheckpoint,在该函数中会通过kill给后台进程发送信号(关键代码:checkpointer.c line1012),从而使后台进程执行checkpoint。执行checkpoint的后台进程如图所示:

所以,我们在调试checkpoint流程时需要附加这个进程。
CreateCheckPoint
实现checkpoint创建的函数为CreateCheckPoint,该函数代码比较长,所以我们只分析其关键流程。在分析流程之前,我们先来看几个重要的结构体。checkpoint的相关信息被保存在名为pg_control的文件中。与该文件对应的是一个全局变量:
/*
* We maintain an image of pg_control in shared memory.
*/
static ControlFileData *ControlFile = NULL;
ControlFileData有非常多的成员,这里我们只说明一些重要的成员:
typedef struct ControlFileData
{
/*
* System status data
*/
DBState state; /* 数据库状态 */
XLogRecPtr checkPoint; /* 最近一次chekpoint的位置 */
XLogRecPtr prevCheckPoint; /* 上一次chekpoint的位置 */
} ControlFileData;
-
state
表示数据库的状态,重启时可以通过state来判断之前数据库是否正常关闭,如果没有正常关闭,则进入恢复流程。
-
checkPoint
checkpoint的位置。checkpoint实际是一个CheckPoint结构体,其中存放了redopoint。在checkpoint流程的最后,这个结构体中的数据会被作为一条XLOG写入日志文件并落盘。 而这条XLOG的LSN就会被记录在ControlFileData的checkPoint成员中。所以在恢复时,通过checkPoint就可以获取到CheckPoint结构体中的数据,从而得到redopoint。
-
prevCheckPoint
在checkpoint的流程中也可能出现系统故障,所以checkPoint对应的数据不一定正确,所以PostgreSQL使用prevCheckPoint来存放上一次chekpoint的位置,如果最近一次的chekpoint不靠谱,那么就使用从上一次的checkpoint开始恢复。
现在我们来看看CheckPoint结构体:
typedef struct CheckPoint
{
XLogRecPtr redo; /* next RecPtr available when we began to
* create CheckPoint (i.e. REDO start point) */
...
} CheckPoint;
在这个结构体中,我也只需要关注第一个成员redo,这就是redopoint。在恢复时总是从redo处开始遍历XLOG,直到日志文件结束。
现在,我们来看看CreateCheckPoint的主要流程及对应的关键代码:
- 获取当前XLOG的写入位置,并将这个位置作为redopoint


- 调用CheckPointGuts将数据文件、日志文件等该落盘的全部落盘

在CheckPointGuts中,通过调用CheckPointBuffers对数据页面进行落盘,CheckPointBuffers遍历所有的数据页面,然后调用SyncOneBuffer将脏页进行落盘。
调用顺序:CheckPointGuts > CheckPointBuffers > BufferSync
关键代码:BufferSync中line1957的while循环
- 将CheckPoint结构体作为一条XLOG写入log buffer,并落盘

在执行完第2步后,redopoint之前的日志对应的数据都落盘了,所以这个redopoint就正式生效了,那么就可以写入log buffer中。
- 将checkpoint日志的位置记录到ControlFileData中

这里注意一下这个ProcLastRecPtr。ProcLastRecPtr是进程私有的一个变量,在执行XLogInsert之后,日志的开始位会被存放在ProcLastRecPtr中,结束位置会被存放在XactLastRecEnd中。

- 调用UpdateControlFile将ControlFileData中的内容更新到pg_control文件中。

至此,checkpoint的创建流程完毕。
Redo
现在,我们来看看当数据库发生故障重启后,是如何通过redo XLOG来恢复数据的。重启恢复的主要函数是StartupXLOG,这个函数非常的长,所以我们依然只分析其关键流程:
- 调用ReadControlFile,读取pg_control文件

- 获取checkpoint

- 判断是否需要恢复

本文深入探讨了PostgreSQL中的故障恢复机制,包括检查点(Checkpoint)的创建过程、redo操作的具体实现,以及如何通过XLOG恢复数据。文章还介绍了如何判断数据是否需要全备份以及redo流程中的关键细节。

1261

被折叠的 条评论
为什么被折叠?



