系统学习 演练学习路径:35 个 lab 的难度排序、4 周基础计划与复盘方法

版本信息最后核对:2026-10-25

Lab 状态:实跑通过2026-10-25 · Docker Compose / 本地目录

备份、隔离恢复、一致性与清理均通过

在隔离目录与独立容器中验证 PostgreSQL 备份可恢复性,核对行数、聚合与序列,并记录真实 RPO/RTO

演练目标

安全边界:本 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

演练步骤

1. 制作并校验备份

用 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 的度量对象。

2. 验证损坏备份会被发现

复制一份备份并截断末尾,验证校验和与恢复流程能拦住坏文件。只处理副本,绝不改动唯一备份:

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 偶然成功,也不能把校验失败的文件用于正式恢复。

3. 恢复到新实例并计时

记录开始时间,使用 --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))"

4. 数据一致性与应用验收

先比结构与内容,再比关系与序列。所有查询都在恢复库执行,避免误读源库:

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 只记录命令耗时而忽略等待和人工步骤。

判断与定位

  1. 先区分“备份任务成功”和“备份可恢复”。任务成功只说明作业完成,可能遗漏表、序列、大对象、权限或数据段损坏;必须实际恢复到隔离实例并跑查询。
  2. 差异要能归因。恢复库少于源库,通常来自备份后的正常写入;若差异超出该窗口,检查备份时点、TOC、日志链和恢复日志,不要直接修改恢复库凑数。
  3. 行数相同不代表内容相同。要同时看金额汇总、数量汇总、最大 ID、时间范围、唯一约束和抽样记录;大表可用聚合与抽样替代全量扫描。
  4. 序列要单独核对。pg_dump 会恢复序列当前值,但 is_called 和后续 nextval 决定新 ID 是否冲突;恢复后先读序列,再插入 canary,最后回滚验证。
  5. 隔离边界要可证明。用 docker network inspect 确认两个网络都是 internal,确认恢复容器只读挂载备份、没有源库连接串和生产凭据;不要只看“我是按隔离步骤做的”。
  6. 计时点要固定。RPO 比较备份或日志恢复点与故障时点的数据差距;RTO 从故障发生或决策开始,到数据可用、应用验收通过为止。实验环境的秒级 RTO 不能直接当作生产承诺。

恢复与回滚

演练中的“回滚”不是把恢复库指给业务,而是中止演练并销毁隔离目标。源库全程只读备份、不执行恢复;一旦发现恢复库被误当生产使用,立即停止应用写入、撤销临时凭据、检查审计日志和写入范围,按事故流程处理。

恢复失败时先保留现场:保存 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- 前缀的资源,不要对生产容器、网络或卷执行删除。

验证

复盘

相关命令

参考链接

读完自测

  1. 备份任务成功、备份文件也能被 pg_restore --list 读取,为什么仍不能证明可以恢复?
  2. RPO 和 RTO 应该怎样测量?为什么不能只计算执行恢复命令的时间?
  3. 恢复后行数和金额都对,为什么还要单独检查序列和外键?
答案
  1. 任务成功只代表备份作业完成,TOC 可读也只代表目录结构没坏;数据段可能截断、表或序列可能遗漏、密钥可能丢失、恢复环境可能缺少权限。只有在隔离实例中实际恢复,并完成结构、数据和应用验收,才能证明备份可用。
  2. RPO 要比较备份或日志恢复点与故障时点的数据差距,可用丢失行数和时间窗口表示;RTO 从故障发生或恢复决策开始,到数据可用、应用验收通过和服务可交付为止,必须包含等待、重试、人工审批和验证时间,不能只测 pg_restore 的执行耗时。
  3. 行数和金额一致仍可能隐藏序列回退和外键孤儿。序列决定后续插入是否主键冲突,外键和约束决定关系是否可信;只比行数会把“看起来有数据、实际不可写或关系断裂”的恢复误判为成功。
微信小程序码:扫码打开命令速查小程序

小程序版

微信扫码,随时查命令