# 隔离备份恢复演练证据 ## 证据边界 - 仅记录日期、角色、短构建标识、状态、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-backup` service 构建 drop-in 的最大文件 mtime;终点是最新 manifest `createdAt`。 - 该差值可称为内部诊断指标“合同 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.sh` - `scripts/qa/validate-offhost-shadow-restore-attachments.py` - `scripts/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 前置增强插队。本地合同通过仍只证明演练器合同,不回答最新正式备份是否可恢复。