实例恢复的原理


数据库宕机原因

导致数据库实例异常终止的原因有以下几种:

·内存不足时被OOM Killer或用户kill掉。

·操作系统崩溃。

·硬件故障导致机器停机或重启。


PG如何保证恢复后数据不丢失

但只要磁盘上的数据没有丢失,PostgreSQL就能保证数据不会丢失,这里说的不丢数据是指如下情况:

·数据库实例还能再次启动,如果数据库无法启动,很多时候相当于数据全部丢失,PostgreSQL会全力保证这种情况不会发生。

·已提交的数据,数据库重启后还在。不会出现数据错乱的情况。

数据库一般是通过重做日志来保证不丢失数据的,每项操作都记录到重做日志中,实例重新启动后,重演(replay)重做日志,这个动作称为“前滚”。前滚完成后,多数数据库还会把未完成的事务取消掉,就像这些事务从来没有执行过一样,这个动作称为“回滚”。在前滚过程中,数据库是不能被用户访问的。每次前滚时,从哪个点开始?


Checkpoint在恢复中的作用

Checkpoint点的概念产生了,每次Checkpoint操作之后都保证Checkpoint点之前的数据已持久化到硬盘中了,实例恢复是,只需要从上一次的Checkpoint点开始重演重做日志就可以了,因为这个点之前的数据已经持久化了,不需要再重演该点之前的重做日志了。在PostgreSQL数据库中重做日志叫WAL日志,即“Write Ahead Log”的缩写。

上一次Checkpoint发生的时间越久,如果数据库异常宕机,则做实例恢复需要的时间就越长。为了保证数据库异常宕机后能在可控的时间内恢复实例,PostgreSQL提供了参数“checkpoint_timeout”来控制Checkpoint发生的间隔时间,默认参数值是5分钟,一般情况下够用了。每次Checkpoint会把内存中的脏数据强制写到磁盘中,这会产生大量I/O。如果增加Checkpoint的时间间隔就会减少一些I/O,为什么呢?想象这个场景:如果一个内存中的数据块在发生两次Checkpoint的间隔中被修改了多次,则Checkpoint时只会写一次I/O,所以加长Checkpoint的时间间隔,这段时间内,即使数据块被修改了多次,也只会产生一次I/O。

上一次发生的Checkpoint点是记录在控制文件中的,所以没有控制文件,数据库也是无法启动的。

这里面还有一个问题,从上一次的Checkpoint点开始重演重做日志,这些重做日志中有一部分已经执行过了,再次重演是否会产生问题?所以这里有一些约束,也就是重演重做日志要求是“幂等”,“幂等”就是不管执行过多少次,都能获得同样的结果,否则就会产生问题。PostgreSQL的数据块大小是8KB,在异常宕机时,有可能写数据块只是写了一部分,这时重演重做日志就不一定是“幂等”,为了避免这种情况,PostgreSQL提供了参数“full_page_writes”,当这个参数设置为“on”时,当一个数据块前一次Checkpoint后在发生第一次变化时,会把该数据块的全部内容写入到重做日志中,当开始恢复时,先把该数据库的全部内存恢复出来,然后应用后续这个数据块的变化,从而防止发生数据块损坏的问题。当然,数据块只有前一次Checkpoint后在发生第一次变化时才记录整个数据块的内容到重做日志中,第二次即之后变化时,只记录变化,不再记录整个数据块。所以从这个原理来讲,加长Checkpoint的时间间隔,也能尽可能地减少重做日志的产生量。

Oracle或其他数据库的DBA可能会奇怪,PostgreSQL数据库是没有回滚段的,那么PostgreSQL数据库在实例恢复的阶段会把未完成的事务回滚掉吗?实际上既不会也不需要,因为这些未完成的事务的状态在CommitLog文件中不是已提交状态,其产生的数据对于其他人来说是不可见的,也就是不用管,最后等VACUUM操作把这些垃圾数据清理掉就可以了。对于其他数据库来说,回滚操作也需要记录重做日志,所以整个恢复过程比PostgreSQL更复杂。另外,因为PostgreSQL数据库没有回滚段,所以也不会有因回滚段损坏导致数据库无法启动的烦恼。


热备份的原理

热备份流程

我们通过底层执行热备份的过程如下:

·调用函数pg_start_backup()开始热备份,如“select pg_start_backup('osdba201901282217');”。

·使用你熟悉的文件系统备份工具(如tar或者cpio,不是pg_dump或者pg_dumpall)来执行备份。

·调用函数pg_stop_backup()结束热备份,如“SELECT * FROM pg_stop_backup();”。

·把热备份开始时的WAL日志文件全部进行备份。


热备恢复流程

·使用热备份做恢复的过程如下:

·把备份的WAL文件复制到备份文件集中的pg_wal目录中。

·启动数据库即可完成恢复。

在上述过程中,我们首先会有一个疑问:使用熟悉的文件系统备份工具备份正在运行的数据库,备份文件是不一致的,这是因为备份会持续一段时间,先复制的和后复制的文件或即使同一个文件中不同部分的内容,也不是同一个时间点的内容,在备份文件集上启动数据库后,数据明显是不一致的,这怎么能行呢?但实际上我们打开数据库后会发现数据却是一致的,其中的奥秘就是热备份使用备份过程中产生的WAL纠正了不一致的日志,简单来说就是通过重演这些WAL文件把数据文件集恢复到一致的状态,这就是热备份的最基本的原理。

具体执行过程的解释如下:

执行函数pg_start_backup()时,会对数据库做一次Checkpoint,同时会把这次Checkpoint的点记录到一个特殊的文件中,即backup_label文件,该文件中有如下内容:

START WAL LOCATION: 0/20000028 (file 000000010000000000000020)
CHECKPOINT LOCATION: 0/20000060
BACKUP METHOD: pg_start_backup
BACKUP FROM: master
START TIME: 2019-01-28 23:37:38 CST
LABEL: osdba201901282217
START TIMELINE: 1

从上面的文件内容中可以看到开始备份时WAL日志的位置。这个backup_label在源数据库的数据目录下。

然后我们不管是开始tar还是使用scp做文件系统的备份时,backup_label都会被复制到备份中。当我们执行pg_stop_backup()时,backup_label文件会被删除,当然在删除该文件之前已经将其复制到了备份中。当我们在备份文件集中启动数据库时,数据库开始做实例恢复,但因为存在backup_label文件,其不会从备份集中的控制文件中指定的上一次Checkpoint点开始应用WAL日志,而是从backup_label文件中指定的WAL日志点开始恢复数据库,然后不停地应用WAL日志,把数据文件推到一个一致点。这里有一个问题,不停地应用WAL日志,什么时候才能知道到达了备份结束的时候呢?如果没有到备份的结束时间点,打开数据库还是会出现不一致的情况。这个过程是靠pg_stop_backup()来实现的,调用pg_stop_backup()时会在数据库的WAL日志中写入一条“XLOG_BACKUP_END”记录,当应用到这条“XLOG_BACKUP_END”记录时就可以知道数据库的备份结束了,数据到达了一致点,这时候数据库就可以对外提供服务了,且不必再担心数据不一致问题。

因为执行pg_start_backup()后,在数据目录中生成了一个固定名字的backup_label文件,所以不能再次执行pg_start_backup(),否则会再次生成一个backup_label文件,这会导致数据混乱。所以这种备份只能启动一个,不能对主库同时启动多个备份,这种备份称为独占型备份(Exclusive Backup)。

除了不能对同一个数据库同时启动多个备份的缺点外,独占型备份实际上还有一个更严重的缺点。如果主库在备份中突然宕机了,backup_label文件就会留在主库的目录中,这时候启动主库,PostgreSQL会分不清这是主库的情况还是备份集做恢复的情况,就会按备份集做恢复的情况对数据库做恢复操作。如果数据库不大,备份持续的时间很短,备份过程中没有发生新的Checkpoint,就不会有太大问题,但如果发生了Checkpoint,数据库原本应该从最后一次的Checkpoint开始恢复,但却从更早的时间开始恢复,会导致数据库的恢复时间变长,更严重的是,发生Checkpoint之后,该Checkpoint之前的WAL文件可能被删除了,而从更早的时间恢复数据库的操作会因为找不到更早的WAL文件而导致数据库无法启动,此时报如下错误:

ERROR:  could not find redo location referenced by checkpoint record
HINT: If you are not restoring from a backup, try removing the file "backup_label".


当然解决方法也很简单,就是删除主库数据目录下的backup_label文件,但这增加了人工操作。所以PostgreSQL 9.1版本开始提供的pg_basebackup工具提供了另一种备份方式,称为“非独占型备份”方式。也就是说,使用pg_basebackup备份不会遇到上述问题。但pg_basebackup使用一个连接到主库上去备份,没有并发,而我们前面讲的手工备份可以同时启动几个scp同时拷贝文件,可以并发。于是从PostgreSQL 9.6版本开始把“非独占型备份”的功能以底层API的方式暴露出来给大家使用:

原先的pg_start_backup()函数只有两个参数,现在增加了第三个参数“exclusive”,我们使用“SELECT pg_start_backup('osdba202001282217',false,false);”就可以启动非独占型备份,在非独占型备份模式下不会在主库中产生backup_label文件,从而不会产生前面所讲的独占型备份面临的问题。

当然,结束备份时也需要以非独占式的方式结束备份:

postgres=# SELECT * FROM pg_stop_backup(false);
lsn | labelfile | spcmapfile
------------+---------------------------------------------------------------+---
0/1D000130 | START WAL LOCATION: 0/1D000028 (file 00000001000000000000001D) +|
| CHECKPOINT LOCATION: 0/1D000060 +|
| BACKUP METHOD: streamed +|
| BACKUP FROM: master +|
| START TIME: 2020-01-28 22:52:54 CST +|
| LABEL: osdba202001282217 +|
| START TIMELINE: 1 +|
| |
(1 row)


以非独占式的方式结束备份,实际上就是给函数pg_stop_backup()增加了一个参数“exclusive”,当然pg_stop_backup函数的返回值也与原来不一样了,其返回了原先backup_label中的内容。我们可以手动创建备份文件集中的backup_label,再把上面的内容填入即可。

当然,为了恢复的“幂等”,不管你是否设置了参数“full_page_writes”为“on”,在备份过程中总是会强制达到“full_page_writes”为“on”的效果,所以热备份可能会导致主库产生更多的WAL日志。