如何使用 PostgreSQL 14 的持续归档备份和基于时间点恢复

持续归档备份

pg 有三种基本的备份方式:

  • 利用 pg_dump 进行 sql dump,属于逻辑备份无法恢复到指定状态。
  • 基于文件系统备份,需要文件系统提供快照功能保证一致性,否则必须停机备份。
  • 持续归档,首选的高可靠性备份技术。

WAL 日志持续归档是实现归档备份的关键,把一个文件系统级别的备份和归档的 WAL 文件结合起来,当需要恢复时,先恢复文件系统备份,然后重放归档的 WAL 文件把系统恢复到当前(或指定的时间点)状态。

持续归档的优点:

  • 不需要完全一致的文件系统备份,通过从重做点重放 WAL 到达一致状态,因此可使用简单工具在不停机状态下制作基础备份。
  • 简单地持续归档 WAL 文件即可在无法频繁完全备份的情况下实现连续备份。
  • 利用基础备份和 WAL 存档重放到指定时间点可以将数据库恢复到基础备份之后的任意时间点。
  • 连续地传输 WAL 归档文件到另一台已应用相同基础备份的机器,即可实现主备复制系统。

缺点:

  • 只能备份恢复整个数据库集簇,不支持更细粒度。
  • 基础备份和持续归档文件会占用大量空间。

开启 WAL 归档

pg 默认在 pg_wal 目录持续生成 16M 大小的 WAL 段文件,没有开启归档模式时 pg 会对 WAL 段文件进行清理和回收。

归档过程是将检查点之前的 WAL 段文件从 pg_wal 目录传输到指定位置(如复制到用户自定义的目录),传输成功后原有的 WAL 段文件将被清除或回收,未成功将保留文件并不断重试归档命令。

pg 归档过程具有极高可扩展性,传输过程即执行用户提供的一条 shell 命令,传输的具体方式和目标位置完全由用户自定义,仅根据命令返回码判断归档是否成功。

开启 WAL 归档的配置示例:

1
2
3
wal_level = replica # 或更高级别
archive_mode = on
archive_command = 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f'

关于归档命令有以下注意点:

  • 归档命令千万不要直接覆盖相同名称的归档文件,因为不同集簇可能产生相同名称的 WAL 段文件,归档命令应该在归档位置已存在归档文件时以非零状态码返回。
  • 归档命令应当保留 WAL 段文件的原始文件名。
  • 设计归档命令时应考虑如何解决或处理潜在的异常情况,否则会导致 pg_wal 目录不断增长从而导致 pg 因空间不足关闭。
  • 有必要开启 logging_collector 参数,之后归档命令的标准错误输出会被收集到数据库日志中,方便对归档过程调试监控。
  • 归档命令只会调用于已写完毕的 WAL 段文件,对于工作负载很小的数据库一个 WAL 段文件可能很长时间都不会切换,设置较小的 archive_timeout 可以缩短事务执行完毕到被可靠地归档之间的时间间隔,也可通过执行 SELECT pg_switch_wal(); 手动切换 WAL 段文件,这也会触发对切换前使用的 WAL 段文件进行归档,该命令只能在主库执行。
  • 如果命令被 SIGTERM 以外的信号或者 shell 的错误(如未找到命令)终止,归档将被中止且服务器将会关闭。

基础备份

基础备份是由多个顺序命令组成的阶段性操作:

  1. 执行 pg_start_backup 开始备份,数据库会执行一次检查点,在备份开始时刻显式创建一个重做点,尽量将此时间点之前的事务写入磁盘,同时记录下该检查点的 LSN 位置。 为了不影响在线运行,该命令默认通过分散 IO 的方式执行检查点,因此可能持续较长时间才返回。 pg_start_backup 还会强制开启整页写入模式,保证在基础备份期间对数据库的写入可以完全回放。
  2. pg_start_backup 执行成功后,用户需要自行使用文件系统工具对数据目录进行归档,获得一份文件系统备份,注意不同文件可能在不同的时间点被复制,因此文件系统备份中数据库的状态可能不一致。pg 预期这种情况的发生,并通过重放在整个基础备份期间写入的 WAL 段文件使其到达一致状态。
  3. 执行 pg_stop_backup,数据库会强制切换正在使用的 WAL 段文件,并记录切换前的最后一条 LSN,结束 LSN 即为本次基础备份实际所备份的一致状态。基础备份期间写入的 WAL 段文件将被归档,结束操作还会发送一份记录了这些 WAL 段文件名称的备份历史文件到归档位置,该文件的文件名记录了使用该基础备份恢复时所需要的第一个 WAL 段文件,文件内容记录了本次备份的起止时间戳和所需要的起止 WAL 段文件。在文件系统备份的基础上应用这些已归档的 WAL 段文件,即可恢复到结束 LSN 所代表的一致状态。
  4. 结束备份后需要在文件系统备份的根目录中手动创建 backup_label 文件,内容为 pg_stop_backup 操作所返回的输出,同时最好单独保存备份历史文件中所记录的 WAL 段文件,文件系统备份加上备份期间写入的 WAL 段文件才是一份有效的基础备份,能够成功恢复到数据库在结束备份时所处的一致状态。

pg_start_backuppg_stop_backup 命令的具体细节可参考:

在安全地归档文件系统备份和备份期间使用的 WAL 段文件后(在备份历史文件中指定),文件名称中数字序列小于备份历史文件名称的 WAL 段文件可以被删除。

恢复需要一个基础备份加上该基础备份之后产生的持续 WAL 归档文件,而重放大量 WAL 归档文件较为耗时,推荐的做法是定期做一次基础备份,同时清理基础备份之前的较早的 WAL 归档文件(清理后无法再恢复到该时间点)。

使用底层 API 制作基础备份

  1. 确保归档功能已开启并工作正常。

  2. 使用超级用户连接到数据库并执行以下命令(备份期间需要保持该连接):

    1
    
    SELECT pg_start_backup('label', false, false);
    
    • 第一个参数是一个自定义的描述性的标签。
    • 第二个参数代表是否开启快速检查点,开启后会执行快速检查点立刻发起大量 IO,可能会影响备份期间的数据库性能。关闭后会分散 IO 降低对数据库影响,但需要执行较长时间。
    • 第三个参数代表是否开启独占备份,新版本已不建议开启。
  3. 使用任意的文件系统备份工具将数据目录进行归档。 如果备份工具因为复制期间文件被更改而以非零状态码返回,该错误可以被忽略。 数据目录下的部分临时子目录、文件或子目录中的文件(如 pg_wal)可以在归档时忽略。

  4. 在同一连接中继续执行如下命令:

    1
    
    SELECT * FROM pg_stop_backup(false, true);
    

    该命令将终止备份模式,并自动切换 WAL 段文件使得备份期间写入的 WAL 段文件能够被归档,默认情况下这些 WAL 端文件归档成功后该命令才会返回,返回的第二个字段输出需要写入到文件系统备份根目录下的 backup_label 文件中,该文件在非独占备份模式需要手动创建。

  5. 结束命令成功返回后,说明备份期间写入的 WAL 段文件已经被归档,此时备份结束。可以将文件系统备份和这些 WAL 段文件保存在一起,它们组成了一份完整的基础备份。

使用 pg_basebackup 制作基础备份

pg_basebackup 封装了底层的 pg_start_backuppg_stop_backup 等命令,并提供了一些非常方便的特性:

  • 支持对数据目录的文件系统备份进行存档压缩,自动忽略不需要的文件,自动写入 backup_label 文件。
  • 自动记录备份文件的校验和,防止备份被更改。
  • 支持自动获取备份期间生成的所有 WAL 段归档文件,并存放到 pg_wal 或其他目录中。

使用示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ pg_basebackup -D /backup/demo -Ft -z -Xs -c fast -P -v
pg_basebackup: initiating base backup, waiting for checkpoint to complete
pg_basebackup: checkpoint completed
pg_basebackup: write-ahead log start point: 0/75ED8820 on timeline 1
pg_basebackup: starting background WAL receiver
pg_basebackup: created temporary replication slot "pg_basebackup_15233"
244438/244438 kB (100%), 1/1 tablespace
pg_basebackup: write-ahead log end point: 0/859200C8
pg_basebackup: waiting for background process to finish streaming ...
pg_basebackup: syncing data to disk ...
pg_basebackup: renaming backup_manifest.tmp to backup_manifest
pg_basebackup: base backup completed

$ ls /backup/demo
backup_manifest  base.tar.gz  pg_wal.tar.gz

分别将 base.tar.gzpg_wal.tar.gz 解压,得到文件系统备份和备份期间写入的 WAL 段归档:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ cat /backup/demo/backup_label
START WAL LOCATION: 0/75ED8820 (file 000000010000000000000075)
CHECKPOINT LOCATION: 0/763C57B0
BACKUP METHOD: streamed
BACKUP FROM: primary
START TIME: 2022-07-09 15:13:35 UTC
LABEL: pg_basebackup base backup
START TIMELINE: 1

$ ls -l /backup/demo/pg_wal
total 278532
-rw------- 1 postgres postgres 16777216 Jul  9 15:13 000000010000000000000075
-rw------- 1 postgres postgres 16777216 Jul  9 15:13 000000010000000000000076
-rw------- 1 postgres postgres 16777216 Jul  9 15:13 000000010000000000000077
-rw------- 1 postgres postgres 16777216 Jul  9 15:13 000000010000000000000078
-rw------- 1 postgres postgres 16777216 Jul  9 15:13 000000010000000000000079
-rw------- 1 postgres postgres 16777216 Jul  9 15:13 00000001000000000000007A
-rw------- 1 postgres postgres 16777216 Jul  9 15:13 00000001000000000000007B
-rw------- 1 postgres postgres 16777216 Jul  9 15:13 00000001000000000000007C
-rw------- 1 postgres postgres 16777216 Jul  9 15:13 00000001000000000000007D
-rw------- 1 postgres postgres 16777216 Jul  9 15:13 00000001000000000000007E
-rw------- 1 postgres postgres 16777216 Jul  9 15:13 00000001000000000000007F
-rw------- 1 postgres postgres 16777216 Jul  9 15:13 000000010000000000000080
-rw------- 1 postgres postgres 16777216 Jul  9 15:13 000000010000000000000081
-rw------- 1 postgres postgres 16777216 Jul  9 15:13 000000010000000000000082
-rw------- 1 postgres postgres 16777216 Jul  9 15:13 000000010000000000000083
-rw------- 1 postgres postgres 16777216 Jul  9 15:13 000000010000000000000084
-rw------- 1 postgres postgres 16777216 Jul  9 15:13 000000010000000000000085
drwx------ 2 postgres postgres     4096 Jul  9 15:20 archive_status

数据目录中的 backup_label 文件并没有记录基础备份结束的 LSN 位置,但可以在 WAL 归档目录下对应的备份历史文件中查看:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ cat 000000010000000000000075.00ED8820.backup
START WAL LOCATION: 0/75ED8820 (file 000000010000000000000075)
STOP WAL LOCATION: 0/859200C8 (file 000000010000000000000085)
CHECKPOINT LOCATION: 0/763C57B0
BACKUP METHOD: streamed
BACKUP FROM: primary
START TIME: 2022-07-09 15:13:35 UTC
LABEL: pg_basebackup base backup
START TIMELINE: 1
STOP TIME: 2022-07-09 15:13:42 UTC
STOP TIMELINE: 1

在使用该基础备份恢复时,恢复进程在重放了 000000010000000000000085 WAL 段文件后,数据库才会进入一致状态,之后数据库才能够接受连接执行查询操作,如下方的恢复日志所示:

1
2
3
4
5
6
2022-07-10 08:05:28.780 UTC [21181] LOG:  restored log file "000000010000000000000084" from archive
2022-07-10 08:05:29.024 UTC [21181] LOG:  restored log file "000000010000000000000085" from archive
2022-07-10 08:05:29.147 UTC [21181] LOG:  consistent recovery state reached at 0/859200C8
2022-07-10 08:05:29.148 UTC [21179] LOG:  database system is ready to accept read-only connections
2022-07-10 08:05:29.232 UTC [21181] LOG:  restored log file "000000040000000000000086" from archive
2022-07-10 08:05:29.480 UTC [21181] LOG:  restored log file "000000040000000000000087" from archive

从归档备份中恢复

恢复的操作步骤如下:

  1. 停止 pg 服务器进程。
  2. 备份当前数据目录,如果空间不够,至少要备份 pg_wal 目录下还未归档的 WAL 段文件。
  3. 移除数据目录下的所有文件和子目录。
  4. 文件系统备份恢复到数据目录,确保恢复后的文件和目录有正确的权限。
  5. 如果做文件系统备份时没有忽略 pg_wal 目录,现在需要清空恢复后的 pg_wal 目录,将未归档的 WAL 段文件复制到该目录。
  6. 在数据目录下创建一个 recovery.signal 文件,该文件指示 pg 在启动时进入恢复模式,并将在恢复成功后自动删除。
  7. 在配置文件 postgresql.conf 中设置和恢复相关的参数,如 restore_command,下文会进行详细介绍。
  8. 启动 pg 服务器。服务器将进入恢复模式,并开始获取和处理恢复所需的 WAL 段文件。如果恢复过程因外部错误(如主机断电)被终止,可以简单地重启服务器让它继续恢复。恢复完成后,服务器将删除 recovery.signal 文件以防止重新进入恢复模式,然后开始正常运行。

恢复的操作流程并不复杂,其关键点在于通过配置文件设置:

  • 恢复时获取已归档 WAL 段文件的方式。
  • 恢复需要到达的目标状态。

恢复命令

恢复时必须设置 restore_command (下文称之为恢复命令)来告诉 pg 如何获取已归档的 WAL 段文件,与存档命令类似,该命令定义如何将 pg 需要的特定 WAL 段文件(也包含其他类型文件)通过用户自定义的方式从归档位置传输到数据目录下的临时位置,然后 pg 会读取该临时文件执行重做。在该命令中可以通过执行脚本的方式,自定义更加复杂的行为。

如果已归档的 WAL 段文件存放于 /backup/demo/pg_wal 目录,则示例的恢复命令为:

1
restore_command = 'cp /backup/demo/pg_wal/%f %p'

设计恢复命令时有以下注意点:

  • 恢复命令一定要在传输失败或传输文件不存在时以非零状态码返回。pg 会尝试通过恢复命令获取一些并不在归档位置中的文件,因此恢复不会因为恢复命令失败而直接中止:

    1
    2
    3
    4
    5
    6
    
    2022-07-10 08:05:24.762 UTC [21181] LOG:  restored log file "00000003.history" from archive
    2022-07-10 08:05:24.766 UTC [21181] LOG:  restored log file "00000004.history" from archive
    cp: cannot stat '/var/lib/postgresql/archive/14/demo/00000005.history': No such file or directory
    2022-07-10 08:05:24.771 UTC [21181] LOG:  starting point-in-time recovery to 2022-07-10 08:03:23.157122+00
    2022-07-10 08:05:24.776 UTC [21181] LOG:  restored log file "00000004.history" from archive
    2022-07-10 08:05:24.862 UTC [21181] LOG:  restored log file "000000010000000000000076" from archive
    
  • 如果 pg 无法通过恢复命令获取某个文件,之后会尝试从数据目录下的 pg_wal 目录中寻找。

  • 如果命令被 SIGTERM 以外的信号或者 shell 的错误(如未找到命令)终止,恢复将被中止且服务器将会关闭。

恢复模式

恢复实际上有两种工作模式:

  • 在数据目录下创建一个名为 standby.signal 的文件,服务器启动后将进入 standby 模式。服务器进入恢复状态,并且在达到归档的 WAL 的末端时不会停止恢复,而是通过连接到由primary_conninfo 设置指定的主服务器或通过使用 restore_command 获取新的 WAL 段,继续尝试恢复。
  • 在数据目录中创建一个叫做 recovery.signal 的文件,服务器启动后将进入目标恢复模式。目标恢复模式在归档的 WAL 段文件被完全重放或达到 recovery_target 时结束。

通常情况下,standby 模式被用来提供高可用和扩展只读库,而目标恢复则被用来恢复丢失的数据或克隆新的服务器。如果同时创建了standby.signalrecovery.signal 文件,则以 standby 模式为优先。

恢复目标

默认情况下,恢复模式会处理完所有可用的 WAL 段文件,即通过递增 WAL 段命名中的序列号不断获取下一个 WAL 段文件,直到获取失败为止,最后会将数据库恢复到「相对最新」的状态。因此在恢复过程中出现的类似「文件未找到」的错误通常是正常的,尤其是在恢复结束时。

如果你只需要恢复到基础备份结束到当前时刻之间的某一个时间点,则需要指定停止点,即恢复目标。恢复目标可以是时间戳、命名的恢复点和事务 ID,在实践中使用基于时间点恢复居多。

我们可以且仅可从以下参数中选择一个来指定恢复目标:

  • recovery_target:唯一可用的值是 immediate,到达基础备份结束时的一致状态即停止恢复。
  • recovery_target_lsn:设置恢复目标为指定的 LSN。
  • recovery_target_name:设置恢复目标为指定的命名恢复点(通过pg_create_restore_point() 创建命名恢复点)。
  • recovery_target_time:设置恢复目标为指定的时间戳。
  • recovery_target_xid:设置恢复目标为指定的事务 ID。

通过以下选项进一步设定是否包括恢复目标以及到达后的行为:

  • recovery_target_inclusive:指定恢复时是否包含恢复目标,即是否重放包含了目标时间戳、LSN 或事务 ID 的 WAL 日志条目,否则将在该恢复目标前停止。默认值为 true
  • recovery_target_timeline:指定恢复时所到达的时间线,通常使用默认值 latest,即归档日志中最后生成的时间线。
  • recovery_target_action:指定到达恢复目标后服务器将执行的动作,仅在设定了恢复目标时生效。可执行以下三种动作:
    • pause:暂停恢复进程。暂停时用户可根据从数据库查询到的当前状态执行下一步操作。可以选择 resume 被暂停的恢复进程,这会导致恢复模式结束;也可以选择更改恢复目标配置后重启数据库服务器,重新恢复到另一目标。如果服务器因 hot_standby=off 配置而无法接受查询,该选项等同于 shutdown
    • promot:结束恢复进程并创建一条新的时间线,开始接受数据库连接。
    • shutdown:直接关闭服务器。此时不会删除 recovery.signal 文件,如果服务器重新启动将立刻再次关闭;如果更改恢复目标配置后重启,将进入恢复模式从上一个恢复目标开始继续重放到新的恢复目标。

恢复终止

在任何情况下,如果恢复进程在到达已配置的恢复目标之前,因处理完了可用的 WAL 段文件而结束,恢复进程将以 FATAL 错误退出导致服务器关闭:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
2022-07-10 08:05:50.673 UTC [21181] LOG:  restored log file "0000000400000000000000D1" from archive
2022-07-10 08:05:50.865 UTC [21181] LOG:  restored log file "0000000400000000000000D2" from archive
cp: cannot stat '/var/lib/postgresql/archive/14/demo/0000000400000000000000D3': No such file or directory
2022-07-10 08:05:51.003 UTC [21181] LOG:  redo done at 0/D2CA8530 system usage: CPU: user: 5.91 s, system: 9.59 s, elapsed: 25.94 s
2022-07-10 08:05:51.003 UTC [21181] LOG:  last completed transaction was at log time 2022-07-10 08:02:30.614229+00
2022-07-10 08:05:51.003 UTC [21181] FATAL:  recovery ended before configured recovery target was reached
2022-07-10 08:05:51.015 UTC [21179] LOG:  startup process (PID 21181) exited with exit code 1
2022-07-10 08:05:51.015 UTC [21179] LOG:  terminating any other active server processes
2022-07-10 08:05:51.026 UTC [21179] LOG:  shutting down due to startup process failure
2022-07-10 08:05:51.047 UTC [21179] LOG:  database system is shut down

如果恢复进程发现了损坏的 WAL 数据,恢复将立刻停止且服务器无法正常启动。这种情况下可以在损坏点之前指定一个新的恢复目标,然后重新运行恢复过程。

如果恢复由于外部原因而失败,比如系统崩溃或 WAL 归档无法访问,可以简单地重新启动服务器继续恢复,恢复进程会从失败的地方重新开始。恢复重启的工作方式很像普通的检查点:服务器定期将其所有状态刷新到磁盘,然后更新 pg_control 文件记录处已理完成的 WAL 数据,在重启时不再需要重新处理这些数据。

时间线

pg 中引入了时间线的概念,每一次恢复成功后都会基于恢复的时间线创建一条新的时间线,时间线主要体现在 WAL 段文件的命名中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
postgres=# SELECT timeline_id FROM pg_control_checkpoint();
 timeline_id
-------------
           2
(1 row)
postgres=# SELECT pg_walfile_name(pg_current_wal_lsn());
     pg_walfile_name
--------------------------
 000000020000000000000086
(1 row)

每次创建一个新的时间线 pg 还会创建一个时间线历史文件 <timeline-ID>.history,记录了该时间线的父时间线。当从包含多个时间线的存档中恢复时,pg 需要时间线历史文件来不断往前回溯,选择正确的 WAL 段文件序列。因此时间线历史文件也会归档到 WAL 归档位置。

引入时间线的好处是,用户可以基于同一备份恢复之后再备份,通过时间线模拟出一条树状的备份历史,然后方便地在不同分叉之间定位和切换,不需要担心它们之间因命名冲突等原因造成混乱。在某些场景下时间线非常有用,通常我们会直接使用 WAL 归档中最近的时间线。

总结

持续归档的基础备份是一个有开始和结束动作的阶段性过程:

  • 开始时 pg 会执行检查点并强制开启 full_page_writes,在开始到结束期间任何时间点的文件系统备份都可以回退到开始时间点进行重放。
  • 结束时 pg 会强制切换一次 WAL 段日志,文件系统备份重放到结束时被归档的最后一条 WAL 段日志就会使数据库到达一致状态。

因此一份完整有效的备份,必须包含文件系统备份特定的 WAL 段日志

作为久负盛名的开源数据库,pg 的持续归档可以评价为「大道至简」:整个过程由简单、基础的数据库指令和 OS 工具协作完成,完美契合 Unix 哲学;归档和恢复的关键过程(archive_commandrestore_command)完全交给用户,具备了极高的可扩展性,但也对管理员的技术水平有一定的要求。

参考链接

updatedupdated2023-06-062023-06-06