PolarDB-PG块级增量备份
背景
PolarDB-PG很多客户的全量备份集大小日益增长,达到了10TB量级,每天进行全量备份消耗大量存储空间,备份IO压力大;另外,备份时间长,由于网络问题、数据库不可用引起的备份失败概率也会增大。此外,RTO指标是线下大客户比较关心的重要指标,PG回放wal日志效率一直比较低下,RTO指标比较差,引入增量块备份,减少wal日志回放,能有效提升RTO指标。
原理
减少冗余文件备份,以全量备份集为基础,块级增量备份只备份上一次全量备份/增量备份到本次增量备份周期内的变化的文件block。
整体流程
备份以7天为一个周期,第1天进行全量备份,第2-7天进行块级增量备份。增量wal日志持续进行备份。
全量恢复,指定某个全量备份集或者块级增量备份进行恢复,如果指定全量备份集,则恢复该全量备份集即可;如果指定块级增量备份集,则需先恢复距离最近的全量备份集,再恢复从最近的全量备份集到当前块级增量备份集之间的所有块级增量备份集,即进行Merge操作。
指定时间点恢复,与全量恢复类似。先恢复距离指定时间点最近的全量备份集,再恢复从最近的全量备份集到指定时间点之前所有的块级增量备份集,最后恢复从最后一个块级增量备份集到指定时间点之间的所有的wal日志。
详细设计
Block变化跟踪
块级增量备份需要跟踪一定周期内文件变化的block,具体方式有以下几种,解析wal日志或者闪回日志,或者通过ptrack记录,即在pg内核引入记录机制,每次修改文件block的操作都在线记录下来。
typedef struct BlockBackupTracer
{
map_t backup_file_map;
SyncLocalCB syncLocal;
SyncBlockCB syncBlock;
}BlockBackupTracer;
typedef struct pgFile
{
char *rel_path;
datapagemap_t pagemap;
}pgFile;
struct datapagemap
{
char *bitmap;
int bitmapsize;
};
a. wal解析
下图为wal日志通过pg_waldump解析的结果,从解析结果可知,wal record会记录db修改的每一个block。因此,可以通过解析wal日志的每一条record,从中找出block变化的信息,并记录下来。

b. 闪回日志解析
PolarDB-PG有闪回功能,每次闪回点(flashback point)都会记录距离上一次闪回点之间发生变化的block,因此可以考虑解析闪回日志,从中获取变化的block。
c. ptrack
PG社区为了支持块级增量备份研发的功能,即在pg内核引入记录机制,每次修改文件block的操作都在线记录下来,具体见下面链接。
https://gist.github.com/lubennikovaav/475a1ac3d394ea08231966a07fecdbde
上面三种方式,考虑到通过闪回功能并未常态化打开,ptrack功能对内核改动过大,而解析wal日志比较成熟。因此,最后决定采取wal解析的方式记录变化的block。具体的方式就是,启动一个独立的日志归档进程,在对日志归档的同时,解析日志内容,将变化的block记录下来,持久化到Block变化索引文件。
增量备份详细步骤
a. select pg_start_backup()
与全量备份类型,连接db执行pg_start_backup,即对db执行checkpoint,产生backup_label文件,并设置Full Page Write。
b. 备份backup_label文件
backup_label文件记录了备份开始的时间、wal日志开始位置等信息,将该文件拷贝到备份集。
START WAL LOCATION: 13/400024C8 (file 000000020000001300000001)
CHECKPOINT LOCATION: 13/40002530
BACKUP METHOD: pg_start_backup
BACKUP FROM: master
START TIME: 2021-06-21 16:28:22 UTC
LABEL: true
START TIMELINE: 2
c. 备份文件列表
readdir获取当前文件列表,并记录下来。
d. 解析记录block变化的索引文件,备份block
for {
ok, block := page.datapagemap_next()
if !ok {
break
}
var hbbuf bytes.Buffer
var header BackupPageHeader
header.Block = block
header.Block_size = 8192
binary.Write(&hbbuf, binary.LittleEndian, header)
Copy(tarFile.tw, &hbbuf)
frontendParam["file"] = frontendFile
frontendParam["offset"] = block * DefaultBlockSize
frontend.funcs["seek"](ctx.frontend.handle, frontendParam)
CopyBufferN(tarFile.tw, frontendReader, buf, DefaultBlockSize)
}

在上一章节我们提到,启动一个独立的日志归档进程,在对日志归档的同时,解析日志内容,将变化的block记录下来,持久化到Block变化索引文件。在增量备份的时间,会解析这些Block变化索引文件,识别出需要备份的文件Block,并将这些Block都拷贝到备份集里。
e. 备份pg_control文件
pg_control version number: 1100
Catalog version number: 201809051
Database system identifier: 6931247945573748603
Database cluster state: in production
pg_control last modified: Sat 19 Jun 2021 10:59:50 AM CST
Latest checkpoint location: 13/35B0
Latest checkpoint's REDO location: 13/3548
Latest checkpoint's REDO WAL file: 000000020000001300000000
Latest checkpoint's TimeLineID: 2
Latest checkpoint's PrevTimeLineID: 2
Latest checkpoint's full_page_writes: on
Latest checkpoint's NextXID: 0:6087507
Latest checkpoint's NextOID: 41168
Latest checkpoint's NextMultiXactId: 1
Latest checkpoint's NextMultiOffset: 0
Latest checkpoint's oldestXID: 1201
Latest checkpoint's oldestXID's DB: 1
Latest checkpoint's oldestActiveXID: 6087506
Latest checkpoint's oldestMultiXid: 1
Latest checkpoint's oldestMulti's DB: 1
Latest checkpoint's oldestCommitTsXid:0
Latest checkpoint's newestCommitTsXid:0
Time of latest checkpoint: Sat 19 Jun 2021 10:59:40 AM CST
...
f. select pg_stop_backup()
与全量备份类似,执行pg_stop_backup函数,退出备份状态。
g. 备份wal日志文件
与全量备份类似,全量备份需要拷贝拷贝数据文件期间所产生的增量wal日志,块级增量备份期间产生的wal日志也需要拷贝到备份集。
块级增量备份以上的几个步骤,基本上与全量备份过程类似。核心步骤是步骤d,通过解析block变化索引文件,将全量文件拷贝降级为增量的数据拷贝。
恢复增量备份详细步骤
以按时间点还原(PITR) 为例,恢复增量备份到指定时间有以下几个步骤:
a. 制定恢复策略
根据恢复时间点,从元数据库或者备份集中进行查询,找出应该恢复的全量备份集、块级增量备份集以及wal日志文件
b. 恢复全量备份集
先下载解压全量备份集到恢复指定的目录。
c. 恢复块级增量备份集
读取全量备份集恢复的目录,获取文件列表list_old,读取块级增量备份集获取文件列表list_new,遍历list_old,如果文件在list_new中不存在,则删除文件。
totalSize := hdr.Size
var wsz int64 = 0
for {
if wsz >= totalSize {
break
}
get header
var hbbuf bytes.Buffer
var header BackupPageHeader
n, _, err := CopyBufferN(&hbbuf, curInput, buf, hlen, nil)
wsz = wsz + hlen
binary.Read(&hbbuf, binary.LittleEndian, &header)
seek
block := header.Block
offset := block * DefaultBlockSize
frontendParam["file"] = frontendFile
frontendParam["offset"] = int(block * DefaultBlockSize)
_, err = ctx.frontend.funcs["seek"](frontend.handle, frontendParam)
copy one page
n, _, err = CopyBufferN(frontendWriter, curInput, buf, DefaultBlockSize, nil)
wsz = wsz + n
}
增量备份恢复,涉及对PG文件的以下几种操作。
d. 恢复wal日志文件
下载增量备份级里的wal日志文件到指定恢复的目录。
e. 设置recovery.conf
recovery_target_time = '2021-06-16 16:01:40'
...
设置数据库的recovery.conf,恢复到某个指定时间点。
f. 拉起数据库
pg_ctl start -D $PGDATA
最后用pg_ctl start命令拉起数据库,等待wal数据回放完成即可。
测试效果
PolarDB-PG其中一个客户使用增量块备份功能的耗时和存储数据,如下表所示。
从备份角度看,增量块备份作为一次逻辑全备,备份时间仅为全量备份的时间1/6,存储空间仅为全量备份的1/13。而且目前测试只是100gb的小实例,随着实例数据量的增大,增量块备份在备份时间和存储将会和全量备份进一步拉开差距。
从恢复角度看,假设对一个大实例来说,原来每周备份一次全量备份,而增量块备份成本较低,可以每天备份一次,那么在最差情况下,可以减少6天的wal日志回放量(最差情况下,高并发6天产生的wal日志需要回放3天时间),从RTO看减少了85.7%的wal回放时间。




