TT Lab
开始
学习 学习路径 课程

FDE综合实战:仓库收到了三次相同订单

回滚计划里的恢复命令从来没有执行过

在 TT Lab 中继续学习

目标

在使用 WAL 模式 SQLite 的客户服务上,实际运行备份 → 证据 → 迁移 → 故障注入 → 恢复 → 证明与备份时刻相同,并把这套流程固化为 migrate.sh、restore.sh、rehearse.sh。

为什么重要

回滚计划书中的恢复命令,通常从来没有执行过。服务运行中用 cp 做的备份,integrity_check 是 ok,提交却丢了;强制终止后残留的 -wal 会被重新应用到恢复的文件上。如果不通过演练确认并用数字留下来,就要到需要回滚的那天才第一次知道。

材料:/opt/lab/p1a-rehearsal/ —— liveapp.py(模拟订单服务:start、status、stop、crash),migrations/002_add_currency.sql,migrations/003_customer_unique.sql。 工作目录是 /root/rehearsal(服务数据库是 /root/rehearsal/live.db)。证据文件放在 /root/rehearsal/evidence。预计 60 分钟,会话结束后 /root/rehearsal 会消失。

步骤

  1. 用 liveapp.py start 启动服务,在服务运行的状态下只把 live.db 主文件 cp 到 /root/rehearsal/copies/cp-only.db,然后在 /root/rehearsal/evidence/cp.tsv 中写下 live 和 cp-only 的行数和 integrity_check。
  2. 在服务运行的状态下,用 .backup 生成 /root/rehearsal/backups/pre-v2.db,用 VACUUM INTO 生成 /root/rehearsal/backups/pre-v2-vacuum.db,并按 file<TAB>rows<TAB>integrity<TAB>journal_mode 的格式写入 evidence/backup.tsv。
  3. 把两个备份的文件 sha256 和 .sha3sum --schema、行数、user_version、integrity 和时间,留在 evidence/baseline.json 中。
  4. 把 migrate.sh DB NNN_이름.sql(占位符依次为数据库和迁移文件名)做成 /root/rehearsal/migrate.sh,并把 002 应用到 live.db。只有 user_version 是 NNN-1 时才应用,如果已经是 NNN 就什么也不做并返回 0,其他情况一律拒绝。
  5. 把 003 应用到 live.db 使其失败,并在 evidence/failure.json 中留下退出码、user_version、前后的内容哈希和错误。migrate.sh 失败时必须不留下任何痕迹。
  6. 把 restore.sh BACKUP TARGET 做成 /root/rehearsal/restore.sh。备份不完整时,不碰目标并拒绝;即使有残留的 -wal,也要恢复成与备份相同的内容并加以证明。
  7. 用 liveapp.py crash 制造部署中的强制终止之后,把 live.db 恢复为 pre-v2.db,并留下 evidence/restore.json。
  8. 把 rehearse.sh DB WORKDIR UP.sql BROKEN.sql 做成 /root/rehearsal/rehearse.sh。从备份到恢复证明一次性跑完并留下 WORKDIR/report.json,只要有任何一项无法证明,就以非 0 值结束。

参考

对运行中的服务的数据库做 cp,会丢掉什么

启动服务,把 live.db cp 到 /root/rehearsal/copies/cp-only.db,然后在 /root/rehearsal/evidence/cp.tsv 中写下 live 和 cp-only 的行数与 integrity_check。

先用 ls -l live.db* 看看主文件旁边有什么。行数要在服务运行期间在 live.db 上统计,副本则用 immutable URI 只读打开。这一步的要点,是看 integrity_check 的结果与行数是否在说互相矛盾的事。

用 .backup 和 VACUUM INTO 做在线备份

在服务运行的状态下,生成 /root/rehearsal/backups/pre-v2.db(.backup)和 /root/rehearsal/backups/pre-v2-vacuum.db(VACUUM INTO),并写入 /root/rehearsal/evidence/backup.tsv。

VACUUM INTO 在目标文件已存在时会失败。journal_mode 可以通过文件头的字节来确认,不用打开文件就能知道。请比较两个备份的行数与第 1 步 cp 副本有什么不同。

留下恢复后要比较的基准值

把两个备份的 sha256 和 .sha3sum --schema,以及 pre-v2.db 的行数、user_version、integrity 和时间,留在 /root/rehearsal/evidence/baseline.json 中。

想一想文件哈希和内容哈希各自证明什么。两个备份的文件哈希应当不同,而内容哈希应当相同。时间用 Pod 的时间,格式为 %Y-%m-%dT%H:%M:%S。从此以后,不要再打开备份文件来写入。

遵守版本号的 migrate.sh

制作 /root/rehearsal/migrate.sh,并把 002_add_currency.sql 应用到 live.db。

版本号取自文件名前面的数字。迁移文件里没有 BEGIN、COMMIT、user_version,所以要由脚本来包装。评分器还会用其他编号、其他 user_version 的数据库来运行。

失败的迁移不能留下痕迹

把 003_customer_unique.sql 应用到 live.db 使其失败,并留下 /root/rehearsal/evidence/failure.json。migrate.sh 即使中间的语句失败,也不能改变任何东西。

请在失败前后测量 user_version 和 .sha3sum --schema。请在文档中确认 sqlite3 CLI 在出错之后会做什么,以及违反约束是否会回滚整个事务。评分器会用失败位置不同的迁移来测试 migrate.sh。

连残留 -wal 也会处理的 restore.sh

制作 /root/rehearsal/restore.sh BACKUP TARGET。对损坏的备份,在碰目标之前就拒绝,恢复之后确认内容哈希与备份相同。

目标旁边如果残留着 -wal,下次打开时会发生什么,请再看一遍 WAL 文档。备份检查要在碰目标之前做,证明要在恢复之后做。评分器会给你因强制终止而残留 -wal 的数据库,以及页面损坏的备份。

恢复部署中被强制终止的生产数据库

在 liveapp.py crash 之后,用 restore.sh 把 live.db 恢复为 backups/pre-v2.db,并留下 /root/rehearsal/evidence/restore.json。

crash 结束后,用 ls -l live.db* 看看留下了什么。故障的 token 在 app/events.log 的最后一条 crashed 行里。恢复之后,也请统计一下以 hotfix- 开头的订单是否还在。

一次性跑完的演练与报告

制作 /root/rehearsal/rehearse.sh DB WORKDIR UP.sql BROKEN.sql。留下 WORKDIR/backup.db 和 WORKDIR/report.json,只有所有证明都成立时才以 0 结束。

请以脚本所在位置为基准调用前面做好的 migrate.sh 和 restore.sh。故障注入没有失败、失败之后内容哈希发生了变化、恢复之后与备份不同——这些情况都必须返回非 0。评分器会用只有 -wal 里留有提交的数据库和随机的版本号来运行。