修复(08-28-production-arm-backup-transfer-incident): 纠正partial停止偏移分类
这个提交包含在:
@@ -2857,11 +2857,21 @@ current shared mapping -> fine-grained stages + mutation listeners
|
||||
offsets or durations indicate a deterministic limit; materially different
|
||||
offsets/durations favor intermittent timeout or network interruption. Do not
|
||||
infer this split from service exit `1`, which is the wrapper's generic code.
|
||||
- A stopping-offset pair is `similar` only when its absolute difference is no
|
||||
more than both `50000 ppm` (5% of the larger aggregate) and `67108864` bytes
|
||||
(64 MiB), equivalently `difference <= min(relative_limit, absolute_limit)`.
|
||||
Using `max(...)` is forbidden: it misclassifies a small nonzero partial and a
|
||||
zero-byte partial as similar merely because both are below 64 MiB.
|
||||
- `last-deadman.json` proves the detector decision. `deadman-alert-state.json`
|
||||
proves the deduplication state. A journal allowlist line with
|
||||
`evaluated sent=true` proves the helper attempted and accepted one send; it
|
||||
does not prove a human noticed the message. Repeated `sent=false` for the same
|
||||
failure is normal deduplication, not a monitoring blind spot.
|
||||
- Keep the two JSON schemas distinct: the dead-man summary contains
|
||||
`status/category/findingCount/findings`, while the alert deduplication state
|
||||
contains only `schemaVersion/status/category`. A probe must not request
|
||||
`findingCount` from the alert state or report its contractual absence as an
|
||||
invalid remote state.
|
||||
- Read-only investigation output is an allowlist of status/category/count/age
|
||||
and capacity fields. Never print unit command lines, raw JSON, raw journal,
|
||||
remote identities, paths, run ids, webhook data or diagnostic stderr.
|
||||
@@ -2961,6 +2971,8 @@ current shared mapping -> fine-grained stages + mutation listeners
|
||||
rsync codes `23/30/12/11/255`; assert the incident summary preserves code and
|
||||
elapsed-time evidence separately from wrapper exit `1` and does not output
|
||||
file names.
|
||||
- Assert `2.68 MB` versus zero is `different`, a large pair within both 5% and
|
||||
64 MiB is `similar`, and exceeding either threshold is `different`.
|
||||
- Fixture one complete source batch, no complete source batch, checksum/size
|
||||
corruption and malformed manifest. Assert the risk classification and
|
||||
transition recommendation change without emitting paths, run ids, artifact
|
||||
@@ -2987,6 +2999,7 @@ state path test returned false -> state missing
|
||||
partial count > 0 -> finalize failed
|
||||
off-host stale -> assume one complete backup still exists on production
|
||||
service exit 1 -> call it the rsync exit code and use service duration
|
||||
partial difference <= max(5% limit, 64 MiB) -> similar
|
||||
```
|
||||
|
||||
#### Correct
|
||||
@@ -3000,4 +3013,5 @@ derive unit identity from installer -> timer trigger + service Result/exit
|
||||
-> safe scheduler/source/transfer/finalize/alert conclusion
|
||||
-> validate the source batch first and classify single-point vs no proven point
|
||||
-> bind retained diagnostics to the safe failure phase; unavailable clocks stay unavailable
|
||||
-> partial difference <= min(5% limit, 64 MiB) before calling it similar
|
||||
```
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
- 生产侧先用 manifest/三文件/身份/字节元数据枚举全部本地候选,只对最新一个元数据合格候选完整读取 database/storage 并计算 SHA-256;旧候选明确输出 `older_metadata_batch_hash_status=NOT_RUN`,不得为了计数连续读取数 GiB。最新候选只有通过完整 SHA-256 合同才输出 `complete_local_recovery_point=yes`;其哈希失败时不得现场回退扫描旧候选,而应保守标为“无已证明可用备份”。输出年龄、元数据合格数量和聚合大小,不输出 run id、文件名或路径。该结果先于传输根因分析,决定当前是“生产单点”还是“无已证明可用备份”。ARM 仍对正式归档执行完整哈希合同。
|
||||
- 对成功与失败路径分别输出本地完整批次、生成期临时对象和保留策略的聚合结果,明确 `retained_after_failure`、`pruned_after_success` 和连续失败累积趋势;不从单次目录计数猜测脚本语义。
|
||||
- partial 只输出每个匿名样本的总字节数、预期总字节数、安全比例和年龄;不输出名称、路径、run id 或 artifact 名称。
|
||||
- partial 停止偏移的“接近”必须同时满足相对差异不超过 `50000 ppm`(5%)和绝对差异不超过 `67108864` bytes(64 MiB),等价于以两者较小值作为上限。不得用两者较大值,否则小样本与零字节样本会被绝对阈值错误吞并为 `similar`。
|
||||
- 生产私有诊断只输出固定的 `transfer_exit_code`、`failure_category`、开始/结束 epoch 和持续秒数。若现有状态没有持久化真实 rsync 码或开始时刻,记录 `NOT_AVAILABLE`,需要本地设计可观测性修订并另行批准部署,不能现场改脚本。
|
||||
- 容量只输出 filesystem 使用率/可用 GiB、完整/不完整/临时批次数量和聚合字节,不列目录。
|
||||
- 采集器先探测账号能力;无 sudo 时不得尝试临时扩权。需要 root-owned 状态时先提交固定 allowlist 入口设计并单独审批。
|
||||
@@ -93,3 +94,12 @@ PLANNING
|
||||
- 腾讯云生产和 ARM 地址、账号、绝对现场路径、完整 run id/digest、密钥、known_hosts、webhook、原始日志和业务内容不进入仓库或聊天。
|
||||
- 远端临时探针若获批,固定目标、SHA-256、语法、sudoers 和自清理均需机械校验,并由外层独立报告入口/暂存残留。
|
||||
- timer `active`、partial 存在、单次备份成功或告警 `sent=true` 都是局部证据,不能扩写为链路整体正常。
|
||||
|
||||
## 2026-08-28 只读调查裁决
|
||||
|
||||
- 生产本地存在一个通过完整哈希合同的最新可用批次,风险分类为 `production_single_point`,不是“当前无已证明可用备份”;元数据合格批次 7 个,失败传输后保留本地批次、异机成功后才 prune,过渡动作保持 `wait_natural`。
|
||||
- ARM 原始调查输出曾将两个 partial 判为 `similar`。该输出保留作为原始证据;现场聚合字节为 `2682168` 与 `0`,按修正后的“双阈值同时满足”合同人工重算为 `different`。一个老 partial 已满足完整合同但未 finalize,另一个较新 partial 为零字节,两者不支持“固定相同偏移的确定性传输上限”,且可能来自不同失败类型。
|
||||
- 现有状态未持久化本次失败的 rsync 原始退出码和传输专属时钟,均保持 `NOT_AVAILABLE`;只读证据不足以证明单一根因。若后续自然周期再次失败,按既定边界转为单独审批的一次受控观测性重传,不扩大只读读取范围来补造结论。
|
||||
- 调查时 ARM 已出现约 12 小时 43 分的新鲜完整原子归档,dead-man 最近 service 为 `success/0` 且状态为 healthy,说明异机链已自然恢复一次;该事实可解除恢复演练的事件阻塞,但不满足本 P1 事件的两个自然周期结案条件。
|
||||
- 私有探针输出的 `arm_deadman_alert_state_finding_count=invalid` 是表示缺陷,不是 ARM 状态损坏:`last-deadman.json` summary 才有 `findingCount`,`deadman-alert-state.json` 按稳定 helper 合同只保存 `schemaVersion/status/category`。后续探针不得对 alert state 索取不存在的 count 字段。
|
||||
- ARM health service 同期为 `failed/1`,而生产公网 `/ready` 调查前后均为 PASS、12 位构建前缀一致。该 finding 指向健康监控执行、状态或通知链,不得扩写为 OA 当前不可用;进一步读取或处置需另行授权。
|
||||
|
||||
@@ -11,22 +11,40 @@
|
||||
- [x] 从 `scripts/run-automatic-backup.sh`、receiver、dead-man 和 systemd installer 提取真实单位名、状态路径来源、阶段和退出码合同,禁止手写近似常量。
|
||||
- [x] 实现只输出 allowlist 的跟踪只读采集器:生产本地完整恢复点合同、本地成功/失败清理保留语义、partial 匿名字节比例、rsync 原始码、无法取得的传输时钟、两端容量;timer/service 继续复用既有只读 systemd 聚合合同,不在 Python 中另写一套。
|
||||
- [x] 为本地完整/无完整批次、成功清理/失败保留、相近/不同停止偏移、rsync `23/30/12/11/255`、缺失持续时间、权限拒绝、畸形 manifest 和敏感输出补合成回归。
|
||||
- [ ] 本地语法、合同、敏感扫描全部通过后,提交远端只读调查摘要与精确权限边界,单独申请执行批准。
|
||||
- [x] 本地语法、合同、敏感扫描全部通过后,提交远端只读调查摘要与精确权限边界,单独申请执行批准。
|
||||
|
||||
### 本地准备结果(2026-08-28)
|
||||
|
||||
- 跟踪调查器:`scripts/qa/inspect-backup-transfer-incident.py`;仅接受显式绝对路径参数,不回显参数值,未知异常也只输出稳定类别。
|
||||
- 可复跑合同:`scripts/qa/run-backup-transfer-incident-contract.sh`;19 项合成检查通过,fixture 与 Python cache 零残留;权限拒绝不会被误写成“无有效备份”,其他阶段的旧日志不会冒充本轮 rsync 诊断;生产多个批次时只哈希最新元数据合格候选,最新哈希失败不回退扫描旧批次。
|
||||
- 可复跑合同:`scripts/qa/run-backup-transfer-incident-contract.sh`;初始 19 项合成检查通过;现场分类缺陷修复后扩展为 20 项,fixture 与 Python cache 零残留。权限拒绝不会被误写成“无有效备份”,其他阶段的旧日志不会冒充本轮 rsync 诊断;生产多个批次时只哈希最新元数据合格候选,最新哈希失败不回退扫描旧批次;停止偏移必须同时满足 5% 和 64 MiB 双阈值。
|
||||
- 既有 `run-automatic-backup-contract.sh` 74 项通过;生产 wrapper 未改。现状没有持久化传输专属开始/结束时刻,故持续时间必须为 `NOT_AVAILABLE`。
|
||||
- rsync 原始码只在安全摘要明确为 `phase_offhost_transfer_failed` 时从 root 私有 `last-error.log` 固定正则提取,避免把 paired-backup/finalize 的旧诊断错误归因给传输。
|
||||
|
||||
## Phase 2:获批的远端只读调查
|
||||
|
||||
- [ ] 在不修改生产/ARM 的前提下采集四类证据;每个临时入口在同轮精确清理并由外层独立复核零残留。
|
||||
- [ ] 先判定“生产单点”或“无已证明可用备份”,再交叉判定主要根因、次要候选、清理/累积语义和容量预算;无法取得的字段标为 `NOT_AVAILABLE/NOT_MEASURED`。
|
||||
- [ ] 记录过渡恢复点决策;只读证据无法收敛时停止扩读,提交一次受控观测性重传的独立审批方案,不直接执行。
|
||||
- [x] 在不修改生产/ARM 的前提下采集四类证据;每个临时入口在同轮精确清理并由外层独立复核零残留。
|
||||
- [x] 先判定“生产单点”或“无已证明可用备份”,再交叉判定主要根因、次要候选、清理/累积语义和容量预算;无法取得的字段标为 `NOT_AVAILABLE/NOT_MEASURED`。
|
||||
- [x] 记录过渡恢复点决策;只读证据无法收敛时停止扩读,提交一次受控观测性重传的独立审批方案,不直接执行。
|
||||
- [ ] 更新本 task 证据,给出单一最小修复方案、回滚方案和生产写操作清单;停止并请求用户审阅。
|
||||
|
||||
### 远端只读调查结果(2026-08-28,追加时间线)
|
||||
|
||||
- 公网 `/ready` 调查前后均 PASS,12 位构建前缀均为 `7b86ffb3c9ef`;生产固定入口与 staging 经外层独立枚举均为 0,ARM `arm_independent_residue=0`。调查未改变在线 OA、正式归档、timer 或业务数据。
|
||||
- 生产:`complete_local_recovery_point=yes`、`backup_risk_classification=production_single_point`;完整哈希通过的最新批次 1 个、元数据完整批次 7 个,最新候选年龄 42885 秒、聚合 11400180 bytes;生产文件系统 82% 使用、10 GiB 可用。源码顺序证明传输失败保留完整本地批次、异机成功后 prune、生成临时对象在失败路径清理,故过渡动作是 `wait_natural`。
|
||||
- 生产最近 daily/watchdog service 均为 `success/0`、timer active;历史失败摘要未保留可绑定到本轮的 rsync 原始码或传输专属时钟,因此退出码和时长均为 `NOT_AVAILABLE`,不得用 service exit、mtime 或对话时刻替代。
|
||||
- ARM 原始输出:partial 2 个,聚合 2682168 bytes,样本总字节分别为 2682168 与 0;调查器当时输出 `arm_partial_stop_offset_class=similar`。该原始结果不回写。
|
||||
- 分类缺陷修订:旧实现以 `max(64 MiB, 5%)` 为接近上限,使 2.68 MB 与 0 字节被 64 MiB 阈值错误吞并。按固定阈值必须同时满足的合同,人工重算为 `different`;一个约 32 日龄 partial 满足完整合同但未 finalize,另一个约 36 小时龄 partial 为零字节,不能据此声称两次都停在同一确定性偏移。
|
||||
- ARM 正式原子归档 7 个、无无效归档,最新完整归档年龄 45756 秒;inbox/archive 文件系统 54% 使用、89 GiB 可用;dead-man service `success/0`、timer active、最近状态 `PASS_OFFHOST_DEADMAN_FRESH/healthy`。链路已自然恢复一个新鲜原子批次,可解除恢复演练的事件阻塞,但 P1 仍需第二个连续自然周期及容量验收后才能结案。
|
||||
- `arm_deadman_alert_state_finding_count=invalid` 不表示远端状态损坏。仓库 helper 明确把 alert state 写成仅含 `schemaVersion/status/category` 的去抖状态;`findingCount` 属于 `last-deadman.json` summary。私有探针错误复用了 summary 读取模板,后续证据不得把这个表示缺陷升级为告警链 finding。
|
||||
- 独立 finding:ARM health service 为 `failed/1`,但公网 `/ready` 实测前后一致且 PASS。这是健康监控执行/状态/通知链问题,不是 OA 当前不可用的证据;本次授权不覆盖进一步调查或修改。
|
||||
- 根因裁决:历史单一 rsync 原始码和可靠传输时长不可得,两个 partial 的真实停止位置明显不同,现有证据不能收敛为单一根因。不得把自然恢复一次冒充“已实施修复”;后续若再失败,候选动作是另行批准的一次受控观测性重传。生产 10 GiB 余量仍需独立容量方案。
|
||||
|
||||
### 分类缺陷复盘
|
||||
|
||||
- 根因类别是“隐含假设 + 测试覆盖缺口”:实现用 `max(absolute, relative)` 把两个独立上限误写成二选一宽松上限;旧正例夹具只证明“绝对差异较小”,没有证明相对差异也在 5% 内,因而为错误实现背书。
|
||||
- 修复机制已闭环:实现改为 `min(...)`;合同直接覆盖现场 `2682168/0`、同时满足双阈值的正例、分别越过相对/绝对阈值的负例;稳定 spec 写明公式、错误写法与断言点。仓库搜索确认该分类公式只有这一个实现位置,不存在需要同步的第二套逻辑。
|
||||
- 原始远端 `similar` 保留,人工修订追加为 `different`;以后任何现场分类缺陷都按同样的不可变时间线处理,不回写旧证据。
|
||||
|
||||
## Phase 3:本地修复与变更审批
|
||||
|
||||
- [ ] 仅修改证据支持的脚本/installer/合同;不轮换已通过初始认证的密钥,不放宽 manifest/finalize 门禁。
|
||||
|
||||
在新工单中引用
屏蔽一个用户