RustFS Scanner 数据用量发布权威性决策:配额准入如何获得可用的权威依据
【免费下载链接】rustfs🚀2.3x faster than MinIO for 4KB object payloads. RustFS is an open-source, S3-compatible high-performance object storage system supporting migration and coexistence with other S3-compatible platforms such as MinIO and Ceph.项目地址: https://gitcode.com/GitHub_Trending/rus/rustfs
本决策文档(ADR)记录了 RustFS 在2026-09-03做出的一项关键架构结论:Scanner 后台扫描产生的数据用量(data usage)是硬配额(hard quota)准入的唯一权威数据源,为此必须保留完整的发布协议与证明层(fence),而不是在扫描滞后时用 best-effort 数据将就。阅读本文后,你将理解配额写路径为何需要"可用性故事"、七大围栏各自的排除对象、八个持久化对象各自的属主与生命周期,以及升级窗口期内 quota 如何降级到用量底线(usage floor)并最终收敛回完整发布。
决策概述:保留 Scanner 发布协议作为配额权威
scanner-usage-authority-decision.md给出的是结论性决策记录,其完整论证与协议细节落在配套契约文档 scanner-usage-publication.md 中,两者共同构成该主题的"决策 + 契约"体系:
RustFS keeps scanner data usage as authoritative for quota admission.
该决策从 backlog 决策记录中选定了选项 A:在配额准入消费 scanner 用量期间,scanner 发布协议保持必要。决策明确把以下机制从"可删除的兼容性杂物"重新定性为协议不变式(protocol invariants):
- 周期纪元(cycle epoch);
- 发布 CAS(publication CAS);
- 数据迁移围栏(data-movement fence);
- 分层注册表围栏(tier-registry fence);
- 观察快照层(observed snapshot layer);
- 持久化用量底线(persisted usage floor)。
换句话说,这些证明层不是历史包袱,而是"让分布式后台扫描的结果可以被配额决策信任"的机制本身。删除任何一个,都需要先证明有替代机制能排除同样的陈旧输入。
为什么配额准入不能接受 best-effort 用量
决策理由(Rationale)直指问题本质:配额是写路径上的准入决策(write-path admission decision)。
如果配额以 best-effort 的 scanner 数据为准,那么扫描临时滞后(scanner lag)就会直接转化为欠强制(under-enforcement)——扫描还没看到的新写入不会被计入用量,配额形同虚设。因此设计上不能靠删掉证明层来简化,反而需要一个权威用量的可用性故事(availability story)。
可用性故事的主角是scanner usage floor(用量底线):当完整的权威快照不可用时(冷启动、升级恢复、不完整周期的修复期间),配额准入使用这个下界值。观察快照(observed snapshot)仍然服务于管理和可观测性,但永远不成为配额权威。
这一判断与 QuotaChecker 的实现 完全一致:get_real_time_usage的查找顺序是——内存权威用量 → 持久化用量底线 → 失败关闭,后两级正是决策文档中"可用性故事"的落地。
发布所有权模型:三个身份缺一不可
一个 scanner 周期只有在同时满足两个条件后才拥有权威发布的资格:持有集群 scanner 领导权声明(leadership claim),并证明存储发布纪元(storage publication epoch)没有移动。
- 领导权声明持久化在 scanner 周期状态(cycle state)中,源码对应 leadership.rs 中的 reconcile_scanner_leadership_claim,它读取并对比
.bloomcycle.bin的持久化修订; - 数据用量发布的准入权归ECStore所有,因为只有它知道 rebalance、decommission 等数据迁移操作是否改变了 scanner 结果允许描述的 generation。
Scanner 可以无发布所有权地计算用量,但绝不能把结果变成权威的、配额可见的状态。一次完整发布因此具有三个身份(identity):
- 拥有该周期的scanner leader epoch;
- 围栏数据迁移的storage publication epoch;
- 被替换用量对象上的per-object CAS revision。
任何一个身份在提交前发生变化,结果就只能作为重试或观察候选,而不是权威基线。
PUT 扇出(fanout)的在途保护
普通 PUT rename 扇出同样追踪实例级在途工作(in-flight work):quorum ACK 并不释放它——真正的磁盘任务保留所有权直到 rename 结束,即使请求调用方已被取消。关键设计取舍是:
- 扫描准入保持仅限迁移(movement-only),持续的 PUT 不会停止命名空间遍历和 scanner 驱动的生命周期发现;
- 遍历后的本地发布检查和**远程发布租约(publication lease)**拒绝尚未结束的扇出;
- begin/end 命名空间 generation使跨扇出的扫描与缓存计划失效;协调者在获取远程租约后、发布权威聚合前,会重查完整活动摘要(activity digest),以捕获"扫描最后一次探测与租约获取之间才结束的尾部"。
该机制不增加命名空间或迁移锁。已通过验证的旧快照仍可能先于新开始的写入;持续或停滞的 PUT 尾部会延迟权威用量发布,延迟通过现有重试调度恢复,而非新增即时唤醒协议。这个 PUT 尾部保护要求所有 writer 节点完成升级,且不证明失败尾部副本已自愈,也不把在途追踪扩展到 multipart 等其它命名空间变更路径。
围栏体系:七道独立证明
协议使用独立的围栏(fence),因为它们排除的是不同的陈旧输入。除非替代方案能证明同样的排除效果,否则不得合并:
| 围栏 | 属主 | 排除对象 |
|---|---|---|
| Scanner 领导权声明 | scanner | 竞争中的 scanner leader 与陈旧周期写入者 |
| 存储发布纪元 | ECStore | 跨 rebalance、decommission 或其它数据迁移 generation 计算出的用量 |
| 发布租约 | 经 ECStore 面活动探针的 scanner 对端 | 尚未确认候选的远程脏用量或维护状态 |
| CAS 修订 | 底层配置对象存储 | 对用量快照、scanner 缓存或周期状态对象的丢失更新 |
| Per-set 新鲜度 | scanner 聚合 | 混入陈旧与当前 set 结果的合并用量快照 |
| 分层注册表 generation | scanner 分层核算 | 依据不同暖分层注册表分类的字节数 |
| 用量底线身份 | scanner 发布与 ECStore 配额回退 | 空值或遗留值变成看似可信的权威配额输入 |
无法为自身读取面证明所需围栏的读者必须**失败关闭(fail closed)**或走文档化的观察路径,绝不允许为缺失或损坏的权威对象合成空用量快照。
缓存执行身份:防止慢扫描覆盖新结果
结构扫描计划摘要(structural scan-plan digest)在普通 bucket 写入下可以保持稳定,因此有范围扫描可以保留未受影响的基线 bucket。但它不足以证明同一周期内可复用已完成的结果。Bucket 工作使用结合了结构计划与完整活动快照的执行摘要(execution digest),bucket 的脏 generation 纳入其缓存身份;已完成的 set 缓存与结构计划分开携带同一执行摘要。持久化 set-root 快速路径要求执行身份相等,并叠加既有的 source、cycle、leader、tier 与缓存结构检查。
Set 扫描还会捕获其起始缓存修订(starting cache revisions):当持久化执行结果不同、需要替换时,这些修订必须保持不变,否则慢扫描可能覆盖更新的已完成结果。既有缓存锁、条件保存与迁移准入仍然围栏提交。
实现层面,可选字段scan_execution_digest追加到 map 编码的缓存元数据中。遗留缓存仍可读,但缺少该身份就无法满足同周期 set-root 复用;旧版读者可以忽略新增的 map 键,但旧版写入者不执行该围栏——可读性不构成混合版本发布安全的保证。
持久化对象契约:每个对象都有唯一属主
持久化对象是兼容性契约的一部分,移除任何一个都需要兼容窗口与专门的清理条目(cleanup entry)。下表对象路径均位于元数据桶的 bucket-metadata 前缀下:
| 对象 | 属主 | 生命周期 |
|---|---|---|
.usage-cache.bin(每个 bucket/set 下) | scanner 磁盘遍历 | 由 scanner 依据对象元数据重建;数据缺失触发该 bucket/set 重扫;数据损坏不构成完整基线 |
.bloomcycle.bin | scanner 周期状态 | 由 leader CAS 更新;状态缺失从未初始化周期开始;损坏或未来状态在自动重试前先被隔离 |
.usage.v2.json与.usage.json | scanner 权威发布 | .usage.v2.json是主要完整用量快照;.usage.json仅在携带有效持久化身份时作为遗留或伴随基线读取。两者都不能绕过 v2 纪元围栏;仅当基线身份与完成字段通过校验时,读者才可将其视为权威 |
.usage.observed.json | scanner 观察路径 | 在权威发布无法被证明但诊断快照仍有用时写入;永不是硬配额权威 |
bucket-metadata/.usage.json | scanner 用量底线 | 携带降级权威用量窗口内配额使用的持久化 per-bucket 底线;在下一次完整 scanner 发布前保持静态 |
.bloomcycle.bin.recovery-required.json | scanner 周期恢复 | 隔离无效周期状态并携带重试证据;仅 scanner 恢复代码可更新或清除 |
.scanner-cycle.lock | scanner 运行时锁 | 序列化周期级工作;锁对象缺失本身不是用量证据 |
.scanner-pause-backlog.json | scanner 暂停与追赶账本 | 在权威发布被数据迁移围栏期间追踪脏用量、发现的周期工作与全量扫描追赶;绝不授予发布准入 |
对象路径常量在源码中有明确出处,例如 data_usage_define.rs 中的路径定义 与 data_usage.rs 中的对象名常量:DATA_USAGE_OBJECT_NAME = ".usage.v2.json"、DATA_USAGE_OBSERVED_OBJECT_NAME = ".usage.observed.json"、LEGACY_DATA_USAGE_OBJECT_NAME = ".usage.json",恢复标记则派生为.recovery-pending.json/.recovery-required.json后缀。
观察快照与用量底线:降级路径的两层
观察快照:诊断与可用性层
观察快照仅在快照明确声明自身为部分或观察性质时才能被提供,且只提供给不会据此做出硬配额、持久性或删除决策的消费者。管理用量视图可以携带完整性标记(completeness flags)暴露该状态,让运维在权威发布被阻塞时仍能看到进度。配额强制不得将观察快照当作当前用量权威。
用量底线:静态下界而非新鲜计数
权威快照不可用时,配额准入可使用持久化用量底线。这是一个可用性回退,而非新鲜计数:
- 实时写入不会推进底线;
- 超限仅受"下一次完整 scanner 发布前已接受的写入"约束;
- 若连有效持久化底线都没有,配额保持不可用并失败关闭。
可用性决策契约:四条款
决策文档给出的可用性契约精确为四条:
- 权威快速路径读取完整的内存或持久化 scanner 用量;
- 升级或发布中断期间,配额可依据持久化 per-bucket 用量底线准入;
- 底线在该中断窗口内是建议性的,必须收敛回完整 scanner 发布;
- 既无权威用量、也无有效底线的 bucket失败关闭。
把该决策改为软配额(soft quota)模型属于产品变更而非 scanner 重构,需要分阶段移除权威特定层,并为上述持久化对象做兼容性处理。
删除与恢复规则:属主隔离
只有对象的属主才能删除或隔离它:
- scanner 可在扫描证明替换内容后重建 per-set
.usage-cache.bin; - scanner 周期恢复可隔离无效的
.bloomcycle.bin,且仅在有效周期状态持久化后才能清除标记; - scanner 发布只能通过上述发布围栏替换
.usage.v2.json或遗留伴随对象; - 配额消费者可读取用量底线,但不得删除或修复它;
- 运维只能通过受支持的 scanner 重置面重置用量状态——该面会记录重置路径并强制全量重建。
缺失、不可解码或无身份的数据不会被转换为零,而是按读者所属面分别报告为 uninitialized、recovery-required、observed-only 或 unavailable。源码中 cycle_state.rs 的恢复标记与重试预算 给出了实现细节:MAX_SCANNER_CYCLE_RECOVERY_RETRIES = 5,恢复状态机(healthy / blocked / paused / recovery-required / cleanup-pending / usage_floor_load_failed 等)由 gauge 指标rustfs_scanner_cycle_recovery_required对外暴露;此外还有幂等的恢复意图(recovery intent)机制,.usage.v2.recovery-intents/前缀下的意图记录支持scanner-usage-full-rebuild动作与 full-rebuild 模式,供运维在确认重置意图后强制重建。
源码佐证:从 QuotaChecker 到发布管线的实现细节
配额检查的三级查找与回归测试
QuotaChecker::get_real_time_usage 是决策"可用性故事"的直接实现:
- 先查内存权威用量
get_bucket_usage_memory,命中即返回(权威快速路径); - 降级窗口(注释明确指向 issue #5716)读取持久化 per-bucket 底线
lookup_degraded_bucket_usage_baseline——该窗口最典型的触发场景是从 pre-v2 版本升级:遗留.usage.json缺少完整性标记被降级为非权威,而 scanner 首个完整周期(写出.usage.v2.json)可能还很遥远,若此时失败关闭,配额桶的每次写入都会在整个窗口内变成可重试 503; - 两级都无结果才返回
QuotaError::UsageUnavailable失败关闭。
仓库测试 checker.rs 中的回归测试 精确验证了这一契约:quota_admission_falls_back_to_legacy_snapshot_baseline构造遗留快照夹具验证回退基线值 1_234、验证未知 bucket 失败关闭、验证删除后端用量后重新创建的 bucket 不会继承旧 incarnation 的大小;quota_usage_rejects_an_unknown_mutation_baseline则验证无权威基线时必须失败关闭。
CAS 发布管线的证明与结果枚举
usage_store.rs 的发布管线 展示了围栏如何落到代码:发布保存走scanner_data_usage_publication_commit_scope_with_release_flag提交作用域,根 ACK 确认要求提交状态为 Committed 且写入 ETag 非空(root_ack_write_is_confirmed);CAS 冲突触发有限重试(PreconditionFailed分支),并据最终结果产出DataUsagePersistOutcome枚举:NoUpdate / Current / AlreadyDurable / PriorCycleDurable / Saved / Deferred(原因) / Failed——其中Deferred(ScannerCycleDeferReason::DataMovement)与PublicationLeaseDeadlineExceeded正是"发布围栏把结果打回重试/观察候选"的代码形态。观察快照写入.usage.observed.json目标路径,且必须先确认权威基线身份存在(usage_snapshot_authoritative_baseline)才允许落盘,呼应"观察层永不作配额权威"。
暂停积压账本:不授予准入的独立记账
backlog.rs 的模块注释明确声明:该账本复制在权威发布路径之外,因此在发布被数据迁移围栏期间仍可推进,但从不授予发布准入。它提供Idle / Paused / CatchingUp / RetryExhausted四个阶段与一组阈值常量:24 小时暂停时长告警、3 个被延迟周期告警、10,000 条积压条目告警、5 分钟最小追赶间隔、1 小时追赶窗口、每窗口最多 4 次尝试、5 次连续失败上限。
既有修复即不变式
契约文档明确指出,若干历史 scanner 修复是这份契约的推论而非独立补丁:
- 不完整的 scanner 用量不得成为完整的 admin 或配额基线——因为完整性与底线身份属于发布所有权的一部分;
- 脏用量与维护确认必须围栏发布——因为携带未确认工作的远程节点会使候选失效;
- 遗留或备份用量对象仅在携带有效基线身份、且不越过主纪元围栏时,才可帮助恢复可用性。
未来变更的影响与边界
决策文档为后续工作划定了两条硬边界:
- #2214是未来用量发布变更的硬性设计输入。未来的提案仍可选择软配额与 MinIO 式 best-effort 用量,但那将是产品变更,必须为持久化工件自带分阶段兼容计划;
- #2219可在当前权威协议的基础上设计 scanner 存储边界(storage boundary)。其接口必须包含契约所需的 CAS 键值、周期锁、用量底线、观察快照与恢复标记能力,不得把这些证明义务隐藏在一个通用对象存储 trait 之后。
对正在演进 RustFS 或设计同类分布式后台扫描 + 配额系统的工程师,这份决策记录的实际价值在于一个反直觉的结论:当后台扫描的结果要支撑写路径准入决策时,保留"多余"的证明层恰恰是最短路径——删除它们并不能让系统更简单,只会把扫描滞后直接转化为配额欠强制。
【免费下载链接】rustfs🚀2.3x faster than MinIO for 4KB object payloads. RustFS is an open-source, S3-compatible high-performance object storage system supporting migration and coexistence with other S3-compatible platforms such as MinIO and Ceph.项目地址: https://gitcode.com/GitHub_Trending/rus/rustfs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考