版本信息最后核对:2026-10-25
Lab 状态:实跑通过
备份、隔离恢复、一致性与清理均通过
安全边界:本 Lab 只使用本地一次性容器、独立网络和
/tmp/lab-restore目录,不连接任何生产数据库。恢复目标必须是与源库隔离的新实例;不要在生产执行pg_restore、DROP DATABASE或覆盖数据目录。真实演练还要遵守数据脱敏、访问审计和备份保留策略。
准备 Docker 与 Compose v2,预留约 1 GB 内存。源库和恢复库分别挂在两个 --internal 网络,彼此不能互通;备份文件先落到宿主机隔离目录,再以只读方式挂给恢复容器。这样即使恢复出的应用配置错误,也无法直接写入源库:
timeout 10 mkdir -p /tmp/lab-restore/backup
timeout 30 docker version --format '{{.Server.Version}}'
timeout 30 docker network create --internal lab-restore-src
timeout 30 docker network create --internal lab-restore-dst
timeout 180 docker run -d --name lab-pg-source --network lab-restore-src \
-e POSTGRES_USER=lab -e POSTGRES_PASSWORD=labpass -e POSTGRES_DB=app \
-v lab-restore-source:/var/lib/postgresql/data postgres:16
timeout 180 docker run -d --name lab-pg-restore --network lab-restore-dst \
-e POSTGRES_USER=lab -e POSTGRES_PASSWORD=labpass -e POSTGRES_DB=app_restore \
-v lab-restore-target:/var/lib/postgresql/data \
-v /tmp/lab-restore/backup:/backup:ro postgres:16
timeout 120 bash -c 'until docker exec lab-pg-source pg_isready -U lab -d app; do sleep 2; done'
timeout 120 bash -c 'until docker exec lab-pg-restore pg_isready -U lab -d app_restore; do sleep 2; done'
timeout 30 docker network inspect lab-restore-src -f 'src_internal={{.Internal}}'
timeout 30 docker network inspect lab-restore-dst -f 'dst_internal={{.Internal}}'
在两个库里创建同构结构。源库模拟“生产”,恢复库只作为空白目标;所有实验写入都只发生在源库和一次性目标库:
cat > /tmp/lab-restore/schema.sql <<'EOF'
CREATE TABLE orders (
id bigserial PRIMARY KEY,
customer text NOT NULL,
amount numeric(12,2) NOT NULL CHECK (amount >= 0),
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE order_items (
id bigserial PRIMARY KEY,
order_id bigint NOT NULL REFERENCES orders(id),
sku text NOT NULL,
qty integer NOT NULL CHECK (qty > 0)
);
EOF
timeout 60 docker cp /tmp/lab-restore/schema.sql lab-pg-source:/tmp/schema.sql
timeout 60 docker exec lab-pg-source psql -U lab -d app -v ON_ERROR_STOP=1 -f /tmp/schema.sql
timeout 30 docker exec lab-pg-source psql -U lab -d app -v ON_ERROR_STOP=1 -c \
"INSERT INTO orders (customer, amount) VALUES ('alice',100),('bob',200),('carol',300);
INSERT INTO order_items (order_id, sku, qty) VALUES
(1,'sku-a',1),(1,'sku-b',2),(2,'sku-c',1),(3,'sku-d',4);"
记录备份时间点基线。基线必须与备份时点对应,否则备份后的正常业务写入会被误判为恢复差异:
timeout 30 docker exec lab-pg-source psql -U lab -d app -Atc \
"SELECT count(*), coalesce(sum(amount),0), max(id) FROM orders;" | tee /tmp/lab-restore/backup/baseline-orders.txt
timeout 30 docker exec lab-pg-source psql -U lab -d app -Atc \
"SELECT count(*), coalesce(sum(qty),0), max(id) FROM order_items;" | tee /tmp/lab-restore/backup/baseline-items.txt
timeout 30 docker exec lab-pg-source psql -U lab -d app -Atc \
"SELECT last_value, is_called FROM orders_id_seq;" | tee /tmp/lab-restore/backup/baseline-seq.txt
date +%s | tee /tmp/lab-restore/backup/backup-start.epoch
用 custom 格式做逻辑备份,复制到宿主机隔离目录后计算校验和、读取目录清单。备份命令要加超时,避免在大库或慢磁盘上无限挂起:
timeout 120 docker exec lab-pg-source pg_dump -U lab -d app -Fc -f /tmp/app.dump
timeout 60 docker cp lab-pg-source:/tmp/app.dump /tmp/lab-restore/backup/app.dump
date +%s | tee /tmp/lab-restore/backup/backup-end.epoch
timeout 30 shasum -a 256 /tmp/lab-restore/backup/app.dump | tee /tmp/lab-restore/backup/app.dump.sha256
timeout 60 docker exec lab-pg-source pg_restore --list /tmp/app.dump \
> /tmp/lab-restore/backup/app.toc.txt
grep -c 'TABLE DATA' /tmp/lab-restore/backup/app.toc.txt
TABLE DATA 条数应与业务表数量一致。目录可读只证明 TOC 没损坏,不证明数据段完整;校验和与真实恢复缺一不可。接着模拟备份完成后的新写入,制造一个可解释的 RPO 缺口:
timeout 30 docker exec lab-pg-source psql -U lab -d app -v ON_ERROR_STOP=1 -c \
"INSERT INTO orders (customer, amount)
SELECT 'post-backup-'||g, g FROM generate_series(1,5) AS g;
INSERT INTO order_items (order_id, sku, qty)
SELECT id, 'sku-post', 1 FROM orders WHERE customer LIKE 'post-backup-%';"
date +%s | tee /tmp/lab-restore/backup/post-write.epoch
timeout 30 docker exec lab-pg-source psql -U lab -d app -Atc \
"SELECT count(*), coalesce(sum(amount),0), max(id) FROM orders;"
源库现在应有 8 条订单、9 条明细;备份里只有 3 条订单、4 条明细。这不是恢复失败,而是备份时点之后尚未进入备份的正常写入,正是本次 RPO 的度量对象。
复制一份备份并截断末尾,验证校验和与恢复流程能拦住坏文件。只处理副本,绝不改动唯一备份:
SIZE=$(wc -c < /tmp/lab-restore/backup/app.dump)
timeout 30 head -c $((SIZE - 2048)) /tmp/lab-restore/backup/app.dump > /tmp/lab-restore/backup/app-corrupt.dump
timeout 30 shasum -a 256 /tmp/lab-restore/backup/app-corrupt.dump
timeout 60 docker exec lab-pg-restore psql -U lab -d postgres -c "CREATE DATABASE corrupt_check;"
timeout 120 docker exec lab-pg-restore pg_restore -U lab -d corrupt_check \
--no-owner --exit-on-error /backup/app-corrupt.dump; echo "corrupt_restore_exit=$?"
timeout 30 docker exec lab-pg-restore psql -U lab -d postgres -c "DROP DATABASE corrupt_check;"
校验和不同就应直接判定备份不可用;即使 pg_restore 偶然成功,也不能把校验失败的文件用于正式恢复。
记录开始时间,使用 --exit-on-error 让结构、约束或数据错误立即暴露。恢复目标是独立容器中的 app_restore,不覆盖源库:
T1=$(date +%s); echo "$T1" | tee /tmp/lab-restore/backup/restore-start.epoch
timeout 180 docker exec lab-pg-restore pg_restore -U lab -d app_restore \
--no-owner --exit-on-error /backup/app.dump
T2=$(date +%s); echo "$T2" | tee /tmp/lab-restore/backup/restore-data.epoch
echo "rto_data_seconds=$((T2-T1))"
先比结构与内容,再比关系与序列。所有查询都在恢复库执行,避免误读源库:
timeout 30 docker exec lab-pg-restore psql -U lab -d app_restore -Atc \
"SELECT count(*), coalesce(sum(amount),0), max(id) FROM orders;"
timeout 30 docker exec lab-pg-restore psql -U lab -d app_restore -Atc \
"SELECT count(*), coalesce(sum(qty),0), max(id) FROM order_items;"
timeout 30 docker exec lab-pg-restore psql -U lab -d app_restore -Atc \
"SELECT last_value, is_called FROM orders_id_seq;"
timeout 30 docker exec lab-pg-restore psql -U lab -d app_restore -Atc \
"SELECT count(*) FROM order_items i LEFT JOIN orders o ON o.id=i.order_id WHERE o.id IS NULL;"
timeout 30 docker exec lab-pg-restore psql -U lab -d app_restore -Atc \
"SELECT (SELECT count(*) FROM pg_indexes WHERE schemaname='public'),
(SELECT count(*) FROM pg_constraint WHERE connamespace='public'::regnamespace);"
恢复库的订单应为 3 条、金额 600、最大 ID 3;明细应为 4 条、数量 8、最大 ID 4;序列 last_value=3、is_called=t;孤儿明细为 0。再做一次事务内写入和回滚,验证约束、外键和序列都可用:
timeout 30 docker exec lab-pg-restore psql -U lab -d app_restore -v ON_ERROR_STOP=1 -c \
"BEGIN;
INSERT INTO orders (customer, amount) VALUES ('restore-canary', 1) RETURNING id;
INSERT INTO order_items (order_id, sku, qty)
VALUES (currval('orders_id_seq'), 'canary', 1);
SELECT count(*), sum(amount) FROM orders;
ROLLBACK;"
timeout 30 docker exec lab-pg-restore psql -U lab -d app_restore -Atc \
"SELECT last_value FROM orders_id_seq;"
T3=$(date +%s)
RPO_ROWS=5
RPO_SECONDS=$(( $(cat /tmp/lab-restore/backup/post-write.epoch) - $(cat /tmp/lab-restore/backup/backup-end.epoch) ))
echo "rpo_rows=$RPO_ROWS rpo_seconds=$RPO_SECONDS rto_data=$((T2-T1))s rto_verified=$((T3-T1))s"
序列在事务回滚后仍会前进,这是 PostgreSQL 序列的非事务特性,不是恢复错误。关键是恢复后 nextval 不与已有 ID 冲突,且唯一约束、外键和检查约束都能正常工作。
备份结束时,源库行数为 3/4;追加写入后变为 8/9。恢复库保持在 3/4,与备份时点基线一致,差额正好是备份后的 5 条订单和 5 条明细。pg_restore --exit-on-error 成功时退出码为 0,恢复库中索引、约束和外键齐全;截断副本的 SHA-256 不同,恢复时报错或数据校验失败。
成功的判据是:备份校验和可复核、TOC 完整、恢复无未解释错误;恢复库的行数、聚合、最大 ID、序列和引用关系与基线一致;事务写入、约束和外键可用;实际 RPO 行数与秒数已记录且业务可接受;RTO 从开始恢复计到验收通过。失败判据是:备份不可读或校验不符;恢复报错后仍继续;行数或聚合无法用备份时点解释;序列回退导致主键冲突;外键孤儿存在;恢复目标与源库网络互通;RPO/RTO 只记录命令耗时而忽略等待和人工步骤。
pg_dump 会恢复序列当前值,但 is_called 和后续 nextval 决定新 ID 是否冲突;恢复后先读序列,再插入 canary,最后回滚验证。docker network inspect 确认两个网络都是 internal,确认恢复容器只读挂载备份、没有源库连接串和生产凭据;不要只看“我是按隔离步骤做的”。演练中的“回滚”不是把恢复库指给业务,而是中止演练并销毁隔离目标。源库全程只读备份、不执行恢复;一旦发现恢复库被误当生产使用,立即停止应用写入、撤销临时凭据、检查审计日志和写入范围,按事故流程处理。
恢复失败时先保留现场:保存 pg_restore 日志、TOC、校验和与目标库状态,再删除 app_restore 并用同一份备份重试;不要在原目标上反复覆盖,也不要手工修补源库。若备份损坏,改用更早备份或异地副本,并明确记录 RPO 违约。若只是应用验收失败,检查应用版本、连接串、权限和依赖是否匹配备份时点,数据继续保持隔离。
timeout 60 docker rm -f lab-pg-restore
timeout 30 docker volume rm lab-restore-target
timeout 60 docker rm -f lab-pg-source
timeout 30 docker volume rm lab-restore-source
timeout 30 docker network rm lab-restore-dst
timeout 30 docker network rm lab-restore-src
rm -rf /tmp/lab-restore
清理前确认备份样本、校验和、基线、RPO/RTO 记录和失败日志已经归档到演练证据目录。只删除带 lab- 前缀的资源,不要对生产容器、网络或卷执行删除。
pg_restore --list 读取,为什么仍不能证明可以恢复?pg_restore 的执行耗时。
小程序版
微信扫码,随时查命令