Checkpoint

要在发生故障后恢复数据一致性(即执行恢复操作),PostgreSQL 需要向前回放 WAL 日志,并将其中表示丢失变更的记录应用到相应的数据页上。为了确定哪些变更丢失了,系统会将磁盘上数据页的 LSN(日志序列号)与 WAL 记录的 LSN 进行比较。但问题是,我们应该从哪里开始恢复?如果恢复起点选得太晚,那么在此之前已经写入磁盘的数据页将无法接收到所有应有的变更,最终导致无法修复的数据损坏。而从日志的起始位置开始恢复又不现实:不仅无法长期保存如此巨量的数据,也无法接受过长的恢复时间。因此,我们需要一个不断向前推进的检查点(checkpoint),从而可以从这个位置安全地开始恢复,同时删除所有更早的 WAL 记录。

创建检查点最直接的方式是:定期暂停系统所有操作,并将所有脏页强制刷新到磁盘。但这种方式显然是不可接受的,因为系统会因此暂停不定但相当长的时间。

正因为如此,PostgreSQL 将检查点的过程分摊到一段时间内完成,实际上构成了一个“区间”(interval)。检查点的执行是由一个特殊的后台进程负责的,这个进程叫做 checkpointer(检查点进程)

检查点开始(Checkpoint start):

checkpointer 进程会将所有可以立即写入磁盘的内容进行刷新,包括:

  • CLOG(提交日志)中的事务状态信息,
  • 子事务的元数据,
  • 以及其他一些结构。

检查点执行过程(Checkpoint execution):

检查点执行的大部分时间都耗费在将 脏页(dirty pages)刷新到磁盘上。

首先,在检查点开始时,所有当时处于“脏”状态的缓冲区(buffer)的页头会被打上一个特殊标记(tag)。这个过程非常迅速,因为它不涉及任何 I/O 操作,只是内存中的标记设置。

随后,checkpointer 会遍历所有缓冲区,并将带有该标记的页写入磁盘。这些页不会被驱逐出缓存(即它们仍然保留在缓冲池中),只是被刷盘,因此在这个过程中可以忽略使用计数(usage count)和 pin 计数(pin count)。

页面按 ID 顺序处理,以尽可能避免随机写入。为实现更好的负载均衡,PostgreSQL 会在多个表空间之间交替进行写入(因为它们可能位于不同的物理设备上)。

后端进程(backend)也可以将打了标记的缓冲页写入磁盘 —— 如果它们先访问到了这些页的话。无论由谁写入,缓冲区的标记都会在这个阶段被清除,因此每个缓冲页在此次检查点中只会被写一次。

很自然地,在 checkpoint 进行期间,缓冲区中的页面仍然可能被修改。但由于这些新的脏页没有被打上标记,checkpointer 会忽略它们。

检查点完成:

当在检查点开始时被标记为脏的所有缓冲页都已经写入磁盘后,检查点就被视为完成。从现在起(但不是在此之前!),本次检查点的起始位置将被作为恢复操作的新起点。在这个点之前写入的所有 WAL 日志都不再需要了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Time →
LSN:
┌──────────────────────────────────────────────────────────────────────┐
│ │
0 ──┴─────┬───────────────────────────────────────────┬────────────┬───────┘
│ │ │
▼ ▼ ▼
1 2 3
✔ 若 3 写入成功:恢复可从 2 开始(即 redo = 2)
✘ 若 3 写入失败:恢复只能从 1 开始(上次 checkpoint)

说明:
- 1:上一次 checkpoint 的起始 LSN(redo)
- 2:本次 checkpoint 开始时wallog最大LSN(新的redo点)
- 3:本次 checkpoint 完成后写入 WAL 的记录(记录了 redo=2)

最后,checkpointer 进程会创建一条表示检查点完成的 WAL 记录,并在其中标明此次检查点的起始 LSN。由于检查点在开始时不会写入任何日志,因此这个起始 LSN 可以是任意类型的 WAL 记录所属的 LSN。
此外,PGDATA/global/pg_control 文件也会被更新,以指向最近完成的检查点。(在此过程完成之前,pg_control 始终保留着上一个检查点的信息。)

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
=> UPDATE big SET s = 'FOO';
=> SELECT count(*) FROM pg_buffercache WHERE isdirty;
count
−−−−−−−
4119
(1 row)

=> SELECT pg_current_wal_insert_lsn();
pg_current_wal_insert_lsn
−−−−−−−−−−−−−−−−−−−−−−−−−−−
0/3E7EF7E0 (1 row)

=> CHECKPOINT;
=> SELECT count(*) FROM pg_buffercache WHERE isdirty;
count
−−−−−−−
0
(1 row)

=> SELECT pg_current_wal_insert_lsn();
pg_current_wal_insert_lsn
−−−−−−−−−−−−−−−−−−−−−−−−−−−
0/3E7EF890
(1 row)

最新的 WAL 条目与检查点完成有关(CHECKPOINT_ONLINE)。该检查点的起始 LSN 出现在 redo 之后;这个位置对应的是检查点开始时最新插入的 WAL 条目。

同样的信息也可以在 pg_control 文件中找到。

1
2
3
postgres$ /usr/local/pgsql/bin/pg_controldata \
-D /usr/local/pgsql/data | egrep 'Latest.*location' Latest checkpoint location: 0/3E7EF818
Latest checkpoint's REDO location: 0/3E7EF7E0

参考书目

  1. Egor Rogov, PostgreSQL 14 Internals, https://postgrespro.com/community/books/internals