回滚计划里的恢复命令从来没有执行过
目标
在使用 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 会消失。
步骤
- 用
liveapp.py start启动服务,在服务运行的状态下只把 live.db 主文件 cp 到 /root/rehearsal/copies/cp-only.db,然后在 /root/rehearsal/evidence/cp.tsv 中写下live和cp-only的行数和 integrity_check。 - 在服务运行的状态下,用
.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。 - 把两个备份的文件 sha256 和
.sha3sum --schema、行数、user_version、integrity 和时间,留在 evidence/baseline.json 中。 - 把
migrate.sh DB NNN_이름.sql(占位符依次为数据库和迁移文件名)做成 /root/rehearsal/migrate.sh,并把 002 应用到 live.db。只有 user_version 是 NNN-1 时才应用,如果已经是 NNN 就什么也不做并返回 0,其他情况一律拒绝。 - 把 003 应用到 live.db 使其失败,并在 evidence/failure.json 中留下退出码、user_version、前后的内容哈希和错误。migrate.sh 失败时必须不留下任何痕迹。
- 把
restore.sh BACKUP TARGET做成 /root/rehearsal/restore.sh。备份不完整时,不碰目标并拒绝;即使有残留的 -wal,也要恢复成与备份相同的内容并加以证明。 - 用
liveapp.py crash制造部署中的强制终止之后,把 live.db 恢复为 pre-v2.db,并留下 evidence/restore.json。 - 把
rehearse.sh DB WORKDIR UP.sql BROKEN.sql做成 /root/rehearsal/rehearse.sh。从备份到恢复证明一次性跑完并留下 WORKDIR/report.json,只要有任何一项无法证明,就以非 0 值结束。
参考
- 服务运行期间,你的 sqlite3 连接不是最后一个连接,所以 -wal 会保留。如果在停掉服务的状态下打开再关闭 live.db,检查点就会运行,第 1 步的条件就没了(即使再次
liveapp.py start,也已经被移过去了)。第 1、2 步要在服务运行的状态下做。 - 只读取备份文件:
sqlite3 'file:backups/pre-v2.db?immutable=1' 'PRAGMA integrity_check' - 文件头第 18 个字节(从 0 数起)为 1 表示回滚日志,为 2 表示 WAL:
od -An -tu1 -j18 -N1 파일(占位符为文件) - 评分器不依赖你启动的服务。第 4、6、8 步会新建数据库并直接运行你的脚本,其余步骤则用备份文件和 liveapp 的记录(app/events.log)重新计算证据文件并核对。删除或修改备份文件,前面的步骤会再次不合格。
- 常见错误:用
sqlite3 db < file.sql做迁移,没有-bail的 BEGIN/COMMIT,留着残留的 -wal 用 cp 恢复。
对运行中的服务的数据库做 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 里留有提交的数据库和随机的版本号来运行。