docs: record access tiering introduction and retirement decisions

Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
Feng Ruohang
2026-09-15 11:26:45 +08:00
parent 13bf126ebd
commit 4093fa0d78
3 changed files with 128 additions and 4 deletions
+8 -2
View File
@@ -24,9 +24,13 @@ and [complete commit range](https://github.com/pgsty/silo/compare/RELEASE.2026-0
- Reconcile ordinary single-object version DELETE across all pools, including
null versions, delete markers and unqualified directory-marker DELETE. This
prevents movement leftovers from surviving a successful response. Unreadable
applies the deletion to every resolved pool copy under existing quorum
rules. Pending outbound delete replication retains versions until the
existing replication worker completes their purge; a successful response
does not imply immediate physical removal from every drive. Unreadable
pools now consistently return 503 instead of depending on pool traversal
order; this extends the existing failure surface. Retry after recovery.
order; insufficient read quorum returns `SlowDownRead`. This extends the
existing failure surface. Retry after recovery.
Cleanup failures also return an error. Batch deletion already fans out across
pools; replication and scanner cleanup keep their existing contracts. See
[scope and limitations](docs/bucket/lifecycle/access-tiering-removal.md#version-deletion-scope).
@@ -35,6 +39,8 @@ and [complete commit range](https://github.com/pgsty/silo/compare/RELEASE.2026-0
its tracker, mover, scanner hooks, configuration, XML actions and metrics.
Accept and ignore retired configuration/XML and preserve ordinary statistics
when reading v9 caches. See [migration notes](docs/bucket/lifecycle/access-tiering-removal.md).
The [decision record](docs/investigations/access-tiering-revert.md) preserves
the feature's introduction, subsequent fixes, rollback scope and review history.
- Preserve the independent multi-pool write, metadata, healing and conditional
deletion fixes from PR #178, including shared remote-tier reference protection.
- Enforce `If-Match` on DELETE, preserve retention and independently ordered
@@ -2,6 +2,8 @@
PR #60 introduced an opt-in scheduler that moved objects between local server pools according to GET frequency. It and its feature-specific fixes have been removed. This does not remove ordinary lifecycle expiration, transitions to remote tiers, rebalance, decommission, or the general multi-pool correctness fixes from PR #178.
The [introduction and rollback record](../../investigations/access-tiering-revert.md) documents the commit history, scope decision, review corrections and unresolved validation findings.
The published Server 20260903 predates this feature. These instructions concern main/snapshot deployments that included #60; upgrading from the published version does not require access-tier configuration cleanup.
## Before upgrading a build with access tiering
@@ -27,8 +29,8 @@ Interrupted rebalance/decommission can leave the same version in more than one p
## Version deletion scope
Ordinary single-object `DELETE ?versionId=...` reconciles the addressed UUID, null version or delete marker across pools. Unqualified DELETE of a directory marker (a key ending in `/`) also addresses its null version and uses this path. A successful response means the addressed copies were removed; other versions remain.
Ordinary single-object `DELETE ?versionId=...` reconciles the addressed UUID, null version or delete marker across pools. Unqualified DELETE of a directory marker (a key ending in `/`) also addresses its null version and uses this path. A successful request applies the deletion to every resolved pool copy under the existing per-pool quorum rules; other version IDs remain. If outbound delete replication is pending, copies retain `VersionPurgePending` until the existing replication worker completes the purge. Success does not guarantee immediate physical removal from every drive.
If a pool is unreadable, these requests can return 503 even when another pool has a readable copy. This extends an existing failure surface: previously the result could depend on whether the unreadable pool preceded the successful pool in traversal order; it now fails consistently. Retry after recovery. Cleanup failures also return an error. Ordinary unqualified DELETE retains its existing semantics. Batch `DeleteObjects` already fans out across pools.
If a pool is unreadable, these requests can return 503 even when another pool has a readable copy. Insufficient read quorum returns `503 SlowDownRead`; other failures retain their corresponding error codes. This extends an existing failure surface: previously the result could depend on whether the unreadable pool preceded the successful pool in traversal order; it now fails consistently. Retry after recovery. Cleanup failures also return an error. Ordinary unqualified DELETE retains its existing semantics. Batch `DeleteObjects` already fans out across pools.
Incoming replicated deletes, lifecycle expiration, free-version cleanup and movement-internal calls retain their existing contracts. In particular, an incoming replicated version delete can leave movement duplicates in other pools; this change does not solve that separate case. Expiration scanners process their own pools and may remove duplicate expired copies in later cycles; free-version cleanup remains local to a pool. Do not treat the ordinary DELETE repair as a guarantee for every source of deletion.
@@ -0,0 +1,116 @@
# 访问频率分层:引入、修复与回退记录
记录日期:2026-09-15。本记录说明 PR #60 的设计、合入后的取舍,以及此次回退为什么同时保留并补齐通用多池正确性修复。操作步骤见[退役迁移说明](../bucket/lifecycle/access-tiering-removal.md)。合并、正式发布和生产部署是不同状态;本记录随回退变更交付,不代表已经发布。
初稿审查时(2026-09-15 03:20 UTC),最终候选尚未移植到远端基线、尚未冻结 PR head,合并前全量检查与三次有效 Linux 运行尚未执行,本变更尚未合并。后续执行状态以承载本记录的 PR 及其绑定提交的验收记录为准;下文的历史实验不替代这些检查。
## 1. 引入的目标和实际范围
[@mrjavadseydi](https://github.com/mrjavadseydi) 在 [PR #60](https://github.com/pgsty/silo/pull/60) 提出了基于 GET 频率的本地池间分层。成功 GET 更新有界滚动计数,后台调度器把热对象提升到配置中的首个池,把已经迁移且变冷的对象降到末个池。功能默认关闭,至少需要两个池;它与普通生命周期过期、远端对象存储 transition、rebalance 和 decommission 是不同机制。
实现不只是一个后台任务:它增加了 GET 计数入口、leader 调度、跨池版本栈复制、来源清理及失败恢复、热池配额、生命周期 XML 扩展、配置项、指标和扫描统计。访问热度统计使 data-usage cache 从 v8 升到 v9。对象本体的存储格式没有因此改变。
搬移要保持完整版本历史、删除标记、null version、时间戳、ETag、校验和和加密元数据;同时需要处理并发写入、目的端已有版本、来源部分删除和远端 tier 引用。合入前的修补和测试针对并覆盖了这些边界,不应把回退解释成贡献无效。贡献者署名继续保留,独立的其他贡献也不回退。
## 2. 可追溯时间线
下表的 PR/Issue 时间使用 UTC;提交链接对应具体代码,不把“报告时间”当作“缺陷首次出现时间”。
| 时间 | 事件 | 本次处理 |
| --- | --- | --- |
| 2026-08-15 10:15 | #60 创建;原始实现 [`7a060cab1`](https://github.com/pgsty/silo/commit/7a060cab1edd5bbc17da7f703bbd1ab7415b6f7c) | 随特性撤销 |
| 2026-09-06 00:09 | [#133](https://github.com/pgsty/silo/issues/133) 报告多池副本写不能权威调和 Object Lock 状态 | 保留解决它的通用修复 |
| 2026-09-06 15:12 | [#144](https://github.com/pgsty/silo/issues/144) 报告条件 DELETE 原子性仅限单个纠删码集合 | 保留解决它的通用修复 |
| 2026-09-08 05:18 | [`9a6e1477f`](https://github.com/pgsty/silo/commit/9a6e1477f45067559def8423d431ee177795134f) 补访问分层兼容标识清单 | 删除功能专属标识,保留有依据的退役兼容 |
| 2026-09-08 07:08 | [`374de0fa3`](https://github.com/pgsty/silo/commit/374de0fa32aa1d6eda57dbba6ac522ebf793b6be) 修复搬移的版本保全、写隔离和删除范围 | 随专属搬移器撤销 |
| 2026-09-08 07:28 | [`5ac33e158`](https://github.com/pgsty/silo/commit/5ac33e1583e838ade3f56c7d60e80e7a854f9a88) 确定性覆盖搬移失败恢复 | 随已删除搬移器的专属测试撤销 |
| 2026-09-08 07:42 | #60 以 [`a3df317ae`](https://github.com/pgsty/silo/commit/a3df317ae0725eb650e4d3e21551154f69be6229) 合入 | 以该 merge 的第一父差异确定功能边界 |
| 2026-09-11 12:34 | [#178](https://github.com/pgsty/silo/pull/178) 合入通用多池写入、元数据与条件删除调和 | 保留,解除测试对访问搬移器的依赖 |
| 2026-09-13 | [`2dd1e00da`](https://github.com/pgsty/silo/commit/2dd1e00da49faf995f2db29807fc6211b8376d7d) 的 CHANGELOG 同时汇总访问分层和通用多池修复 | 拆开表述,不整条删除独立修复历史 |
| 2026-09-15 | 维护者决定收缩访问分层;完成来源分析、三种候选反证、退役兼容、普通版本 DELETE 修补与外部评审 | 形成此次选择性回退 |
#178 中的 [`e59a3d938`](https://github.com/pgsty/silo/commit/e59a3d938ed25c1bcd51efbb4ad6955073d195f7) 提供池级串行化、字段调和和条件删除;[`ccb676e60`](https://github.com/pgsty/silo/commit/ccb676e60cb7441ee65ff7c35f3b7828979101fd) 保护仍被其他副本使用的远端 tier 引用;[`51d41345f`](https://github.com/pgsty/silo/commit/51d41345f7ac532f9c6fea2b7dba5f29da930b8f) 保存 Linux 重启及 OIDC 验收记录。这三项不属于仅为访问频率调度而存在的代码。
截至回退评估时,公开 Server `RELEASE.2026-09-03T13-18-01Z` 早于 #60 合入。退役迁移主要针对运行过后续 main、自行构建或快照版本的实例;不能据此声称正式 release 用户普遍启用过此特性。
## 3. 为什么回退,为什么不能整批撤销后续修复
维护者的取舍是:默认关闭的可选调度能力,对配置、生命周期、缓存、统计和核心多池写入路径带来了过大的维护面。此次移除的是该能力及专属实现,没有测量并宣称吞吐量提升、延迟降低或固定减少一次分布式锁往返。
“后来改过同一文件”不等于“由 #60 引发”。#133/#144 的报告早于 #60 合入;更关键的是,原有 rebalance、decommission 和复制写入也会使同一版本暂时存在于多个池。访问分层消失后,这些状态仍然合法存在。移除调和与锁纪律会重新允许旧副本遮蔽新元数据、条件删除选错版本、清理错误被吞掉等问题。
评估在隔离工作树中实际比较了三条路线:
| 候选 | 通用多池回归 | 普通指定版本 DELETE |
| --- | --- | --- |
| A:撤销 #60,保留 #178 | 原有 13 组通过 | 仍能成功返回后留下可读副本 |
| B:同时撤销 #60#178 的存储修补 | 相同 13 组中 10 组失败 | 问题仍在 |
| CA 加普通版本 DELETE 调和 | 13 组原有及当时新增的 8 组通过 | 同一复现通过 |
这些是 2026-09-15 的历史对照结果,不是最终 PR head 的发布验收。后续补上目录标记、真实 rebalance 中断等覆盖后,通用多池测试达到 23 组。测试通过不能替代来源分析,来源分析也不能替代最终候选的运行验证。
## 4. 最终保留与删除的边界
- 删除访问 tracker、调度/搬移器、GET 和 scanner 钩子、热池配额、专属配置帮助、生命周期动作、指标及专属测试。
- 保留普通过期、远端 transition、rebalance/decommission、复制写入,以及 #178 的 Object Lock、标签、条件删除、元数据调和与远端引用保护。
- 原有 data-usage 生成解码器恢复到 #60 前的实现;允许读取 v8/v9,利用字段编码跳过已退役热度字段,继续写 v8。普通字段保真由历史真实 v9 样本测试覆盖。
- 仅容忍准确的十个退役 ILM 键;读取生命周期时丢弃退役扩展。纯访问动作的规则需先清理才能再次编辑;混合规则保留普通动作。
- 已经搬移的对象留在当前池;没有全量搬回、自动删除所有重复版本、后台清理服务或对象元数据重写。
曾对本地审查提交 `d06f1c614` 做过声明来源复核:消失的 357 个声明均不在 #60 之前,删除的十个文件均由 #60 引入;#60 原先删换的 41 行旧文本按忽略空白比较有 40 行恢复,剩下一行保留 #178 在已持有池锁时调用 `getWritePoolIdx(..., true)` 的修正,避免对同一对象再次取锁。生成缓存解码器与功能前逐字节一致,原有 13 组通用测试没有删除。这是该审查版本的保全证据,不能把数字脱离 SHA 当作未来所有版本的保证。
## 5. 普通版本 DELETE 是独立补洞
功能移除不会自动消除历史重复副本。普通单对象指定版本 DELETE 因而复用既有调和路径:在池级对象锁内读取每个池的目标版本,计算一次条件及回调,先删除非权威副本,再处理权威副本;任何不可读池或清理错误都不能当作成功。
范围包括 UUID、null version、delete marker,以及原先就被解析为 null version 的未指定版本目录标记 DELETE。入站复制、搬移内部调用、生命周期过期和 free-version 清理保留各自语义;批量 `DeleteObjects` 原本就会向池并发扇出,不是此次遗漏。
删除标记需要向 retention/metadata 回调传入与 set 层相同的 `MethodNotAllowed``ObjectNotFound` 语义。直接复用拒绝 marker 的元数据更新入口会错误地拒绝合法版本删除。回调从所有副本合并独立更新的 Object Lock 和标签,不能随意只采用一个池的状态。
存在两项明确的成功/失败边界:
1. 读法定多数不足时返回 `503 SlowDownRead`,即使另一个池有可读副本。旧路径的结果会受池遍历顺序影响;新路径把失败语义统一。它是正确性与可用性的取舍,需要恢复后重试。
2. 出站删除复制尚未完成时,成功响应可以表示各副本进入 `VersionPurgePending`,由既有 worker 完成清理。原有每池 quorum 规则也继续适用;不能把成功响应等同于每一块盘立即物理删除。
## 6. 审查如何改变了方案
本地先形成三个线性审查提交:`8fdfdabd9` 移除特性,`d06f1c614` 补普通 DELETE`6e3fdca97` 去除重复扫描。它们记录审查演进,最终 PR 在独立远端基线上重放,提交 ID 会改变;不应把线性演进误认为三份同时维护的实现。
Claude Code Opus 5 / max 的五轮实现评审要求补齐退役兼容、说明协调停机及环境一致性、验证真实池故障,并纠正运行证据措辞。随后 Claude 与 ZCode 的独立复核再次确认了回退边界和普通 DELETE 语义;最终两轮计划商榷收束了交付流程。
| 意见 | 裁定与处理 |
| --- | --- |
| 指定版本 DELETE 重复扫描所有池 | 接受。第一次扫描已在同一锁内得到目标副本,直接合并其结果。16 盘池在回调前的读取计数从 32 降为 16;这不等于总 I/O 或延迟减半。 |
| N 个副本产生 N 条 DELETE 审计 | 反驳。底层调用只追加上下文标签,HTTP 层向每个配置审计目标发送一次请求事件。成功完成调和时池标签最终指向 primary;不增加额外 NoAuditLog 修改。 |
| retention/metadata 顺序与 set 层不同 | 差异存在,但已有明确注释。保留 retention 优先,拒绝后不删除、不调度删除复制、不 Sweep;不宣称任意自定义回调都能交换。 |
| 把优化 amend 进旧提交并直接丢弃 main 脏修改 | 不 amend 已审历史。先保存完整文件、补丁、哈希及可达恢复引用,核对覆盖后受控恢复。八组新增通用测试承接,访问搬移专用测试由真实 rebalance 中断覆盖替代。 |
| 从当前本地分支直接开 PR | 调整。其祖先含另一个任务的 IAM/超时提交 `ebc9937d9`;从远端 `89637554d` 仅移植本次变更,不夹带或删除独立工作。 |
| 合并之后再跑全量与 Linux 验收 | 不接受。先完成文档和移植,冻结 PR head,再验收;不能把旧 SHA 的测试直接提升为新基线的通过记录。 |
| 不同基线 diff 必须逐字节一致 | 改为每提交 stable patch-id、路径与完整树等价性核验。blob hash 和行号随基线变化,不应成为错误的拒绝依据。 |
`objectPoolInfos` 的并行查询作为独立性能跟进,本次不增加并发实现。Contributor 署名、#132 配额指标、#77 桶元数据、federated COPY、IAM、超时及其他独立修复不因本次取舍被整体撤销。
## 7. 历史运行证据与未定案事项
以下是移植前 `d06f1c614` 的历史实验,不是最终 PR head 的验收替身。
| 运行 | 实际观察 | 不应推导的结论 |
| --- | --- | --- |
| `f590538f`,四节点双池 | 旧版真实 rebalance 中断留下 9 个重复 UUID;协调停机换新后对象四节点可读,旧配置与普通规则可编辑,真实缓存头 v9→v8;删除一个 addressed UUID 后四节点 HEAD/GET 404,另一个版本仍可读 | 没有验证全部 9 个不同 UUID 收敛;物理元数据仅每池抽查一盘;没有整份运行时统计守恒测量 |
| `fd93cd37`,三节点双池 | 停掉一个池所在节点,保留 namespace 锁的 2/3 法定多数;DELETE 返回 503 SlowDownRead,源全部四盘保留目标版本;恢复后 DELETE204,各节点 HEAD/GET404 | 不能推广为跨主机网络、持久盘及压力验收 |
| 被弃用的四节点停池拓扑 | 同时丢失 namespace 锁法定多数,发生客户端超时,记录脚本还遇到 NoneType 错误 | 不是“池读取返回503”的证明 |
| `83676ca2` | 升级后配置/生命周期编辑之后一次 HeadObject 返回503artifact 没有记录 DELETE 自身响应 | “DELETE之后”仅来自脚本顺序,不能写成已证实 DELETE204 后异常,也不能归类为已修复、既有问题或暂态 |
**开放项:`83676ca2` 的 HEAD503 仍未定案。** 可直接比较的目标阶段历史运行是一失败、一成功;一次未复现不足以关闭问题。仅凭 `SlowDownWrite` 等错误名字也不能给其他失败确定容量或环境根因。
最终候选的合并前复核采用有界规则:要求三次有效升级后 DELETE/HEAD 运行,最多五次总尝试;每次记录 DELETE 码/耗时、失败 HEAD 的节点与版本、GET 错误码、两池全盘元数据、固定间隔重试时序和旧版同拓扑对照。旧版可能保留副本返回200,不要求它满足新增跨池删除契约。
有证据证明在目标操作前失败的 harness 尝试才能不计入有效运行,但仍计入总尝试。任何目标阶段的新503或数据不变量失败都不能通过补跑抹掉,必须暂停合并并定位。三次通过也只满足这项工程检查,不证明历史异常已消失;开放项在合并和正式发布评估时仍须可见。
## 8. 交付与恢复纪律
最终候选使用独立分支;main 的五个旧修改先保存完整内容、二进制补丁、SHA-256 和具名 Git 恢复引用,再核对原 HEAD/哈希及测试覆盖,只恢复这五个文件。禁止用整树 reset 或 clean 代替受控归一;若用户已有新增编辑,应保留并重新核对。
全量 cmd/internal、相关 race、构建、vet、lint、生成文件和兼容检查,以及上述 Linux 验收,绑定最终候选的实际 SHA。若纳入新的远端提交,重新记录基线与 head,复核变更并重跑受影响验收。文档与原始测试记录各自保留其对应版本,不篡改旧失败、不用新通过覆盖旧记录。
逐节点滚动升级未通过既有二进制校验检查,采用[协调停机方案](../bucket/lifecycle/access-tiering-removal.md#before-upgrading-a-build-with-access-tiering)。实验使用单个 Docker Linux VM 和 tmpfs;正式 tag、包、镜像、跨主机及生产部署仍是独立交付。未验证全部重复 UUID 或运行时统计守恒应如实披露,不能反向引入自动搬回/清理需求或无关重构。