PostgreSQL 增量备份集的有效恢复位点

5 minute read

背景

PostgreSQL支持PITR即时间点恢复,为了支持时间点恢复,至少需要一次全量备份,然后需要归档日志。

这句话描述可能不够清晰,至少需要哪些归档日志,全量备份的时间点有没有要求呢?

本文要解答这个问题。

什么是全量备份

全量备份指的是对数据库的$PGDATA以及所有表空间文件(包括全局数据文件、事务日志文件、配置文件、控制文件、表空间数据文件等)进行一次全量的拷贝。

一个数据库的目录结构通常如下

cd $PGDATA  
drwx------ 6 digoal digoal 4.0K Aug 13 08:23 base  
-rw------- 1 digoal digoal   44 Aug 23 00:00 current_logfiles  
drwx------ 2 digoal digoal 4.0K Aug 16 11:28 global  
drwx------ 2 digoal digoal 4.0K Aug 23 00:00 log  
drwx------ 2 digoal digoal 4.0K Aug 13 07:25 pg_commit_ts  
drwx------ 2 digoal digoal 4.0K Aug 13 07:25 pg_dynshmem  
-rw------- 1 digoal digoal 4.5K Aug 13 07:25 pg_hba.conf  
-rw------- 1 digoal digoal 1.6K Aug 13 07:25 pg_ident.conf  
drwx------ 4 digoal digoal 4.0K Aug 23 10:18 pg_logical  
drwx------ 4 digoal digoal 4.0K Aug 13 07:25 pg_multixact  
drwx------ 2 digoal digoal 4.0K Aug 16 11:28 pg_notify  
drwx------ 2 digoal digoal 4.0K Aug 13 07:25 pg_replslot  
drwx------ 2 digoal digoal 4.0K Aug 13 07:25 pg_serial  
drwx------ 2 digoal digoal 4.0K Aug 13 07:25 pg_snapshots  
drwx------ 2 digoal digoal 4.0K Aug 16 11:28 pg_stat  
drwx------ 2 digoal digoal 4.0K Aug 23 13:57 pg_stat_tmp  
drwx------ 2 digoal digoal  20K Aug 14 13:13 pg_subtrans  
drwx------ 2 digoal digoal 4.0K Aug 13 07:25 pg_tblspc  
drwx------ 2 digoal digoal 4.0K Aug 13 07:25 pg_twophase  
-rw------- 1 digoal digoal    3 Aug 13 07:25 PG_VERSION  
drwx------ 3 digoal digoal 1.3M Aug 23 10:18 pg_wal  
drwx------ 2 digoal digoal 4.0K Aug 14 22:00 pg_xact  
-rw------- 1 digoal digoal 2.2K Aug 13 07:26 postgresql.auto.conf  
-rw------- 1 digoal digoal  23K Aug 16 11:25 postgresql.conf  
-rw------- 1 digoal digoal   34 Aug 16 11:28 postmaster.opts  
-rw------- 1 digoal digoal   90 Aug 16 11:28 postmaster.pid  

注意pg_tblspc里面是软链接,这里面对应的是表空间目录。也需要备份。

全量备份可以通过COPY文件的方式,给文件系统、块设备打快照的方式,等进行全量的备份。

COPY文件可以使用操作系统的命令(如果这么做,建议你考虑到软链接的问题,一定要记得备份实际的文件)。

如果是远程备份,可以配置数据库的流复制,通过pg_basebackup命令进行流式的备份,这样你不需要考虑表空间需要单独备份的问题,pg_basebackup会帮你做掉。

不管何种方式备份(除了pg_basebackup),都需要你执行pg_start_backup(‘’),这样数据库会做一次检查点,同时强制打开full page write(确保即使某些用户关闭了full page write,备份还是有效的。),拷贝完后,执行pg_stop_backup()。注意pg_stop_bacup()之前,你的备份是无效的。所以备份完成一定要记得pg_stop_backup()。后面会说为什么要这么做。这些步骤pg_basebackup会自动帮你做。

什么是不一致拷贝

因为全量备份是不需要停库,也不影响业务的。属于热备份。

热备份比如会带来一个问题,例如用户在写数据,数据库在刷脏页等操作,你在备份时可能拷贝走的文件是partial block,一个块中有一半新的一半旧的数据。造成不一致。

不过你不需要担心这个不一致的问题,因为PG考虑到了,并且有方法解决它。

这也是为什么需要执行pg_start_backup(),开启full page write的原因。开启full page write后,检查点之后,任何一个BLOCK第一次变成dirty block时,都会往WAL里面写下完整的数据块。

通过wal的完整数据块,可以修复备份过程中拷贝走的不一致数据块。

什么是一致性位点

既然备份走的文件里有不一致的数据块,以及PG有不一致的修复方法。那么就一定有一致的位点。

什么是一致的位点呢?就是指数据库认为所有的数据块都是一致的,没有partial write(一半新、一半旧)的情况。

什么是检查点,数据库DOWN机、服务器DOWN机如何恢复到一致性状态

检查点是数据库的一致性点,做检查点的目的是将SHARED BUFFER中的脏页刷到磁盘中持久化。同时开启full page write的情况下,检查点之后第一次变成dirty block时,都会往WAL里面写下完整的数据块。

做检查点可能需要一定的时间,这段时间随着数据库的读写,会产生一些WAL,因此检查点对应到WAL文件中,有一个开始位置和结束位置,比如开始位置在WAL文件A中,结束位置在WAL文件F中。

当数据库服务器异常DOWN机时,是需要恢复的。从数据库最后一次完成的检查点的WAL开始位置获取WAL开始恢复,一直恢复到检查点的逻辑位置结束位置为止。

即要恢复到一致位点,需要从A到F(CKPT的结束RECORD),只有到了这里,数据库才是一致的状态。

备份集如何恢复到一致性位点

那么备份集如何达到一致位点呢?

其实原理和检查点差不多,通过全量备份集来恢复,至少也要恢复到pg_stop_backup()的位置。

例如早上9点开始全量备份,早上11点备份结束,备份期间(9到11点)产生了A-F这些WAL文件。

那么这个备份集必须要包含A-F这些文件,才能恢复到一致性的位点。

为什么这么说,你想象一下,假设你在10点59快要备份结束的时候,数据库产生了一个脏页,并write到磁盘,此时你刚好拷贝到了这个partial block。而这个BLOCK是最后一次检查点之后第一次变更的BLOCK,那么你的备份集如果要恢复到一致性位点,必须使用F这个文件内(包含了这个块的FULL PAGE)来恢复这个BLOCK到一致的状态。

是不是很好理解呢?

什么情况下数据库会处于recovery状态不起来

当数据库在恢复时,如果没有达到一致性的状态(即前面提到的,没有恢复到必要的WAL位点)时,数据库会处于recovery状态无法连接。即使使用了standby模式,也一样。

采取什么措施

1. 继续获取更多的WAL,并恢复WAL,直到恢复到一致性点的WAL。(例如检查点逻辑位置,或者全量备份pg_stop_backup()的位置)。

2. 如果你没有足够的wal可用来恢复(无法达到一致性位点),怎么办呢?你可以强制promote激活。但是有可能遇到块错误的风险。当读取到这类数据块时会报错,可以使用zero demage block隐藏参数来跳过它。或者使数据库将这些块设置为INIT状态(使用vacuum freeze可以处理它)。

为什么检查点不适合跨度太大

postgresql有参数控制检查点的跨度,检查点做得太快(跨度小),检查点做得慢(跨度大)。

当数据库很繁忙时,一个检查点可能会跨越若干个WAL文件,为了到达一致性的位点,至少需要APPLY这些跨越的WAL文件才行。

最好的办法:

数据库产生脏页多,并且很繁忙时,跨度大一点,避免检查点引入的FSYNC IO开销影响业务。

数据库产生的脏页小,不繁忙时,跨度小一点,让数据库快速的到达一致性状态。

好在PostgreSQL 9.6开始就支持根据负载动态的调整检查点的跨度了。

PITR(时间点恢复)一致性的要素总结

1、数据库有两个一致性的点,检查点结束位点、全量备份的pg_stop_backup()位点。

为了让数据库可以恢复到一致性位点,需要足够的WAL,恢复到这两个位点以上,数据库才是一致的状态。

开启full page write,并且做好WAL归档是非常重要的,确保你在任意时候都可以恢复到一致的状态。

2、不要让检查点的时间太长,这样可以让数据库快速达到一致性状态。(PG 9.6已经支持动态CKPT调度,很棒吧)

3、recovery.conf中有一个参数,恢复到一致性的点即停止并pause或promote,是一个很不错的developor参数。

什么是时间线、恢复时如何利用时间线文件

时间线是数据库promote的时候产生的,当PostgreSQL的standby节点从只读节点变成读写节点时,会自动创建一个时间线文件。时间线文件是用于标记数据库是什么时候激活的。

时间线文件中包含了新时间线的第一条WAL记录的位置,也即是上一个时间线的最后一笔WAL RECORD的结束位置。

cat 00000002.history   
1       660/95B6F2A0    no recovery target specified --- 意思是恢复到这里,你就不要再恢复时间线1的WAL了。请切到时间线2。  

分析时间线1切换时的WAL文件

pg_waldump 000000010000066000000095 less
rmgr: Heap        len (rec/tot):    911/   911, tx:  474860760, lsn: 660/95B66B80, prev 660/95B66B58, desc: HOT_UPDATE off 12 xmax 474860760 ; new off 14 xmax 0, blkref #0: rel 1663/13146/2619 blk 9655  
rmgr: Heap        len (rec/tot):     54/    54, tx:  474860760, lsn: 660/95B66F10, prev 660/95B66B80, desc: LOCK off 13: xid 474860760: flags 0 LOCK_ONLY EXCL_LOCK , blkref #0: rel 1663/13146/2619 blk 9655  
rmgr: Heap        len (rec/tot):     73/ 31477, tx:  474860760, lsn: 660/95B66F48, prev 660/95B66F10, desc: UPDATE off 13 xmax 474860760 ; new off 13 xmax 0, blkref #0: rel 1663/13146/2619 blk 9658 FPW, blkref #1: rel 1663/13146/2619 blk 9655  
rmgr: Btree       len (rec/tot):     64/    64, tx:  474860760, lsn: 660/95B6EAA0, prev 660/95B66F48, desc: INSERT_LEAF off 1384, blkref #0: rel 1663/13146/2696 blk 126  
rmgr: Heap        len (rec/tot):   1251/  1251, tx:  474860760, lsn: 660/95B6EAE0, prev 660/95B6EAA0, desc: HOT_UPDATE off 10 xmax 474860760 ; new off 11 xmax 0, blkref #0: rel 1663/13146/2619 blk 9657  
rmgr: Heap        len (rec/tot):    184/   184, tx:  474860760, lsn: 660/95B6EFC8, prev 660/95B6EAE0, desc: INPLACE off 14, blkref #0: rel 1663/13146/1259 blk 0  
rmgr: Transaction len (rec/tot):     34/    34, tx:  474860760, lsn: 660/95B6F080, prev 660/95B6EFC8, desc: COMMIT 2017-08-23 09:46:13.592320 CST  
rmgr: XLOG        len (rec/tot):    106/   106, tx:          0, lsn: 660/95B6F0A8, prev 660/95B6F080, desc: CHECKPOINT_ONLINE redo 660/95B6F0A8; tli 1; prev tli 1; fpw true; xid 0:474860761; oid 683262; multi 1; offset 0; oldest xid 274864396 in DB 13146; oldest multi 1 in DB 13146; oldest/newest commit timestamp xid: 0/0; oldest running xid 0; online  
rmgr: XLOG        len (rec/tot):    106/   106, tx:          0, lsn: 660/95B6F118, prev 660/95B6F0A8, desc: CHECKPOINT_SHUTDOWN redo 660/95B6F118; tli 1; prev tli 1; fpw true; xid 0:474860761; oid 675073; multi 1; offset 0; oldest xid 274864396 in DB 13146; oldest multi 1 in DB 13146; oldest/newest commit timestamp xid: 0/0; oldest running xid 0; shutdown  
rmgr: XLOG        len (rec/tot):    106/   106, tx:          0, lsn: 660/95B6F188, prev 660/95B6F118, desc: CHECKPOINT_SHUTDOWN redo 660/95B6F188; tli 1; prev tli 1; fpw true; xid 0:474860761; oid 675073; multi 1; offset 0; oldest xid 274864396 in DB 13146; oldest multi 1 in DB 13146; oldest/newest commit timestamp xid: 0/0; oldest running xid 0; shutdown  
rmgr: XLOG        len (rec/tot):     50/    50, tx:          0, lsn: 660/95B6F1F8, prev 660/95B6F188, desc: PARAMETER_CHANGE max_connections=1000 max_worker_processes=128 max_prepared_xacts=0 max_locks_per_xact=6400 wal_level=replica wal_log_hints=off track_commit_timestamp=off  
rmgr: XLOG        len (rec/tot):    106/   106, tx:          0, lsn: 660/95B6F230, prev 660/95B6F1F8, desc: CHECKPOINT_SHUTDOWN redo 660/95B6F230; tli 1; prev tli 1; fpw true; xid 0:474860761; oid 675073; multi 1; offset 0; oldest xid 274864396 in DB 13146; oldest multi 1 in DB 13146; oldest/newest commit timestamp xid: 0/0; oldest running xid 0; shutdown  
----- 这是上一个时间线文件的内容,最后一条即时间线文件中的前一条RECORD。  

分析时间线2的第一个WAL文件。

pg_waldump 000000020000066000000095 less
rmgr: XLOG        len (rec/tot):     50/    50, tx:          0, lsn: 660/95B6F1F8, prev 660/95B6F188, desc: PARAMETER_CHANGE max_connections=1000 max_worker_processes=128 max_prepared_xacts=0 max_locks_per_xact=6400 wal_level=replica wal  
_log_hints=off track_commit_timestamp=off  
rmgr: XLOG        len (rec/tot):    106/   106, tx:          0, lsn: 660/95B6F230, prev 660/95B6F1F8, desc: CHECKPOINT_SHUTDOWN redo 660/95B6F230; tli 1; prev tli 1; fpw true; xid 0:474860761; oid 675073; multi 1; offset 0; oldest xid 27  
4864396 in DB 13146; oldest multi 1 in DB 13146; oldest/newest commit timestamp xid: 0/0; oldest running xid 0; shutdown  
-----这条之前的WAL都是继承自上一个时间线的,000000020000066000000095就是从000000010000066000000095复制出来的文件。  
rmgr: XLOG        len (rec/tot):     42/    42, tx:          0, lsn: 660/95B6F2A0, prev 660/95B6F230, desc: END_OF_RECOVERY tli 2; prev tli 1; time 2017-08-23 16:48:43.384886 CST  
rmgr: Standby     len (rec/tot):     50/    50, tx:          0, lsn: 660/95B6F2D0, prev 660/95B6F2A0, desc: RUNNING_XACTS nextXid 474860761 latestCompletedXid 474860760 oldestRunningXid 474860761  
rmgr: Standby     len (rec/tot):     50/    50, tx:          0, lsn: 660/95B6F308, prev 660/95B6F2D0, desc: RUNNING_XACTS nextXid 474860761 latestCompletedXid 474860760 oldestRunningXid 474860761  
rmgr: XLOG        len (rec/tot):    106/   106, tx:          0, lsn: 660/95B6F340, prev 660/95B6F308, desc: CHECKPOINT_ONLINE redo 660/95B6F2D0; tli 2; prev tli 2; fpw true; xid 0:474860761; oid 675073; multi 1; offset 0; oldest xid 274864396 in DB 13146; oldest multi 1 in DB 13146; oldest/newest commit timestamp xid: 0/0; oldest running xid 474860761; online  
rmgr: Standby     len (rec/tot):     50/    50, tx:          0, lsn: 660/95B6F3B0, prev 660/95B6F340, desc: RUNNING_XACTS nextXid 474860761 latestCompletedXid 474860760 oldestRunningXid 474860761  

recovery.conf文件中包含了对时间线的这样一段描述。

当用户恢复数据库时,如果要恢复到当前控制文件更大的时间线,(即跨时间线恢复,恢复到另一个激活的数据库),那么需要设置为latest。

# If you want to recover into a timeline other than the "main line" shown in  
# pg_control, specify the timeline number here, or write 'latest' to get  
# the latest branch for which there's a history file.  
#  
#recovery_target_timeline = 'latest'  

在了解了时间线的原理后,我们就需要注意一件事情。

如果你的系统中有非同步流复制关系的主备,并且主备都有归档文件时,千万不要搞错了他们的关系。

例如

1、A是主库

2、B是异步备库

3、A产生了一堆WAL归档。

4、某一时刻t1,做了一个全量备份

5、之后的某一时刻t2,HA程序认为A挂了,把B激活成为新的主库,激活时产生了几个文件:上一个时间线的最后一个WAL,新时间线的第一个WAL,以及一个新时间线文件,告诉你切换的WAL位点是,1 660/95B6F2A0 no recovery target specified,然后

6、由于是异步模式,A还有一些WAL没有发给B。

7、用户想恢复到t3的某一时刻,

8、如果你需要用老的备份集恢复到t3,那么就涉及到跨时间线恢复。注意当恢复需要用到B激活时的临界WAL文件时,千万不要使用A归档的临界WAL文件来恢复,否则会在老的时间线越走越远。

9、为了让恢复走上新时间线的道路,需要具备B上面产生的三个文件:上一个时间线的最后一个WAL,新时间线的第一个WAL,以及一个新时间线文件。

建议:

在切换时间线后,使用新的主库做一次全量备份。

数据库备份、恢复、容灾最佳实践文档

《PostgreSQL on ECS多云盘的部署、快照备份和恢复》

《PostgreSQL 最佳实践 - 块级增量备份(ZFS篇)验证 - recovery test script for zfs snapshot clone + postgresql stream replication + archive》

《PostgreSQL 最佳实践 - 块级增量备份(ZFS篇)双机HA与块级备份部署》

《PostgreSQL 最佳实践 - 块级增量备份(ZFS篇)单个数据库采用多个zfs卷(如表空间)时如何一致性备份》

《PostgreSQL 最佳实践 - 块级增量备份(ZFS篇)备份集自动校验》

《PostgreSQL 最佳实践 - 块级增量备份(ZFS篇)方案与实战》

《PostgreSQL 最佳实践 - 块级别增量备份(pg_rman baseon LSN)源码浅析与使用》

《PostgreSQL 最佳实践 - 任意时间点恢复源码分析》

《PostgreSQL 最佳实践 - 在线增量备份与任意时间点恢复》

Flag Counter

digoal’s 大量PostgreSQL文章入口