跳转到主要内容

混合 EC 布局与迁移列表

本文记录 issue #261 与 PR #262 的审查结论,本文已按 #276 后的 main 0fc5a0b5 更新。实际修复通过维护者 PR #267 合入,#262 是后来被取代的重复 PR。

先说结论

问题成立。一次成功的降级覆写可能让一个无版本对象同时存在两种 EC 布局:在线磁盘上有更新的副本,离线磁盘上保留旧副本。GET 与 ListObjectsV2 能选出更新的可读多数;decommission 与 rebalance 使用 all-versions 遍历,此时元数据合并可能以旧布局为参考,只统计旧布局。旧布局低于 quorum 后,对象就从迁移列表中消失。迁移报告成功,旧池被移除后对象丢失。

PR #262 的修复方向正确。它只放宽已有 mixed-EC recount 的一个过窄条件,仍把 recount 限制在“每个非空流都只有一个普通 null version”的安全范围,并增加 quorum、流顺序、迁移和排除场景测试。该修复已经通过 #267 合入,#267 还增加了完整的池移除回归测试。

什么条件会触发

需要同时满足以下条件:

  1. 对象存储在纠删码集合中。
  2. 写入或覆写发生时有一个或多个磁盘不可用。
  3. 可用性优先的写入在在线磁盘上使用了不同的 parity 或 layout。
  4. 离线磁盘仍保留旧元数据。
  5. 在 decommission 或 rebalance 扫描前,修复没有完成。

只要某个 EC 组达到 quorum,对象仍然可读。问题是否出现还取决于旧、新元数据流到达的顺序,所以不同磁盘组合下表现会有差异,但底层状态本身是确定的。

什么时候引入

混合布局是可用性优先降级写入的自然结果。SILO 相关的列表修复在 2026-10-16 的 e90cb2067 中引入,用于处理更广泛的列表一致性问题。该修复只为 latest-only 读取打开了 recount;all-versions 的迁移调用仍使用旧选择路径。

于是出现了分裂契约:

调用方 请求模式 main 上的行为
GET 与 ListObjectsV2 latest-only 对符合条件的混合 EC 组重新计数,选出可读 quorum。
ListObjectVersions、decommission、rebalance 及部分 Walk 调用 all versions 普通合并失败后停止,可读多数被少数旧布局遮住。

所以这个缺陷来自 2026-10-16 修复的范围没有覆盖完整调用链。底层降级元数据状态可以更早存在;真正暴露数据丢失路径的是只有一类调用方学会了恢复。

根因,用简单的话说

假设有 16 个磁盘:4 个磁盘仍保存旧的 12+4 元数据,12 个磁盘保存新的 8+8 元数据。两组都描述同一个 null version,但 EC header 不同。

合并逻辑按顺序查看元数据流。看到差异后,它可能以当前 EC 作为参考,只统计匹配该 EC 的条目。旧组只有 4 个,达不到 8 的 quorum,于是返回空结果。latest-only 读取已有的第二次 recount 会发现新组有 12 个,能够达到 quorum。

迁移路径传入的 requestedVersions 是 0,而 mixed-EC fallback 原来被 requestedVersions 等于 1 的条件保护。于是同一份元数据对 GET 是“可读对象”,对 decommission 却是“没有对象”。

这个保护条件不能简单删除所有安全边界。不能把任意 EC 组、版本历史、UUID 冲突、删除标记、free version,或 legacy/partial header 混在一起。安全范围是:每个非空流只有一个普通 null version,并且有现代 EC header。此时列最新版本与列全部版本实际上是在问同一个逻辑对象。

影响

操作员在移除存储池前看到的都是正常的:

  • GET 返回新数据。
  • ListObjectsV2 返回对象。
  • decommission 和末尾检查都看不到对象。
  • rebalance 把对象留在源池。
  • 无版本或 suspended bucket 的 ListObjectVersions 也可能漏掉对象。
  • 旧池移除后,GET 和两个列表都找不到对象。

问题严重之处在于迁移状态会错误地报告成功。旧池一旦删除,通常无法再重试恢复。

备选方案

方案一:迁移前必须先成功 heal

这可以作为运维缓解,但不是完整修复。磁盘可能一直不可用,MRF 可能放弃;旧元数据虽然有效,GET 也不一定会排队 heal。如果把所有迁移都阻塞到布局统一,池退役就会依赖无关的磁盘恢复。

方案二:让迁移调用方统一使用 latest-only 列表

这会把列表策略复制到 decommission 和 rebalance 中,并可能改变它们处理版本历史的方式,还会留下 ListObjectVersions 与 Walk 的不一致。

方案三:合并所有 EC 组或降低 quorum

不安全。两个都低于 quorum 的组不能因为对象名相同就合票;降低 quorum 可能选出无法重建的数据。

方案四:扩展现有的窄范围 recount

这是最小的安全改动。recount 只在普通选择失败后运行,只对非 strict 读取生效,并且只处理每个流一个普通 null version。PR #262 正是这个方案。

PR 判断

PR #262 去掉 mixed-EC 条件对 latest-only 模式的依赖,并增加:

  • requestedVersions 为 1 与 0 时相同的选择断言;
  • strict、UUID、delete marker、free version、history、legacy、partial EC 等排除场景;
  • decommission 与 rebalance 的迁移回归测试;
  • 将新回归测试加入 race workflow。

聚焦基线测试在 main 上会在可读混合 EC 场景失败,在 PR 上通过。新增测试也保留了重要的负面断言:低于 quorum 的组不能被错误合并。该补丁不改变 API、配置、磁盘格式、指标或依赖。

合入后的实现符合原建议:这个窄范围 recount 充分解决 #261,同时没有引入可能掩盖不安全元数据状态的广泛修复机制。原 issue 已标记完成,#262 已因被 #267 取代而关闭。

剩余边界

补丁只负责选出可读组,不负责修复旧组。离线磁盘恢复后仍应执行 heal。它也无法恢复旧版本中已经完成错误 decommission 后丢失的对象。其他畸形或混合版本历史仍按保守方式处理,这是刻意保留的安全边界。

验证参考