全量备份提供一个不依赖同种方法更早备份的基线。增量备份保存相对于指定参考点的变化,恢复时仍然依赖补全省略内容所需的材料。这一区别描述的是恢复依赖,不是对执行速度或传输字节数的普遍保证。如果变化过的数据范围很大,增量备份本身也可能相当大,不能仅凭“增量”两个字就承诺备份很快。

坏掉的样子

全量备份损坏,可能使所有依赖它的增量都失去恢复能力。同样,删除必要的中间增量,即使最新目录与清单还完整,也可能破坏后续恢复。保留策略必须沿着依赖关系作决定,不能只按时间戳独立删除看起来较旧的目录。一个较新的文件,并不自动替代所有较旧的恢复材料。

参考对象同样重要。相对于上一次增量建立备份,会形成一条链;相对于某个全量基线建立备份,则具有另一种依赖形状,通常称为差异备份。合成全量通过已有备份材料重建一个新基线。这些名词在不同工具中不保证实现完全相同,因此,不能只依据名称设计轮换,必须核对实际产品的清单格式与恢复规则。

物理数据库备份还需要让捕获文件达到一致状态的必要日志。全量独立于更早备份,并不意味着它独立于全部恢复日志、软件、配置或外部依赖。本实验通过流式收集带上所需 WAL,并保持整个备份链中的数据库版本与页面校验配置一致。这样验证的是明确的一套恢复条件,而不是省略前提的“有全量就一定能启动”。

快速验证

在 Linux 环境中,以非超级操作系统账户运行下面脚本,准备 PostgreSQL 18 服务端和客户端程序,包括合成备份工具。本次实际执行版本是 PostgreSQL 18.6。脚本创建私有临时主库与恢复集群,不打开网络监听。取得备份之前先开启日志摘要,让数据库能够为增量请求识别发生变化的数据块。

#!/bin/sh
set -eu
umask 077
lab=$(mktemp -d /tmp/xbcatm-incremental.XXXXXX)
mkdir "$lab/socket"
primary=$lab/primary
restored=$lab/restored
cleanup() {
    for cluster in "$restored" "$primary"; do
        if test -f "$cluster/postmaster.pid"; then
            pg_ctl -D "$cluster" -m fast -w stop >/dev/null
        fi
    done
}
trap cleanup EXIT
initdb -D "$primary" --no-locale --encoding=UTF8 --auth-local=trust --auth-host=reject >/dev/null
cat >> "$primary/postgresql.conf" <<CONF
listen_addresses = ''
unix_socket_directories = '$lab/socket'
port = 56432
summarize_wal = on
CONF
pg_ctl -D "$primary" -l "$lab/primary.log" -w -t 15 start >/dev/null
export PGHOST=$lab/socket PGPORT=56432 PGDATABASE=postgres PGUSER=$(id -un)
export PGCONNECT_TIMEOUT=2
psql -X -v ON_ERROR_STOP=1 -qAt <<'SQL'
CREATE TABLE marker (id integer PRIMARY KEY,value text);
INSERT INTO marker VALUES (1,'full');
SQL
pg_basebackup -D "$lab/base" -X stream -c fast -w
psql -X -v ON_ERROR_STOP=1 -qAt -c "INSERT INTO marker VALUES (2,'first_increment')"
pg_basebackup -D "$lab/inc1" --incremental="$lab/base/backup_manifest" -X stream -c fast -w
psql -X -v ON_ERROR_STOP=1 -qAt <<'SQL'
UPDATE marker SET value='final' WHERE id=1;
DELETE FROM marker WHERE id=2;
INSERT INTO marker VALUES (3,'second_increment');
SQL
pg_basebackup -D "$lab/inc2" --incremental="$lab/inc1/backup_manifest" -X stream -c fast -w
for backup in "$lab/base" "$lab/inc1" "$lab/inc2"; do
    pg_verifybackup -q "$backup"
done
echo input_backups_verified
if pg_combinebackup -o "$lab/missing-middle" "$lab/base" "$lab/inc2" >"$lab/missing-middle.log" 2>&1; then
    echo 'incomplete chain unexpectedly combined' >&2
    exit 1
fi
grep -E 'starts at LSN .*but expected' "$lab/missing-middle.log"
echo missing_middle_rejected
pg_combinebackup -o "$lab/combined" "$lab/base" "$lab/inc1" "$lab/inc2"
pg_verifybackup -q "$lab/combined"
echo combined_backup_verified
mkdir "$restored"
cp -a "$lab/combined/." "$restored/"
cat >> "$restored/postgresql.conf" <<CONF
port = 56433
summarize_wal = off
CONF
pg_ctl -D "$restored" -l "$lab/restored.log" -w -t 15 start >/dev/null
PGPORT=56433 psql -X -v ON_ERROR_STOP=1 -qAt <<'SQL'
SELECT pg_is_in_recovery();
SELECT id,value FROM marker ORDER BY id;
DO $$ BEGIN
  IF (SELECT string_agg(id||':'||value,',' ORDER BY id) FROM marker)
     IS DISTINCT FROM '1:final,3:second_increment' THEN
    RAISE EXCEPTION 'incremental restore result mismatch';
  END IF;
END $$;
SQL
printf 'lab_directory %s\n' "$lab"

全量备份包含第一行,值为 full。第一次增量参考这份全量的清单,并增加第二行。第二次增量参考第一次增量的清单,把第一行改为 final,删除第二行,再增加第三行。这不仅测试新行出现,也测试已有行的更新和删除,避免把简单追加成功误当成整个增量恢复语义正确。

重建之前,脚本逐份验证所有输入备份。第一次合成故意只提供全量和第二次增量,它必须由于起始日志位置不匹配而失败;脚本检查这个具体原因后,才打印缺少中间备份被拒绝的标记。负向对照没有删除任何原始备份。这样的失败揭示了单纯统计目录数量无法识别的依赖关系。

完整合成命令按顺序提供全量、第一次增量与第二次增量。输出的合成全量再次接受清单验证,然后复制到独立恢复目录并实际启动。预期只有第一行 final 和第三行 second_increment,SQL 断言检查这两行组合后的精确结果。实例已经结束恢复,先前删除的第二行不能重新出现,否则说明恢复结果不符合第二次增量所代表的状态。

合成工具验证的是输入之间的关系,并不替代每一份输入的完整性检查。因此,输入验证、输出验证与真实启动分别建立不同事实。脚本使用普通复制,让恢复实例运行时保留原始备份不变。退出时停止全部实验服务,打印目录保留日志和材料供复核,不把运行中的恢复目录当成唯一剩余备份。

核算恢复成本,不只看备份速度

增量可以减少重复复制未变数据块,但恢复可能先要取得、验证和组合多份材料,之后才能启动数据库。应测量包括恢复与应用核验在内的完整路径。每天备份变快,不足以证明事故发生时能够达到恢复时限;存储传输、重建所需额外空间和后续重放,都可能成为真正的限制。

保留策略旁边应维护依赖清单。PostgreSQL 允许合成全量成为后续合成的输入,这会改变操作人员需要取回的链路。按照既定策略退役旧材料之前,应先验证新基线。改变校验状态或引擎要求,也可能需要重新取得全量。每次轮换后,既要演练近期终点,也要演练承诺保留的较旧终点,防止节省存储时悄悄缩短了真正可恢复的区间。

延伸阅读

来源