PostgreSQL 增量备份集的有效恢复位点
背景
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篇)双机HA与块级备份部署》
《PostgreSQL 最佳实践 - 块级增量备份(ZFS篇)单个数据库采用多个zfs卷(如表空间)时如何一致性备份》
《PostgreSQL 最佳实践 - 块级增量备份(ZFS篇)备份集自动校验》
《PostgreSQL 最佳实践 - 块级增量备份(ZFS篇)方案与实战》
《PostgreSQL 最佳实践 - 块级别增量备份(pg_rman baseon LSN)源码浅析与使用》
《PostgreSQL 最佳实践 - 任意时间点恢复源码分析》
《PostgreSQL 最佳实践 - 在线增量备份与任意时间点恢复》