跳转到主要内容

列举不能丢掉仍有仲裁的 null 版本

警告

历史 LIST 设计已由新决策覆盖 · Earlier LIST design superseded (2026-10-17)

English: The investigation below is retained as historical evidence. Its current-design and pending-work guidance is superseded by the latest LIST recovery decision (English). Refer to that decision for the final scope, compatibility, costs and acceptance limits. This does not change older binaries or announce a production release.

中文: 下文保留历史调查证据;其中关于当前方案和待办的说明已被新设计覆盖,请参考最新 LIST 恢复设计决策(中文),以了解最终范围、兼容性、代价和验收边界。这不改变旧二进制行为,也不是生产发布公告。

发布更新,2026-09-20: null 版本仲裁修复已随 Server 20260916 发布。#218 跟踪的滚动重启列举限制仍然开放。以下调查与验证记录保留当时的日期和范围。

本文记录 2026-09-16 调查的列举漏项:未开版本控制的桶在并发覆盖写下返回 HTTP 200 的列表,却少了同一端点仍能 GET 的 key。这里说明已确认的机制、提交 8d06424b1 的窄修复、这个修复证明了什么,以及仍由 #218 跟踪的反例。

状态: 已在 main,晚于已发布的 Server 20260903。修复在实施前经过三轮外部评审,用确定性的顺序反例、签名 HTTP 列举和差分输入验证。它没有关闭列举问题;见边界。

缺陷

列举会合并被询问各盘的版本流。各盘不一致时,mergeXLV2Versions 选出一个最新版本并统计同意它的流。上游 2022 年的改动(PR #14125)让这个计票容忍 healing 留下的签名差异,这是必要的,但它的选择与计票方式对输入顺序敏感。当每块盘各有一个普通 null 版本、而更新的少数盘排在最后时,旧一代的计数会被重置。实际达到仲裁的旧一代于是被丢弃,合并返回空,解析器把这个 key 当作不存在。请求本身仍然成功,客户端看到的是一份看起来完整、其实有洞的列表。读取该 key 能成功,因为读路径自行解析仲裁。

旧代码在九种盘序下如此失败,并有完整的签名 LIST 证据,而同样内容在其他顺序下列举正确。这个缺陷在已发布的 Server 20260903 中同样存在。

修复

补做的计票只在原选择没有达到仲裁时进行,并且只针对能够精确判断的输入:每个非空盘流恰好有一个普通 null 版本,没有 free version,纠删参数全部相同,而且按裁剪前的原始输入判断。满足时按头部重新分组,只有达到原有效仲裁的组才会返回。混合历史、显式版本 ID、删除标记、free version 与混合纠删布局保持原有行为;严格模式、签名、平票与代表条目的规则不变,合并的共享调用方对所有原本成功的输入得到同样的结果。

验证:九个旧顺序与七个新顺序反例通过;5,620 个差分输入保持约定结果;完整签名 HTTP 列举、对象读取、普通与 race 运行,以及扫描器、healing、迁移这些合并的消费方都做了检查。合入 main 前,六个顶层与 23 个嵌套用例在普通与 race 模式下通过。四节点十六盘集群上并发覆盖写下的 20,000 次稳态成功 LIST 没有漏项。

仍然开放的部分

同一集群上,四个节点滚动重启叠加并发覆盖写,27,966 次成功 LIST 中有 2,624 次少了一到四个 key。这些 key 是可读的:8,192 组同端点 GET/HEAD 核查中 8,186 组返回 200,内容是最后确认的写入。最强的样本开始于最后一个节点健康检查返回后 17.6 s,漏掉的 key 最后一次成功 PUT 在 11.5 s 前已确认,之后没有新写入。原始 XML、HTTP 关联与正文哈希经独立复核,分页与解析解释不了它。

修复后的构建仍有把"无法决策"当作"不存在"而不让请求失败的出口:没有任何一代达到仲裁的合并;有效条目少于仲裁或没有缓存元数据的解析器;丢弃该条目、只在失效 reader 多于仲裁允许时才让列举失败的 partial 回调;以及元数据读取意外失败时跳过该条目的 walker。列举仲裁按被询问的盘数计算,不随中途失效的 reader 调整。样本命中了哪一个出口,从 HTTP 证据看不出来;失败请求的逐盘流、有效仲裁、补票资格与缓存状态没有采集,运行后恢复的元数据也不是失败时的输入。

下一步是对一次失败 LIST 做有界的诊断采集,再据此构造确定性回归,然后才裁决契约是否改成让请求失败或返回三态结果。在此之前,滚动重启期间不要运行会删除源端列表中缺失对象的同步工具,集群稳定后重新列举。