39 KiB
隔离备份恢复演练证据
证据边界
- 仅记录日期、角色、短构建标识、状态、finding 类别和清理结果。
- 不记录远端地址、SSH 身份、完整目录、凭据、账号、原始 manifest、业务字段或附件内容。
- 腾讯云是 OA 生产;ARM 仅是异机备份、外部健康探测和隔离恢复演练节点。
Attempt 1:前置门禁失败关闭
日期:2026-08-23
| 检查项 | 结果 |
|---|---|
生产公网 /ready(演练前) |
PASS |
| 计划基线构建短标识 | 7b86ffb |
| 最新 ARM 正式日批次选择 | 已选择最新候选,未降级到旧批次 |
| manifest 来源构建门禁 | FAIL:build_mismatch |
| 失败阶段 | select_latest_batch |
| finding 类别 | latest_batch_invalid |
| 数据库/附件复制 | 未开始 |
| 一次性 PostgreSQL / 恢复写入 | 未创建、未执行 |
| runtime 清理 | PASS |
| 临时 root 入口清理 | PASS |
| 结论 | NO_GO |
该结果只证明最新候选未通过来源构建门禁,不能单独证明备份内容损坏。首次失败证据必须保留,禁止自动改选更旧批次或放宽门禁。
失败后只读复核
日期:2026-08-23
| 检查项 | 结果 |
|---|---|
生产公网 /ready |
PASS,构建短标识 7b86ffb3 |
| 生产自动备份 timer | active |
| 生产自动备份 unit 冻结构建 | 7b86ffb3 |
| ARM dead-man timer | active |
| ARM 公网健康 timer | active |
| ARM 一次性 root 入口残留 | 0 |
| ARM 本轮 runtime 残留 | 0 |
| ARM 普通用户暂存候选 | 窄名称初扫发现 1;随后按本轮 5 个文件的 SHA-256 精确匹配并全部删除 |
只读复核排除了“当前生产构建事实源过期”和“生产自动备份 unit 仍冻结旧构建”两种解释。ARM 上本轮 5 个普通用户暂存文件均经 SHA-256 与本地候选逐一匹配后精确删除,未执行宽泛目录删除。剩余调查重点是:最新 ARM 正式日批次是否来自当前生产自动备份链,以及 manifest 构建身份在生产生成、异机传输或 ARM 日归档选择中的哪一环发生漂移。
Attempt 1 当时判定与重试门禁
- Attempt 1 仍为
NO_GO,当时 AC1、AC3、AC4、AC5 未满足;该次没有形成数据恢复 RTO/RPO 实测值。 - 不重试恢复,不触碰 ARM 正式归档,不修改腾讯云生产数据或配置。
- 下一步只允许以窄权限读取以下脱敏事实:最新 ARM manifest 的构建短标识、批次时间与最新生产成功状态是否指向同一 run id、manifest 摘要是否一致。
- 若确认只是 ARM 最新批次来源/归档选择异常,另行规划修复备份链;若确认归档身份正确且仅演练器口径有误,先更新任务设计并重新批准。
- 任何第二次恢复尝试前,必须更新规划、重新生成一次性入口并再次取得用户明确批准。
只读身份探针结果
日期:2026-08-23
| 检查项 | 结果 |
|---|---|
| 最新 ARM manifest 构建短标识 | ec682565 |
| 当前生产/自动备份 unit 短标识 | 7b86ffb3 |
| 构建匹配 | NO |
| 最新批次年龄 | 约 18 小时,within_24h=yes |
| run id 与目录配对 | PASS |
| DB/附件 manifest 大小与 SHA-256 | PASS |
| source image digest | 存在 |
| manifest 摘要 | 已取得 12 位脱敏前缀,不保存完整摘要 |
| 探针临时 root 入口清理 | PASS,残留 0 |
| 普通用户暂存清理 | 3 个候选均按 SHA-256 精确删除,目录残留 0 |
| ARM 两个监控 timer | active |
ec682565 是仓库可识别的历史提交,也是当前生产提交的祖先。随后只读对时确认:当前生产自动备份合同约 1.3 小时前才刷新,而最新 ARM 批次约 18 小时前生成;下一次自然调度约 14 小时后执行。因此最新 ARM 批次是在当前发布之前由当时活动构建生成,不支持“错误来源批次”或“传输损坏”的判断。
当前 finding 应归类为“发布后尚未产生新构建恢复点”。首次恢复入口按当时已批准的精确构建门禁正确失败关闭;该 NO_GO 是 Attempt 1 的真实历史结论,不覆盖也不回写。后续设计复盘确认,严格相等不是原始备份可恢复性的必要条件,新的准入合同见 2026-08-24 规划修订。
当时用户决策:不手工触发生产自动备份,等待既有 timer 自然生成并异机归档下一批恢复点。该决策不是完成项,也不永久排除手工备份;若后续自然批次异常,可重新作为需明确批准的备选项。
2026-08-24 设计复盘与有效产出
- 自 2026-08-23 对时到当前日期,下一次自然调度窗口已经过去;当前唯一可立即执行的动作是按新设计重新只读探测 ARM 最新批次,而不是继续等待。
- Attempt 1 已经观测到:生产合同刷新后,到下一个自然备份抵达前,没有与当前构建严格相等的异机恢复点。该“同源恢复点可用滞后”是独立于数据 RPO/RTO 的有效恢复画像,即使数据库恢复未开始也必须保留。
ec682565与7b86ffb3的 migration 目录均为 65 个,目录差异为 0;这证明本次历史批次与当前 schema 没有 migration 增量,但不能证明“65”适合作为长期硬编码断言。- 修订后的恢复点门禁为:manifest 来源提交是当前生产构建的已知祖先或相同,批次年龄不超过 24 小时,且 manifest/两份产物完整。严格相等只用于度量同源恢复点何时形成。
- migration 验证改为来源提交目录名称集合与恢复库成功集合精确一致;来源/当前集合不同则把“升级到当前 schema”单独标为未验证,不在本轮临时执行 migration。
2026-08-24 最新自然批次只读探针
本节仅追加,不改写 Attempt 1 及 2026-08-23 既有证据。
| 检查项 | 结果 |
|---|---|
演练开始时公网 /ready |
PASS |
| 公网当前构建唯一解析 | PASS,短标识 7b86ffb3 |
| 生产自动备份 unit 与公网构建 | MATCH,timer active |
| ARM 最新批次构建短标识 | 7b86ffb3 |
| 来源与当前构建关系 | SAME;因此同时满足“祖先或相同” |
| 批次年龄 | 68491 秒,约 19 小时 2 分,within_24h=yes |
| run id 与目录配对 | PASS |
| DB/附件 manifest 大小与 SHA-256 | PASS |
| source image digest | 存在 |
| manifest 摘要 | 已取得 12 位脱敏前缀,不保存完整摘要 |
| 来源/当前 migration 目录 | 均为 65 个,差异文件 0 |
| 探针临时 root 入口 | 自清理 PASS,复核残留 0 |
| 普通用户暂存 | 3 个候选按 SHA-256 精确删除,目录残留 0 |
| ARM 两个监控 timer | active |
同源恢复点画像:生产备份合同刷新距探针约 94465 秒;最新同源 manifest 距探针 68491 秒,因此该同源批次的 manifest 创建时间约在合同刷新后 25974 秒(约 7 小时 13 分)。当前证据没有直接采集 ARM 原子归档完成时刻,不能把该差值表述为精确“到达 ARM”耗时;只能证明同源批次创建滞后约 7 小时 13 分,并且在本次探针时已经可用。
执行方结论:最新自然批次满足修订后的来源关系、新鲜度、run id 配对和产物完整性门禁,可进入恢复重试审批。该结论不是恢复授权;本次只读探针没有复制或解包备份、创建 Docker 对象、恢复数据库/附件或修改生产与 ARM 正式归档。
2026-08-24 同源滞后锚点更正
本节追加说明前节 25974 秒的锚点,不改写既有记录。
- 起点实际是生产
automatic-backupservice 构建 drop-in 的最大文件 mtime;终点是最新 manifestcreatedAt。 - 该差值可称为内部诊断指标“合同 drop-in 文件刷新到同源 manifest 创建”,不能称为应用发布完成到恢复点到达 ARM 的精确耗时。
- 当前证据没有采集 ARM 原子归档完成时刻,因此正式
same_build_recovery_point_lag只能保留为“存在发布后空窗”的定性 finding,7h13m不对外引用为精确恢复画像。 - 2026-08-23 “下一次自然调度约 14 小时后”的估算存在时区处理错误:探针把 systemd 输出的本地时区时间直接附为 UTC,导致约多算 8 小时。修正后自然调度约在当时探针后 6 小时,与 2026-08-24 最新 manifest 创建时间一致;没有证据表明中间发生非计划手工备份。
Attempt 2:临时副本目录身份误用
日期:2026-08-24
本节仅追加,不改写 Attempt 1、只读探针或锚点更正记录。
| 检查项 | 结果 |
|---|---|
| 最新正式批次前置门禁 | PASS |
| 正式归档读取/修改 | 只读使用、零修改 |
| 临时副本创建 | 已复制到本轮私有临时区 |
| 失败阶段 | create_isolated_runtime |
| finding 类别 | copied_batch_invalid / batch_identity |
| PostgreSQL 容器 | 未创建 |
| DB/附件恢复 | 未开始 |
| migration/关键表/附件关联验证 | 未执行 |
| runtime 清理 | PASS,复核残留 0 |
| 临时 root 入口清理 | PASS,复核残留 0 |
| 普通用户暂存清理 | 6 个候选按 SHA-256 精确删除,目录残留 0 |
| ARM 两个监控 timer | active |
| 结论 | NO_GO |
根因:runner 的 validate_batch 同时承担正式源目录身份和批次内容校验。正式源目录 basename 必须与 manifest runId 配对是正确安全门禁;复制后的私有临时目录固定名为 batch,内容仍保持同一 manifest/run id,却被同一个 basename 断言错误拒绝。该 finding 不表示备份损坏,也没有进入数据库或附件恢复。
修订边界:正式源目录继续执行 basename/run id 强校验;临时副本只跳过目录 basename 断言,manifest、run id 格式、文件集、构建、时间、大小、SHA-256 和与源元数据相等比较全部保留。任何重试前必须补“正式源通过、同内容改名副本通过、任一内容篡改失败”的合成回归并再次取得明确批准。
2026-08-25 Attempt 2 finding 分类补充
本节只追加分类,不改写 Attempt 2 原始结果。
- Attempt 2 finding 类别明确为
runner_defect,具体子类为“源目录身份断言误用于临时副本”。 - Attempt 1 finding 是“发布后尚未形成同源恢复点”;Attempt 2 finding 是演练器缺陷。两者均未证明备份不可恢复,不能合并表述为“备份连续失败两次”。
- Attempt 2 的正式批次前置门禁
PASS、临时复制完成、恢复前失败关闭、正式归档零修改和所有临时对象零残留,证明安全门禁在错误发生后按设计收口。 - 临时副本修订不是简单放宽:副本 manifest
runId必须重绑到源侧已验证的 run id,并继续通过 manifest、文件集、构建、时间、字节数、SHA-256 及源/副本元数据相等校验。 - 新增强制负例:源目录 basename 与 manifest run id 不配对必须
NO_GO;副本 manifest run id 即使内部自洽,但与源侧已验证 run id 不同也必须NO_GO。
2026-08-25 runner 修复验证与最新批次探针
本节仅追加,不改写 Attempt 1、Attempt 2 或既有探针记录。
Runner 修复验证
| 回归 | 结果 |
|---|---|
| 正式源 basename 与 manifest run id 配对 | PASS |
| 同内容改名副本重绑源侧 run id | PASS |
| 副本内容篡改 | 预期失败,PASS |
| 源 basename 与 manifest run id 错配 | 预期失败,PASS |
| 副本 manifest run id 内部自洽但不同于源 run id | 预期失败,PASS |
| 动态 migration 集合正/反例 | PASS |
| 既有隔离备份/恢复合同回归 | PASS |
实现结果:正式源目录继续校验 basename/run id;临时副本 manifest run id 必须等于源侧已验证 run id,并继续验证 manifest、文件集、构建、时间、大小、SHA-256 及源/副本脱敏元数据完全一致。
08-25 最新自然批次
| 检查项 | 结果 |
|---|---|
演练开始时公网 /ready |
PASS |
| 公网构建与自动备份 unit | MATCH,短标识 7b86ffb3,timer active |
| ARM 最新批次构建短标识 | 7b86ffb3 |
| 来源与当前构建关系 | SAME |
| 批次年龄 | 22249 秒,约 6 小时 11 分,within_24h=yes |
| run id 与目录配对 | PASS |
| DB/附件 manifest 大小与 SHA-256 | PASS |
| source image digest | 存在 |
| manifest 摘要 | 已取得 12 位脱敏前缀,不保存完整摘要 |
| 来源/当前 migration 目录 | 均为 65 个,差异文件 0 |
| 探针 root 入口及普通用户暂存 | 残留均为 0 |
| ARM 两个监控 timer | active |
该批次不是 Attempt 2 的旧候选,新鲜度和全部身份/完整性门禁已重新计算。当前满足 Attempt 3 恢复审批前提,但本次批准不包含实际恢复,未复制、解包或恢复该批次。
Attempt 3:数据库恢复阶段失败关闭
日期:2026-08-25
本节仅追加,不改写 Attempt 1、Attempt 2 或既有探针记录。
| 检查项 | 结果 |
|---|---|
| 候选文件 SHA-256、Bash 与 sudoers 语法 | PASS |
| 最新正式批次前置门禁 | PASS;使用 08-25 新候选 |
| 正式归档读取/修改 | 只读取用;runner 没有归档写入路径 |
| 临时副本创建及源侧身份重绑 | PASS;否则不会进入数据库恢复阶段 |
| 失败阶段 | restore_database |
| runner 聚合类别 | database_restore_failed |
| migration/12 类表/附件恢复与关联验证 | 未执行 |
| runtime 清理 | PASS,复核目录残留 0 |
| 一次性 PostgreSQL 容器清理 | PASS,复核残留 0 |
| 临时 root 入口与库清理 | PASS,复核残留 0 |
| ARM 两个监控 timer | active |
腾讯云生产公网 /ready(演练后) |
PASS,构建短标识仍为 7b86ffb3 |
| 结论 | NO_GO |
Attempt 3 在数据库恢复命令返回非零后正确停止,未进入 migration、关键表、附件恢复或附件关联检查。当前输出不能区分 dump 列表校验失败、恢复命令失败、工具/参数合同不匹配或其他 runner 调用问题,因此该次 NO_GO 不能被改写为“备份不可恢复”,也不能据此给出数据恢复 RTO 或完整恢复结论。
本轮还暴露了独立的演练器可观测性缺陷:restore-db.sh 的详细输出仅写入权限 0600 的本轮私有日志,而失败清理随后删除该日志,最终只保留 database_restore_failed 聚合类别。安全清理是正确的,但失败关闭前必须先生成不含路径、凭据、业务内容或原始 SQL 的阶段化脱敏诊断;在该合同补齐并通过回归、重新审批前,不执行 Attempt 4,也不改选旧批次或放宽门禁。
措辞对齐说明:既有 Attempt 2 段落中的“临时副本只跳过目录 basename 断言”仅描述不把自建目录名当归档证据,不构成放宽。生效合同始终是副本 manifest run id 必须等于源侧已验证 run id,并且副本全部脱敏元数据与源侧首次校验结果完全一致。
2026-08-25 Attempt 3 后本地诊断修复
本节只追加本地 runner 与合成回归结果;未连接 ARM、未探测新批次、未执行 Attempt 4,也未修改腾讯云生产或 ARM 正式归档。
- runner 将数据库恢复拆为
dump_list、target_confirmation、restore_apply三个阶段,只输出PASS/FAIL/NOT_RUN、执行路径枚举、数字退出码、数字或UNKNOWN错误计数和稳定错误类别。 - allowlist 诊断在 trap/finally 等价清理之前输出;原始 stderr 仍只存在于权限
0600的本轮私有日志并随 runtime 删除。注入退出码23的回归证明:阶段诊断先输出,runtime/access 清理随后均为PASS,调用方在清理后仍能读取数字退出码。 - PostgreSQL 16 custom dump 直接驱动仓库
scripts/restore-db.sh的 Docker stdin 路径成功;host-file 路径通过 PostgreSQL 16 工具执行同一真实脚本成功。没有在测试中重写等价恢复逻辑。 - dump-list 失败、目标确认失败和 list/target 通过后 apply 失败三条负例均通过;后续阶段保持
NOT_RUN,apply 非零不会因对象错误计数较小而降级为成功。 - 捕获诊断不含 fixture 凭据、连接串、SQL、临时路径或自由文本工具错误;既有合成成对备份/恢复回归通过,自动异机备份合同
74项通过。
截至 Attempt 3,最新备份是否真的可恢复这个核心问题仍然没有答案。目前验证到的是演练器、恢复点身份门禁和每次失败后的安全收口,不是最新备份的数据可恢复性。Attempt 1 是发布后恢复点缺口;Attempt 2 是临时副本身份规则误用;Attempt 3 目前只能确定演练器可观测性不足,底层数据库恢复失败原因仍未知。三次均为 NO_GO,但不得把次数解释为“最新备份连续三次不可恢复”。
2026-08-25 Attempt 4 前最新批次只读探针
本节仅追加探针与本地 Git 静态核对结果;未复制、解包或恢复批次,未安装恢复 runner,未执行 Attempt 4。
| 检查项 | 结果 |
|---|---|
演练前公网 /ready |
PASS,构建短标识 7b86ffb3c9ef |
| ARM 最新批次探针 | PASS |
| 最新批次构建短标识 | 7b86ffb3 |
| 来源提交唯一解析 | PASS |
| 来源与当前生产构建关系 | SAME |
| 批次年龄 | 26652 秒,约 7 小时 24 分,within_24h=yes |
| run id 与目录配对 | PASS |
| DB/附件大小与 SHA-256 | PASS |
| source image digest | 存在 |
| manifest 脱敏摘要前缀 | ee1f97a5b42d |
| 来源/当前 migration 集合 | 65/65,差异条目 0 |
| 仓库 HEAD 相对生产构建的应用代码漂移 | 文件数 0 |
| 探针 root 入口清理 | PASS,残留 0 |
| 普通用户探针暂存 | 残留 0 |
| ARM 两个监控 timer | active |
该批次满足 Attempt 4 的构建关系、新鲜度、run id 配对、产物完整性和 schema 集合前置门禁;该结论仍不是恢复授权,也不回答最新备份是否可恢复。
恢复 runner 的持久化限制不予接受:截至本节时,修复后的 runner 与八类本地回归仍位于不提交的个人 workspace,仓库尚无可由其他执行者独立复跑的 runner 合同门禁。Attempt 4 实际恢复前必须先把 runner 的稳定来源和对应合同回归纳入 scripts/qa/,由合同脚本机械验证候选 SHA-256、三阶段诊断、源/副本身份、真实 restore-db.sh 双路径和失败清理;未完成前不安装 Attempt 4 恢复入口。
2026-08-25 runner 持久化合同结果
本节仅追加本地跟踪文件与合同回归结果;未连接 ARM、未渲染现场候选、未执行 Attempt 4。
新增跟踪文件:
scripts/qa/offhost-shadow-restore-runner.template.shscripts/qa/validate-offhost-shadow-restore-attachments.pyscripts/qa/run-offhost-shadow-restore-runner-contract.sh
| 合同类别 | 结果 |
|---|---|
| 私有参数渲染、确定性候选 SHA-256、shell 注入拒绝 | PASS |
| 正式源身份与改名私有副本源侧重绑正例 | PASS |
| 源 basename/run id 错配、副本篡改、副本 run id 与源不同负例 | PASS |
| dynamic migration 集合指纹正/反例,无硬编码数量 | PASS |
| 附件本地路径/大小/SHA-256 正例,路径逃逸与 S3 负例 | PASS |
三阶段诊断、执行路径、两个退出码、后续 NOT_RUN |
PASS |
真实 scripts/restore-db.sh Docker stdin 与 PostgreSQL 16 host-file 路径 |
PASS/PASS |
| apply 非零失败关闭、诊断先于清理、清理后可读、无敏感自由文本 | PASS |
持久化模板不含远端地址、个人用户目录、远端账号、现场路径、实际容器/数据库名、生产构建值或 migration 指纹;这些值只能来自私有 JSON,并在候选写出前通过精确字段集、类型、字符集、绝对路径、shadow 命名和摘要格式校验。check-sensitive-content.mjs 对三个新增文件定向扫描结果为 PASS,扫描文件 3、finding 0;本地测试容器残留 0。
Attempt 4 的 runner 产生过程已从个人 workspace 人工产物改为“跟踪模板 + 跟踪合同 + 私有参数确定性渲染 + 候选 SHA-256”。合同未通过不得生成或上传候选。该结果只补齐演练器可重复性,仍不构成最新备份可恢复性结论或 Attempt 4 恢复授权。
2026-08-25 私有渲染与阶段日志门禁补强
本节仅追加本地门禁修订与合成回归结果;未连接 ARM、未创建现场私有配置、未渲染或上传 Attempt 4 候选,也未执行实际恢复。
.gitignore已增加专用/.restore-drill-private/目录;合同分别注入仓库内未忽略的私有 JSON 和未忽略的候选输出路径,两者均在候选创建前按预期失败。ignored 目录内的合成输入/输出正例通过,测试文件在退出时清理。- 私有原始日志仍为
0600且只在初始化时截断一次;PostgreSQL 容器启动、dump list、目标确认、restore apply 和附件恢复使用固定阶段标记追加。合同静态拒绝后续截断,最终 runtime 清理与只输出 allowlist 诊断的边界不变。 - PostgreSQL 16 fixture 增加连续成功查询的稳定就绪门禁,避免镜像首次初始化临时服务关闭窗口造成合成回归假失败;真实
scripts/restore-db.sh字节未修改,Docker stdin 和 host-file 两条路径仍直接执行仓库脚本并通过。 - 本轮新增门禁回归全部
PASS;完成完整质量门禁并形成可追溯 Git 提交前,仍不得准备或上传 Attempt 4 候选。
完整本地门禁随后通过:runner 合同全部 PASS,既有隔离备份/恢复合成演练 PASS,自动异机备份合同 74/74 PASS;task validate、Trellis governance、process baseline、默认敏感扫描和三个新增文件定向敏感扫描均 PASS。文档审计错误 0,仅保留既有未登记文件 warning;shellcheck 在本机不可用,已以 Bash 语法、真实双路径集成回归和仓库合同覆盖。测试容器、本地私有渲染目录及候选残留均为 0。
Attempt 4 环境差异预先裁决规则
本节在 Attempt 4 新批次探针和实际恢复之前追加,不能按执行结果事后改写。
- Attempt 2 的源/副本身份重绑和 Attempt 3 的三阶段诊断已经由提交
5ec7db1下的可复跑合同机械把关;合同失败时不得生成或上传候选。 - 若同一提交的合同、候选 SHA-256 和 ARM 安装前摘要均通过,但 Attempt 4 再次在恢复完成前暴露演练器侧失败,则默认 finding 升级为“本地合同环境与 ARM 实际执行环境存在系统性差异”,不再直接归为又一个孤立 runner 缺陷。
- 后续调查必须先形成脱敏环境差异矩阵,至少比较 Docker/Podman 实现与版本、PostgreSQL 16 镜像摘要和工具版本、shell/coreutils/openssl/python 行为、文件系统与权限/SELinux/AppArmor、可用 PATH、恢复脚本及依赖摘要、网络模式和资源约束。未取得差异证据前,不继续逐点放宽或修补 runner。
- 该规则不把任何失败预判为 ARM 环境问题,也不改变备份内容判定。若阶段诊断明确证明 dump、数据库对象、migration 或附件内容不满足既有合同,仍按对应数据恢复 finding 如实分类。
2026-08-25 Attempt 4 执行前最终探针与候选
本节仅追加最新批次只读探针、候选本地渲染与合同结果;未上传或安装恢复候选,未执行 Attempt 4 实际恢复,未写腾讯云生产或 ARM 正式归档。
| 检查项 | 结果 |
|---|---|
| ARM 最新正式批次探针 | PASS;动态重选后仍为同一最新批次 |
| 最新批次构建短标识 | 7b86ffb3 |
| 来源与当前生产构建关系 | SAME |
| 批次年龄 | 38572 秒,约 10 小时 43 分,within_24h=yes |
| run id 与目录配对 | PASS |
| DB/附件大小与 SHA-256 | PASS |
| source image digest | 存在 |
| manifest 脱敏摘要前缀 | ee1f97a5b42d |
| 探针临时 root 入口/sudoers 残留 | 0/0 |
| 普通用户探针暂存 | 再次通过 SHA 清单后逐一删除,残留 0 |
| ARM dead-man/健康 timer | active/active |
| 来源/当前 migration 目录 | 65/65,差异条目 0 |
候选由提交 5ec7db1 的 scripts/qa/offhost-shadow-restore-runner.template.sh 和 ignored 私有 JSON 确定性渲染。私有 JSON 权限为 0600、精确 20 个字段,runner 权限为 0700,输入与输出均命中仓库根锚定 ignore。两次独立渲染的字节和 SHA-256 完全一致;候选 SHA-256 为 81f4eca2cf173196e1a57b781d28629412de7b7fe1a09b001b02d2025950391c,合同报告值与本地 sha256sum 重算一致。
完整 runner 合同再次全部 PASS:源/副本身份、三条身份负例、dynamic migration、附件安全、五阶段追加日志、失败先诊断后清理、真实 restore-db.sh Docker stdin/host-file 双路径、退出码和敏感输出边界均通过。私有候选包共 6 个文件(runner、附件校验器、两份现有恢复脚本、sudoers、SHA 清单),包内 SHA 清单复核 PASS;runner 钉住的三份依赖摘要全部匹配,sudoers 固定命令与 runner 自身路径一致,scripts/restore-db.sh 相对生产构建仍无字节漂移,本地测试容器残留 0。
准备过程有一次安全失败:首次来源 migration 枚举误把 migration_lock.toml 当作 migration 名称,现场显示 66/65。该错误发生在候选渲染前,没有产生候选;随后改为只读取来源提交 server/prisma/migrations 树下的目录对象,并与当前工作树目录集合独立比较,结果为 65/65、差异 0 后才生成私有配置。Attempt 4 若获批准,安装前仍须在 ARM 复核上述候选包 SHA 清单;批次年龄和最新批次身份也须以实际执行开始时为准,超过 24 小时或最新批次变化时重新探针,不沿用本节结论。
Attempt 4:目标数据库确认阶段失败关闭
日期:2026-08-25
本节仅追加 Attempt 4 原始脱敏结果,不改写前置探针、候选合同或环境差异预先裁决规则。
| 检查项 | 结果 |
|---|---|
| ARM 安装前候选包 SHA 清单 | PASS |
| 正式批次与私有副本前置门禁 | 已通过,否则不会进入 restore_database |
| dump list | PASS,退出码 0 |
| target confirmation | FAIL |
| 失败阶段/类别 | restore_database / restore_target_confirmation_failed |
| 执行路径 | DOCKER_STDIN |
| restore apply | NOT_RUN |
| restore 脚本/真实 pg_restore 退出码 | NOT_RUN/NOT_RUN |
| pg_restore 错误计数 | NOT_RUN |
| 稳定错误类别 | TARGET_CONFIRMATION_FAILED |
| migration/12 类关键表 | NOT_RUN/NOT_RUN |
| 附件恢复与业务关联 | NOT_RUN |
| runtime/临时 root 入口清理 | PASS/PASS |
| 普通用户候选暂存 | PASS,残留 0 |
| 结论 | NO_GO |
dump_list=PASS 只证明最新 custom dump 可由 ARM 的 PostgreSQL 16 工具读取目录;target_confirmation=FAIL 发生在实际恢复调用之前,因此本次没有数据库数据写入、migration 查询、关键表读取、附件解包或附件关联校验。数据恢复 RTO 未形成,实际执行时的精确 RPO 秒数未被失败输出保留;只能确认 runner 已在创建 runtime 前通过不超过 24 小时的硬门禁,不能用前置探针年龄冒充执行时精确 RPO。
本轮输出没有分别保留 target query 的退出码和“查询成功但数据库名不等”布尔值,现有证据尚不能区分连接/认证/工具调用失败与目标名称比较失败。正式归档在 runner 中只有读取和复制路径,未观察到归档写入;但失败发生在末尾源批次复校验之前,因此“正式归档零修改”仍需后续只读摘要复核,不以代码静态结论替代现场证据。演练后生产 /ready 也须另行补充,不能从 ARM timer 或清理 PASS 推断。
按 Attempt 4 开始前已提交的裁决规则,本次先登记为 local_contract_vs_arm_environment_difference_investigation,不是新的孤立 runner 缺陷,也不是备份不可恢复证据。下一步仅允许建立脱敏环境差异矩阵和补齐生产/归档只读复核;在差异证据、修订设计和新批准之前,不修改 runner、不创建 Attempt 5、不复用本批次盲重试。
Attempt 4 失败后的只读环境差异调查
本节仅追加失败后的只读 ARM 信息与本地合成复现;未在 ARM 启动调查容器、未读取业务数据、未修改 runner 或正式归档。
| 维度 | 本地合同环境 | ARM 实际环境 | 判定 |
|---|---|---|---|
| CPU/容器架构 | x86_64 / linux/amd64 |
aarch64 / linux/arm64 |
不同 |
| Docker client/server | 25.0.4/25.0.4 |
28.0.2/28.0.2 |
不同 |
| PostgreSQL 16 镜像实例摘要前缀 | fff3594bf464 |
c05eced0bdb4 |
多架构实例不同 |
| 存储/cgroup/rootless | overlay2/systemd/no |
overlay2/systemd/no |
相同 |
| Bash/Python/OpenSSL/coreutils | 5.1.4/3.9.2/1.1.1w/8.32 |
5.1.4/3.9.2/1.1.1w/8.32 |
相同 |
| AppArmor/SELinux | Y/unavailable |
Y/unavailable |
相同 |
| 工作文件系统 | ext2/ext3 类 |
ext2/ext3 类 |
相同 |
| 必需命令 | 合同执行通过 | PASS |
无缺失证据 |
差异矩阵本身不能证明 Docker 28 或 arm64 有缺陷。真正收敛根因的是启动时序覆盖不一致:tracked runner 在启动 PostgreSQL 后以第一次 pg_isready 成功结束等待;本地合同夹具曾真实出现“初始化临时服务正在关闭”的失败,之后夹具改成连续两次真实 SELECT 1 成功才继续,但 runner 的等待实现没有同步修改。于是合同环境稳定化后绕过了 runner 的竞态,而 ARM 仍执行首次 pg_isready 路径。
失败序列与该机制一致:dump list 不连接目标数据库,因此可在 PostgreSQL 初始化服务切换窗口中 PASS/0;紧随其后的 target query 需要真实连接,命中关闭/重启窗口即进入 TARGET_CONFIRMATION_FAILED,actual restore 尚未调用。本地使用相同镜像配置、相同随机凭据和 runner 中完全相同的 target query,在连续两次真实查询确认稳定后得到退出码 0、数据库名精确匹配、stderr 为空;本地测试容器随后清理为残留 0。
当前高置信度根因类别为 postgres_initialization_readiness_race,上位 finding 仍为开跑前定义的 local_contract_vs_arm_environment_difference_investigation。系统性差异具体表现为:合同夹具和 runner 没有复用同一稳定就绪路径,架构/容器版本造成的时序差异把该覆盖缺口暴露在 ARM。修复方向应是 runner 与合同共同执行“连续成功的真实目标数据库查询”并保留 target query 数字退出码与名称匹配布尔值,而不是为 ARM 增加一次固定 sleep、放宽目标确认或再次只修测试夹具。该修复和任何 Attempt 5 均须先更新设计、补可复跑负例并重新批准。
失败后只读残留复核:一次性容器、runtime、root 入口、恢复库目录、sudoers 和普通用户暂存均为 0;ARM dead-man 与公网健康 timer 均为 active。演练后生产 /ready 与正式归档摘要在此时仍待独立只读补证。
Attempt 4 后正式归档只读复核
日期:2026-08-25
本节追加 Attempt 4 后正式归档只读探针结果,不回写前置探针或 Attempt 4 原始失败记录。探针不复制、不解包、不恢复、不启动数据库容器,也不修改正式归档。
| 检查项 | 结果 |
|---|---|
| 只读探针 | PASS,退出码 0 |
| 最新批次构建短标识 | 7b86ffb3,与当前生产构建相同 |
| 探针时批次年龄 | 42007 秒,约 11 小时 40 分,within_24h=yes |
| run id 与目录配对 | PASS |
| DB/附件产物完整性 | 大小与 SHA-256 聚合 PASS |
| source image digest | 存在 |
| manifest 脱敏摘要前缀 | ee1f97a5b42d |
| 探针临时访问清理 | PASS |
| 探针入口/暂存残留 | 0/0 |
Attempt 4 前后探针的最新构建短标识、run id 配对、产物完整性和 manifest 脱敏摘要前缀一致,后置探针仍在 24 小时内通过。由此正式归档的内容级零修改已取得现场只读补证;该结论仅覆盖正式归档,不替代恢复成功证明,也不回答最新备份是否可恢复。
腾讯云生产演练后 /ready 仍未独立补证。ARM 的 timer active、清理 PASS 和归档探针均不能推导生产在线状态;在取得 /ready 正常状态及 12 位构建前缀前,AC2 保持未满足,证据不得表述为生产前后均正常。
Attempt 4 后生产只读补证与本地稳定就绪修复
日期:2026-08-25
执行方沿既有审计窄路径完成腾讯云生产演练后 /ready 只读核验:状态 PASS,12 位构建前缀 7b86ffb3c9ef,与演练前记录一致。地址和原始响应未写入本证据;该读取没有登录、写库、重启、切换或配置修改。至此 Attempt 4 的生产前后 /ready 均正常已补证,但不改变本轮 restore apply 未执行和最新备份可恢复性仍未知的结论。
用户随后批准的范围仅为本地 runner 修复与回归,不连接 ARM、不执行 Attempt 5。修复把 PostgreSQL 就绪从默认 Unix socket 的首次 pg_isready 改为容器内 TCP 回环 127.0.0.1 的真实 SELECT current_database():initdb 临时服务器不监听 TCP,因此该判据结构性排除临时服务器;连续两次成功和最多 60 次有界重试只作为额外稳定保险。目标名称成功但不匹配立即失败,查询非零重置 streak;实际 restore-db.sh 仍走原路径且字节不改。
合同夹具与 runner 分叉审计结果:
| 覆盖对象 | 合同机制 | 是否存在等价逻辑分叉 |
|---|---|---|
| 私有配置渲染与候选 SHA | 直接使用跟踪模板和合同内唯一渲染器 | 否 |
| 正式源/临时副本身份 | 从 rendered runner 提取 validate_batch 同一实现 |
否 |
| PostgreSQL 目标就绪 | 从 rendered runner 提取同一 TCP 目标确认函数;真实 PostgreSQL 16 fixture 也调用该函数 | 原分叉已消除 |
| DB restore Docker stdin/host-file | 直接执行仓库 scripts/restore-db.sh |
否 |
| 附件引用安全 | 直接执行跟踪附件校验器 | 否 |
| 失败诊断与 finally 清理 | 从 rendered runner 提取初始化和 finalization 同一实现 | 否 |
| migration 指纹 | 对 rendered 值、目录集合变化和禁止硬编码做静态/合成断言 | 无等价重写;真实恢复集合仍待远端演练 |
| 12 类表、storage 与完整编排 | 本地只做组件合同,不执行含真实备份的完整 runner | 明确覆盖限制,不能推断远端成功 |
就绪回归全部 PASS:socket 路径假定可用但 TCP 永久失败仍在第 60 次失败;首次查询失败后连续两次成功在第 3 次通过;一次成功后失败会清零 streak 并在第 4 次通过;目标名不匹配第 1 次立即失败;永久查询非零以 TARGET_QUERY_NONZERO 失败。真实 PostgreSQL 16 fixture 使用 extracted rendered-runner 函数就绪后,restore-db.sh Docker stdin 与 host-file 两条路径均成功;dump-list、目标错配、apply 非零、诊断先于清理、敏感输出负例继续通过。本地测试容器残留 0。
完整复跑结果:runner 合同全部 PASS,既有成对备份/恢复演练 PASS,自动异机备份合同 74/74 PASS;task validate、Trellis governance、process baseline、默认/定向敏感扫描和 git diff --check 均通过。文档审计错误 0,仅保留既有未登记文件 warning;scripts/restore-db.sh 相对生产构建无字节漂移,测试容器和生成缓存残留均为 0。
本地合同通过只证明演练器修复与组件路径,不回答最新正式备份是否可恢复。下一步仍须先提交可追溯变更,再动态选择最新正式批次、执行只读身份探针并重新渲染候选;Attempt 5 实际恢复必须另行批准。
Attempt 5 前最新正式批次只读身份探针
日期:2026-08-25
本节仅追加用户批准的最新正式批次只读探针与本地仓库关系核对;未执行恢复、未复制或解包正式归档、未启动 Docker、未读取数据库或附件内容。
| 检查项 | 结果 |
|---|---|
| 探针结果 | PASS;退出码未单独输出 |
| 最新批次构建短标识 | 7b86ffb3 |
| 来源与当前生产构建关系 | SAME |
| 探针时批次年龄 | 64625 秒,约 17 小时 57 分,within_24h=yes |
| run id 与目录配对 | PASS |
| DB/附件大小与 SHA-256 | PASS |
| source image digest | 存在 |
| manifest 脱敏摘要前缀 | ee1f97a5b42d |
| 来源/当前 migration 目录 | 65/65,差异 0 |
| 仓库 HEAD 相对生产构建的应用代码漂移 | 文件数 0 |
| 探针 root 入口/普通用户暂存残留 | 0/0 |
| ARM dead-man/健康 timer | active/active |
timer 状态只采用已验证私有配置中的实际 unit 名只读取得;一次使用猜测名称得到的无效读数已当场撤销,未进入证据结论。探针结果表明该最新批次继续满足构建、新鲜度、身份和完整性前置门禁,但仍不是恢复成功证明。
Attempt 5 最终候选尚未渲染。原因是用户提出的 RTO 分阶段记录已进入设计、但 runner 实现尚未另行批准;若先渲染,后续计时代码会改变候选字节和 SHA-256。必须先裁决并完成 RTO 八阶段本地合同,再从跟踪模板重新确定性渲染候选并核对 SHA-256,实际恢复仍须单独批准。
Attempt 5 前八阶段 RTO 本地合同
日期:2026-08-25
用户随后批准的范围仅为本地实现八阶段 RTO 计时并执行合同回归;本阶段未连接 ARM、未渲染现场候选、未生成 Attempt 5 候选 SHA-256,也未执行实际恢复。此前探针批次的 64625 秒新鲜度只属于探针时点,后续不得复用,实际恢复前必须重新选择最新正式批次并重跑只读身份门禁。
runner 现在以连续边界依次记录 batch_copy、postgres_container_start、dump_list、target_confirmation、restore_apply、database_verify、storage_attachment_verify 和 final_integrity。同一个时间点关闭前一段并开启后一段;成功路径八段均为非负整数且精确加总为唯一的 data_restore_rto_seconds。失败路径先结算当前阶段,保留已完成和当前阶段的数字,后续阶段为 NOT_RUN,然后才清理私有 runtime。最长 60 次 TCP 目标确认及其 1 秒间隔只位于 target_confirmation 边界内。
可复跑合同直接驱动 rendered runner 的同一计时函数。确定性成功夹具得到八段 3/5/2/11/7/13/17/3 秒,总计 61 秒且总字段只输出一次;目标确认失败夹具保留 2/4/1/9 秒、后四段 NOT_RUN、部分总计 16 秒,并证明计时输出位于 runtime cleanup 之前;restore apply 失败夹具记录当前段 12 秒、后续三段 NOT_RUN、部分总计 28 秒。捕获输出不含路径、连接串、凭据或 SQL。
完整本地门禁结果:runner 合同全部 PASS;真实 PostgreSQL 16 fixture 和未修改的 scripts/restore-db.sh Docker stdin/host-file 双路径继续通过;既有成对备份/恢复演练 PASS;自动异机备份合同 74/74 PASS;task validate、Trellis governance、process baseline、docs registry、敏感扫描和 git diff --check 通过。文档审计错误 0,保留既有未登记文件 warning;本地测试容器残留 0,scripts/restore-db.sh 相对生产构建仍无字节漂移。
Attempt 5 前增强停止线从本节起生效:八阶段计时完成并通过合同后,下一份满足 24 小时门禁的新鲜批次直接进入最终候选渲染、SHA-256 核对、只读探针和实际恢复审批。此后发现的改进项,除非会导致 Attempt 5 失败或产生错误结论,否则只登记为后续任务,不再作为 Attempt 5 前置增强插队。本地合同通过仍只证明演练器合同,不回答最新正式备份是否可恢复。