TDSQL | 《checkpoint 原理浅析》

Checkpoint 定义
抛开官方定义从我们对数据库系统理解来看,修改数据一般是在缓存进行修改,数据库会有专用后台 Backend 进程负责定时将脏块刷入磁盘,进行一个持久化。PG 的 Checkpoint 也是类似,官方文档对 Checkpoint 的定义,首先 Checkpoint 是一个顺序的事物记录点,同 Checkpoint 这个时间之前所有的 heap,以及索引文件修改都被认为是有效的。
在 Checkpoint 执行时,所有脏数据会被刷到磁盘,并记录一个特殊的切换记录到 Wal 日志里,当然这个数据修改是在 WAL 日志之后,也就是说是日志先行,也叫预写式日志。Checkpoint 还有一个作用,在 Crash 后或者重启需要 REDO 时,会根据 Checkpoint 记录去恢复到对应时间点。
这是 Checkpoint 作用的一个体现。从其它信息可以看到在 Checkpoint 之后,这时会把所有的数据做持久化到磁盘上,同时还会清理一些数据库认为不再需要的 Wal 日志,而在运维过程中要特别注意这个点,因为某些异常会导致 wal 累积;wal 的清理是一个清理加回收的机制。
简单来说 Checkpoint 是事务顺序记录点,主要进行刷脏页,redo 时会参考 Checkpoint 进行日志回放,此外还会清理不需要的 Wal 日志。
Checkpoint 触发条件
接下来看 Checkpoint 触发条件。在 PostgreSQL 中 Checkpoint 是由 Checkpointer 进程来执行的。Checkpoint 进程的主流程是无条件 for 循环,在未触发 Checkpoint 时,一直在 Waitlatch 中 Sleep,也就是在 Epoll_wait 中观察 List 链表,看是否有事件句柄已经就绪。
就绪就是某个条件是否在触发 Checkpoint,如果已存在一个就绪事件,那么 Wake up,通过 Setlatch 中 Write pipe 方式去进行唤醒并执行 Checkpoint。可以看到左图就是 Checkpoint 触发方式第一种主动定时触发,右图是另外一种方式其它进程被动触发。
这里主要看被其它进程触发的情况,被动触发这里可以注意到其实也是进程间通信一个比较好的范例。
这些其它进程例如 Postgre 可以用 Force 场景去触发;Startup 在 End of recovery 也就是 REDO 结束时去触发;Walwriter 在 XlogFlush 时,如果接收 Shutdown 信号也会去做 Checkpoint;使用 Basebackup 时也会去做一个 Checkpoint。这些 Backend 或工具,会通过 RequestCheckpoint 函数去触发 Checkpoint。
我们看一下具体的步骤是什么样的:
第一步,各自进程在各自触发场景里先修改共享内存 Ckpt_flags,Checkpointer 进程后续会根据 ckpt_flags 去决定是否要触发 Checkpoint。
第二步各个进程通过调用系统 kill 接口,向 Checkpointer 进程发送 INT 中断信号。
这里为什么要发信号?Checkpoint 在空闲时是在 Epoll_wait 中 Sleep 的,在这里需要触发让它 wakeup。Checkpointer 主函数注册了中断信号的信号处理函数,当收到中断信号后会进入中断,调入处理函数去执行 SetLatch,以 Write local pipe 方式去进行 Wakeup。唤醒后 Checkpoint 会去检测 Ckpt_flag 被设置为什么内容,如果被设置则会执行 Checkpoint。
来看下可以触发 checkpoint 的 Flags:
第一个是 checkpoint is shutdown,数据库 shutdown 的时候
第二个是 End of recovery,当 REDO 结束时,
第三个 Immediate 立即执行
可以看到这些 Flag 其实是多个 Flag 进行一个或运算,组成了 ckpt_flags。
接下来两个相对于其它 Flag 是一个辅助 Flag,Wait 指的是触发后要等待 Checkpoint 完成,Requested Flag 表示已经触发了 checkpoint。
主要看 Cause_Xlog 和 Cause_Time。Cause_Xlog 指数据库在有大量数据写入时,生成的 Wal 日志达到一定数量触发,其实就和之前比较熟悉的参数 Checkpoint_segments 类似。
当 Wal 日志新增的数量大于等于 Checkpoint_segments -1 时会触发 Checkpoint,如果是默认参数配置,例如 Walsize 16M 然后 Max_Wal_size 为 1G,这种情况下它是 42 个 Wal 日志会触发一次 Checkpoint
我们有时会看到 Checkpoint 的比较频繁,它会提示需要去 Increase Max_Wal_size 参数。那么为什么要增大这个参数?Checkpoint_segments 参数在 9.5 之后就不再是单独的参数,它是 Max_wal_size 和 CheckPointCompletionTarget 两个参数联动的结果。提高 Max_Wal_size,相当于增加了被除数,那就是说要生成更多的 Wal 日志时,才会去做 Checkpoint,降低了 Checkpoint 的频率。
Cause_Time 其实很简单,就是 Checkpoint timeout,默认每到五分钟自动触发一次。
以手动强制触发 Checkpoint 为例可以看到当语句是 Checkpoint_statments 时,通过 Request Checkpoint 函数接口,实际传递的几个 Flag 做了一个运算,如果 RecoverInProgress()为 false 则为 CHECKPOINT_FORCE。这里提及下在主库里是 checkpoint,在备库里是 restartpoint
Checkpoint 会做什么
前面定义时已经聊到这一块,当然 Checkpoint 首先要做的肯定是刷脏页,具体的过程是怎么样的?需要刷哪些内容?这里有个图表,从上向下依次可以看到 CLOG 或者子事务,组合事务还关系映射、复制槽、快照等等。
抓取 Checkpointer 的系统调用,抓取过程中 Checkpoint flush 的对象不同,具体的操作可能不同。
主要看下数据缓冲的 Flush 过程,首先用 Open 打开文件句柄是 Relationfilenode 对应的物理文件。这里是用 Pwrite 接口写入修改内容,Pwrite 接口设计思想是为了解决并发下原子写问题。
lseek + write 的方式无法保证原子性。
例如并行写入的场景下,线程 A 不再指向文件的真正末尾位置,它指向文件先前的默认位置,线程 B 写入信息的位置。当线程 A 写入文件时,可能被线程 B 写入的信息覆盖。
插入 PPT 图片
所以后面有了 Pwrite 接口,看起来它是一个原子的写入接口,实际上在数据库里,并不是原子写,我们都知道 DB 的 BLCLKSZ 是 8k,OS 是 4K,如果写一半异常掉电是不是会产生坏页?我们称为页裂场景。
在这里 PG 有 FPW 这个特性去进行兜底。假设发生了异常掉电,只写入了 4k,那么就可以通过完整的顺序 Wal 日志去进行恢复。
Pwrite 写完后,调用 Fsync 将缓冲立即落盘。
这里为什么要用 Fsync 去做缓冲区刷入磁盘立即落盘的操作呢?通常写文件时内核是先将数据复制到缓冲区再排入写队列,晚些时候再写入磁盘,这种方式称为延迟写。为了保证文件系统和缓冲区内容的一致性,确保修改过块立即写到磁盘上,可以使用 Fsync 将对应的缓冲快立即落盘。除了 Fsync 还有其它两种方式,比如 sync,
sync 的特点是将所有修改过的缓冲块排入写队列就直接返回。而 Fsync 它只对由 FD 指定文件起作用,同时它要等待写磁盘操作结束才会返回。
另外一种落盘方式叫做 fdatasync,在 PG 里也使用到了,很多文件也是用 Fsync 去进行落盘操作,那么有什么区别?它和 Fsync 的功能大致上相同,但是它只影响文件中数据部分,除数据外,Fsync 可能还会同步更新文件属性。
下面介绍更新位点信息,例如下图是更新的 ControlFile 位点信息,更新 ControlFile 的共享内存结构体,再把 ControlFile 持久化到 pg_control 文件里去,除此之外,还有 XlogControl 这些共享内存结构体等。
插入 PPT 图片
这些点有什么用?Checkpoint 这些点前面定义的时候也了解到,当 REDO 时,会根据这些点去进行回放,这是其中一个功能。在这里还有一个需要注意的地方,以这两个共享内存结构体为例,一个是 ControlFile,一个是 Xlogctl。在使用、修改时加的锁是不一样的。
对于 ControlFile,它使用的是轻量级锁 LWLock。为什么要用 LWLock?我们知道在 PG 里 LWLock 本身就是用来保护共享内存并发访问的机制,它适用并发访问临界区较长的场景。在提供保护同时,等锁进程会进入一个专用队列进行 Sleep,可以降低一些性能上损耗。XlogCtl 这个共享内存结构体,在这里用的是自旋锁 SpinLock,SpinLock 大多数情况下是一种 TAS 的操作,如果获取到就执行,没有获取到下次会再尝试获取。
由于临界区非常简单只有一条一行,因此用自旋锁效率就足够,但是临界区比较长就不太适用了,这样可能会造成 CPU 空转。
第三个点比较重要的是清理老旧 Wal 日志,PG 目录下保留多少日志就取决于这里。
运维过程中经常会遇到 Wal 日志累积的场景,我们先看 Wal 日志是是怎样的逻辑。首先会根据指针的偏移量,去估算出两次 Checkpoint 之间产生的 Wal 日志量。在 KeepLogSegments 这个函数里,根据配置的 Wal_keep_segments 及最小复制槽的 Restart_lsn 取得这两个之间最小的做为一个取值,计算出一个日志号。
比这个日志号更早的日志,后续会进入清理和回收逻辑,也就是复制槽的 Restart_Isn 和 Wal_keep_segments,这两个值会影响到 Wal 日志保留的计算。
这里依据上次计算的偏移量,结合一套计算公式,计算出需要重用开始回收日志号,它会从这个日志号进行回收重用,也就是两次 Checkpoint 之间做预估,回收一部分后续使用。
插入 PPT 图片
Checkpoint skipped 机制
首先关注触下 flags,当 flags 为 checkpoint-is-shutdown 或者 checkpoint-end-of-recovery 或者 checkpoint-force 时,也就是说数据库停机,redo 完成,手动强制执行触发 checkpoint 这几种方式是不是进入这个 if 逻辑的
接下来看第二个 If 条件,当 Important_lsn 和 ControlFile 记录的 Checkpoint 点相等时它会进入到里面直接 Return。那么这个等值成立的条件是什么?右值我们很熟悉,是上一次 Checkpoint 记录的点。左值是什么?是 Wal 日志正在写入数据的位置。当前 Wal 日志写入位置等于上次 Checkpoint 的位置。这说明它的位点和 Wal 日志没有更新,也就说明了我们数据库里没有修改数据的操作,也就没有新的脏块,在这里跳过刷脏、清理 WAL 等操作逻辑成立的。
这个机制看起来比较合理,但是实际上某些场景会不会存在隐患?下面是之前处理的一个案例,简单描述一下,有一个实例 Wal_keep_segments 配置为 256,并且没有使用复制槽,同时归档也没有报错,但是在这种情况下,它保留了 17000 多个 Wal 日志。
起初判断是不是 Checkpoint 或进程异常,但在抓取系统调用之后,发现并没有异常。当打开 Debug 日志之后,可以看到它是进入了刚才分析的 Checkpoint skip 机制,这个案例场景是集中持续一段时间的大并发写入,Wal 日志产生速度非常快,同时对应归档速度稍慢。这样 Wal 产生快、归档慢,本身是缓慢的累积的过程。后续写入动作就停止了,并且在一段时间内没有修改数据的操作,这时 Checkpointer 每到 5 分钟通过 Timeout 自动触发时,就进入 Checkpoint skip 机制,不会再去清理 wa 日志。
插入 PPT 图片
这里的规避方案是手动强制触发了一次 Checkpoint,从数量统计下来已经确定成功了,就只剩 300 多个了。
这个是一种比较特殊的场景,但是我们有可能会遇到,这里其实还有一个小点,我们刚才已经了解了清理 Wal 日志的逻辑,那 Wal 日志累积的场景还会有哪几种?其实还有以下几种。
插入 PPT 图片
一、Wal_keep_segments 被调大,13 之后改为 Wal_keep size,这个参数给调大。
二、复制槽长时间处于非活跃状态,可以查询 PG Replication slow 视图,是否有 Inactive 的复制槽,当然在 13 版本后增加了一个参数可以缓解这个问题,也就是 max_slot_wal_keep_size,这个参数可以配置复制槽保留多少 Wal 日志。
三、可能是 archive 进程异常,在进程异常的情况下,是不会进入 Remove 函数去清理 Wal 日志。
四、Checkpointer 进程的其它异常,可能性比较小。
Checkpoint 过程记录
Checkpoint 的过程记录可以通过设置 GUC 参数 Log_ Checkpoints=on,打开这个参数 pgLog 会记录过程,记录两条信息,第一条是 Checkpoint 的触发条件和方式,第二条是 Checkpoint 的工作内容,比如说刷了多少脏块,清理或回收多少 Wal 日志,执行时间多久等。
插入 PPT 图片
【TDSQL | 《checkpoint 原理浅析》】第二种方式可以通过命令行工具 pg_controldata 去解析 pg_control 文件,这样可以获取 Checkpoint 相关位点信息。同样还可以使用 pg_control_Checkpoint 函数,去获取 Checkpoint 相关位点。这个比较适合做 Checkpoint 相关监控。这里可以看到命令函数工具和系统函数,它的位点和这几个项目或条目是一致的,在内核里访问的是相同结构。

    推荐阅读