RustFS 存储层架构解析:Storage API 契约、集群控制面与后台控制器的职责边界
【免费下载链接】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
导读:本文以 docs/architecture/storage-control-data-plane.md 为主线,系统梳理 RustFS 中"存储 API 契约层—集群控制面—后台控制器"三层的职责划分与防漂移约束。你将从源码与测试两个层面理解:为什么契约层必须与 ECStore 实现细节解耦、ClusterControlPlane只读门面如何从端点池投影出拓扑/成员/池状态/对端健康等快照、以及 scanner、heal、lifecycle 等后台服务为何不能折叠进一个泛型控制器。读完本文,你将具备判断"某个新存储 API 面、集群读模型或后台状态面该由哪一层负责"的架构决策能力。
一、文档定位:什么时候该读这份架构说明
这篇架构文档是一份边界所有权与防漂移基线,而非操作手册。它的适用场景非常明确:
- 你要新增一个 storage API 表面(storage API surface);
- 你要新增一个集群读模型(cluster read model);
- 你要新增一个后台服务的状态/协调表面(background-service status/reconcile surface)。
在这些场景下,你需要先回答一个问题:"这个表面该由哪一层拥有?哪些行为绝不能漂移?"
文档给出了三层权威来源(Source of truth):
| 层 | 权威位置 | 职责 |
|---|---|---|
| Storage API 契约 | crates/storage-api(trait 契约) | 定义对象、桶、拓扑、能力快照等对外契约类型 |
| ECStore 门面分组 | crates/ecstore/src/api/mod.rs(facade groups,api::cluster) | 把 ECStore 内部实现以显式门面暴露给外层兼容边界 |
| 集群控制面 | crates/ecstore/src/cluster | 承载只读的ClusterControlPlane与各类快照 |
| 控制器词汇 | background-controller-contract.md | 定义 Desired/Current/Status/Reconcile 等统一词汇 |
二、Storage API 契约层:只定义契约,不吸收实现细节
契约层是整个架构的"宪法"。文档明确要求:Storage API contracts 绝不能吸收 ECStore 或读取管线的实现细节。
2.1 契约层明确"不负责"的四类内容
以下内容越界,不属于契约层:
- KMS/SSE 实现——加密是存储后端的具体能力,契约层不应暴露其内部机制;
- Range 与压缩行为——读取时的范围切片与磁盘压缩属于读取管线细节;
- 纠删码与 bitrot 逻辑——ECStore 的纠删码编码与位衰减校验属于存储实现;
- 远端磁盘传输与恢复——internode 数据传输与磁盘故障恢复不进入契约。
也就是说,契约层只声明"是什么"(对象长什么样、操作签名如何、快照结构如何),不声明"怎么做"。
2.2 No-drift 行为清单:什么绝对不能变
契约层存在的意义是保证兼容性基线不漂移。文档列出以下不变式(no-drift behavior):
- 对象到 set 的哈希映射不变(object-to-set hash);
- 写仲裁(write quorum)语义不变;
- 读端解密、etag/checksum、版本、删除标记行为不变;
- 纯移动(pure move)期间,公共兼容路径通过临时 re-export 或 wrapper 保持可用。
这一"临时 re-export / wrapper"策略在 crates/ecstore/src/api/mod.rs 中得到了具体体现——整个文件就是一个庞大的显式门面层,例如pub mod cluster直接 re-export 了ClusterControlPlane、各快照类型与投影函数:
pub mod cluster { pub use crate::cluster::{ ClusterControlPlane, ClusterControlPlaneSnapshot, ClusterDriveMembership, ClusterEndpointType, ClusterLocalNodeStorage, ClusterLocalNodeStorageSnapshot, ClusterMembershipSnapshot, ClusterNodeMembership, ClusterPeerHealth, ClusterPeerHealthSnapshot, ClusterPoolState, ClusterPoolStateSnapshot, ClusterRpcBoundarySnapshot, ClusterRpcChannelSnapshot, ClusterRpcPlane, ClusterRpcTransport, local_node_storage_snapshot_from_membership, membership_snapshot_from_endpoint_pools, peer_health_snapshot_from_membership, pool_state_snapshot_from_endpoint_pools, rpc_boundary_snapshot, topology_snapshot_from_endpoint_pools, topology_snapshot_from_endpoint_pools_with_capabilities, }; }(见 crates/ecstore/src/api/mod.rs)
2.3 契约类型与能力状态模型(源码级展开)
契约层的核心类型定义在 crates/storage-api/src/topology.rs 与 crates/storage-api/src/capability.rs。
拓扑快照的层级结构(TopologySnapshot→TopologyPool→TopologySet→TopologyDisk):
pub struct TopologySnapshot { pub pools: Vec<TopologyPool>, pub capabilities: TopologyCapabilities, // profiling / numa / failure_domain_labels / media_labels } pub struct TopologyPool { pub pool_index: usize, pub pool_id: Option<String>, pub labels: TopologyLabels, // zone / rack / node / media / numa_node / additional pub sets: Vec<TopologySet>, } pub struct TopologyDisk { pub pool_index: usize, pub set_index: usize, pub disk_index: usize, pub disk_id: Option<String>, pub labels: TopologyLabels, pub capabilities: DiskCapabilities, // media_type / failure_domain / numa / profiling }(见 crates/storage-api/src/topology.rs)
所有结构体都标注了#[serde(deny_unknown_fields)],意味着契约快照的序列化格式是严格封闭的——多一个未知字段即反序列化失败,这从机制上防止了契约漂移。
能力状态四态模型(CapabilityState):Supported/Unsupported/Disabled/Unknown,其中Unknown是默认态,且通过#[serde(other)]保证未来新增的状态值也能被保守地解析为Unknown,而不是解析失败——这是前向兼容的关键设计:
pub enum CapabilityState { Supported, Unsupported, Disabled, #[serde(other)] #[default] Unknown, }(见 crates/storage-api/src/capability.rs)
CapabilityStatus则由state+ 可选reason组成,控制面投影时会给每个状态附上人类可读的reason字符串(下文会看到大量示例)。
三、集群控制面:从端点池投影只读快照
3.1 设计原则:先做只读门面,不急于独立 crate
文档给出的演进纪律非常明确:
ClusterControlPlane先作为crates/ecstore/src/cluster内部的只读门面存在。在内部依赖稳定之前,不要创建独立的 cluster crate。
这是典型的"先内聚、后抽取"策略——避免在依赖关系尚未稳定时过早拆 crate 导致的编译与架构震荡。
3.2 初始范围的五类快照
控制面的初始范围被严格限定为以下快照投影(snapshot projection):
| 快照 | 含义 |
|---|---|
| Topology snapshot | 拓扑快照:pool/set/disk 三级结构 + 能力状态 |
| Membership snapshot | 成员快照:节点与驱动器的归属关系 |
| Lock registry snapshot | 锁注册表快照 |
| Peer health snapshot | 对端健康快照 |
| Pool state snapshot | 池状态快照 |
3.3 只读边界:禁止做什么
文档用否定式清单定义了门面的边界。控制面必须:
- 不暴露本地磁盘路径(本地路径是 ECStore 内部实现细节);
- 不启动健康检查(不发起 probe);
- 不改变端点所有权(不 mutate endpoint ownership);
- 不改变 placement/readiness 判定。
源码中的实现完全遵循了这一约束。crates/ecstore/src/cluster/control_plane.rs 中的ClusterControlPlane仅持有EndpointServerPools引用,并暴露一系列*_snapshot()只读方法:
pub struct ClusterControlPlane { endpoint_pools: EndpointServerPools, } impl ClusterControlPlane { pub fn topology_snapshot(&self) -> TopologySnapshot { ... } pub fn membership_snapshot(&self) -> ClusterMembershipSnapshot { ... } pub fn pool_state_snapshot(&self) -> ClusterPoolStateSnapshot { ... } pub fn local_node_storage_snapshot(&self) -> ClusterLocalNodeStorageSnapshot { ... } pub fn peer_health_snapshot(&self) -> ClusterPeerHealthSnapshot { ... } pub fn rpc_boundary_snapshot(&self) -> ClusterRpcBoundarySnapshot { ... } pub fn read_snapshot(&self) -> ClusterControlPlaneSnapshot { ... } }(见 crates/ecstore/src/cluster/control_plane.rs)
其中read_snapshot()一次性聚合六类子快照,形成完整的ClusterControlPlaneSnapshot:topology、pool_state、local_storage、peer_health、rpc_boundary、membership。
3.4 控制面投影的具体数据模型
从源码可以完整还原各快照的字段:
成员快照(ClusterMembershipSnapshot)按节点分组、按驱动器展开:
pub struct ClusterNodeMembership { pub node_id: String, pub grid_host: String, pub is_local: bool, pub pools: Vec<usize>, } pub struct ClusterDriveMembership { pub pool_index: usize, pub set_index: usize, pub disk_index: usize, pub node_id: String, pub is_local: bool, pub endpoint_type: ClusterEndpointType, // Path | Url }(见 crates/ecstore/src/cluster/control_plane.rs)
池状态快照(ClusterPoolStateSnapshot)给出每个池的容量结构统计:set_count、drives_per_set、endpoint_count、local_drive_count、remote_drive_count、legacy标记以及端点类型集合。remote_drive_count通过endpoint_count.saturating_sub(local_drive_count)计算。
本节点存储快照(ClusterLocalNodeStorageSnapshot)只保留本地节点,并区分path_drive_count(本地路径端点)与url_drive_count(远端 URL 端点)——这正是"不暴露本地磁盘路径"原则的体现:只统计数量,不泄露路径字符串。
RPC 边界快照(ClusterRpcBoundarySnapshot)将通道显式划分为两个平面:
// 控制面通道:metadata / lock / health / administrative,走 gRPC // 数据面通道:remote_disk_stream,走 internode data transport(见 crates/ecstore/src/cluster/control_plane.rs)
3.5 peer_health_snapshot:投影而非探测
文档特别强调了一个易被误解的点:peer_health_snapshot只是把 internode 健康追踪器(internode health tracker)已经观测到的结果投影出来,门面本身不发起任何 probe、不发出任何 RPC 健康检查。
源码对应的投影逻辑在peer_health_status_for_node(见 crates/ecstore/src/cluster/control_plane.rs),通过rustfs_io_metrics::internode_metrics::cluster_peer_observed_online_status查询既有观测,映射为三种状态:
| 观测结果 | 投影状态 | reason 字符串 |
|---|---|---|
| 本地节点 | Supported | local node does not require peer health probing |
对端可达(Some(true)) | Supported | peer marked reachable by internode health tracker |
对端不可达(Some(false)) | Unknown | peer marked unreachable by internode health tracker |
未被上报(None) | Disabled | peer health not reported by endpoints |
注意这里"不可达"被投影为Unknown而非Unsupported——因为不可达可能是暂时性的,控制面只反映观测事实,不做归因。
3.6 测试如何守护只读边界
crates/ecstore/src/cluster/control_plane.rs 内置了一组单元测试,直接守护文档承诺的架构约束,是理解本层的最佳入口:
topology_snapshot_maps_endpoint_sets_without_local_paths:构造/tmp/rustfs-cluster-control-plane-{0..3}本地端点池后生成拓扑快照,并断言序列化后的 JSON 中不包含任何本地路径字符串(assert!(!encoded.contains("/tmp/rustfs-cluster-control-plane")))——这是"不暴露本地磁盘路径"约束的机械化验证;topology_snapshot_uses_url_hosts_as_disk_ids:URL 端点的disk_id与nodelabel 使用node1.example:9000这样的 host:port 标识;membership_snapshot_groups_nodes_and_drives:验证节点分组与驱动器展开;pool_state_snapshot_counts_local_remote_drives_and_endpoint_types:验证本地/远端驱动器计数与端点类型集合;local_node_storage_snapshot_keeps_only_local_drive_counts:验证只统计本地节点的 path/url 驱动器数量;peer_health_snapshot_reports_observed_peer_status:预置可达/不可达/未上报三台对端,验证投影状态与 reason 完全符合预期;control_plane_read_snapshot_combines_topology_and_membership:端到端验证read_snapshot()聚合后的完整快照;rpc_boundary_snapshot_keeps_control_rpc_separate_from_data_streams:验证控制通道全部为 gRPC、数据通道全部为 internode data transport,二者平面严格分离。
这些测试的存在,让"门面不越界"从文档口号变成了每次cargo test都会执行的机械检查。
3.7 风险控制:三条不可简化的语义
文档最后为控制面演进列出了三条风险红线:
- 分布式锁仲裁保持 per-set——绝不能退化成按节点数或端点数判断;
- RemoteDisk 的 suspect/offline/recovery、超时与连接驱逐语义不得简化——磁盘故障语义是数据安全底线;
- 若健康影响行为改变生产行为,必须用 feature gate 包裹。
这与 docs/architecture/readiness-matrix.md 中的"行为保持基线"一脉相承:控制面快照、健康探测、就绪发布各司其职,任何改动都不得破坏既有的FullReady组合语义(storage_ready && iam_ready && lock_quorum_ready && peer_health_ready)。
四、后台控制器:先固定词汇,再谈统一抽象
4.1 现状:没有泛型控制器,只有统一词汇
文档给出了一个反直觉但非常重要的现状:RustFS 中不存在BackgroundControllertrait、调度器或服务注册表。scanner、heal、lifecycle、replication 等后台服务各自暴露类型化的快照(snapshot)与协调计划(reconcile plan)。
这份文档(连同其姊妹篇 background-controller-contract.md)的作用是先统一词汇与规则,为未来可能的统一抽象铺路。
4.2 五词词汇表:架构对话的公共语言
| 术语 | 含义 | 边界 |
|---|---|---|
| Desired | 来自环境变量、持久化配置、模块开关、feature flag、桶配置或 admin 配置的静态意图 | 只读;收集 Desired 状态时绝不规范化或修改配置 |
| Current | 观测到的本地运行时状态:configured、disabled、running、degraded、stopping、unknown | 只读;绝不通过会产生存储/网络副作用的 probe 推断 |
| Status | 可机器检查的快照:计数器、worker 数、队列压力、上次周期、上次错误、取消来源、shutdown handle 形态 | 无副作用;缺失的表面上报为unknown,绝不猜测 |
| Reconcile | 对比 Desired、Current、Status 后产出的计划 | 已发布的计划只做报告;唯一允许的 worker 变更请求是none |
| Side effects | 写/删、队列准入、目标激活、外部 I/O、指标发射、就绪发布、对端信号、配置重载 fanout | 每个服务在控制器触碰之前必须显式声明 |
4.3 状态模型:用代码能证明的最窄状态
快照只使用代码能证明的最窄状态集合,共 8 个状态:NotConfigured(无有效 Desired 源)、Disabled(有 Desired 源但显式禁用,区别于配置缺失)、Starting、Running、Degraded(活动但存在已知错误/部分/停滞)、Stopping、Stopped(区别于 Disabled 和 NotConfigured)、Unknown(无安全状态面,优先于臆测)。
关键规则是:不为快照发明新的故障分类,且取消来源与 shutdown handle 形态必须与 Desired 的启用/禁用状态分开上报。
4.4 只读快照的硬性要求
后台服务的状态采集必须满足:
- 绝不启动/停止/调整大小/唤醒任何 worker;
- 绝不写入存储数据、对象元数据、目标状态、队列条目、持久化配置或 resync 元数据;
- 绝不发布就绪信号或对端重载信号;
- 缺失字段为
unknown或附带说明后省略; - 对同一快照重复调用
reconcile必须返回相同计划(确定性); - scanner、heal、lifecycle、replication 的状态不得隐藏其队列与准入耦合。
4.5 耦合清单:哪些服务不能折叠进泛型控制器
文档在 background-controller-contract.md 的 Coupling Notes 中列出了一份重要的耦合清单——这些服务共享状态或关闭契约,折叠进泛型控制器前必须有服务特定的保持测试:
- Scanner 蕴含 heal:
init_data_scanner(rustfs/src/startup_lifecycle.rs)启动的循环会入队 heal 工作,scanner 状态必须分离调度器状态与工作源统计; - Heal/AHM 持有自己的取消令牌:
create_ahm_services_cancel_token与init_heal_manager(rustfs/src/startup_background.rs)创建,shutdown_ahm_services(rustfs/src/startup_shutdown.rs)关闭,heal 准入与通道关闭语义必须保持完整; - Replication 有两个关闭契约:
init_background_replication(rustfs/src/startup_storage.rs)启动的池通过关闭通道停止 worker,而init_resync(rustfs/src/startup_bucket_metadata.rs)启动的 resync 使用取消令牌,admin 触发的 resync 使用 per-bucket 令牌——三种关闭机制不可混为一谈; - Lifecycle 不是独立周期控制器:过期、转换与 stale-multipart 清理由
ECStore::init(init_background_expiry、init_background_stale_multipart_upload_cleanup,见 crates/ecstore/src/store/init.rs)启动,通过bind_background_cancel_token绑定运行时令牌,且scanner 是它们的事件源; - Notification 与 audit 共享运行时模式但不共享生命周期:
init_event_notifier与start_audit_system(rustfs/src/startup_audit.rs)启动,shutdown_event_notifier与stop_audit_system(rustfs/src/startup_shutdown.rs)关闭——活跃事件流与目标投递启用必须分离; - 动态配置重载是 admin 触发的 fanout 而非循环:
apply_dynamic_config_for_subsystem、signal_dynamic_config_reload、signal_config_snapshot_reload(rustfs/src/admin/service/config.rs); - 容量刷新任务通过
init_capacity_management_managed返回的CapacityBackgroundTasks持有(rustfs/src/capacity/capacity_integration.rs,由 rustfs/src/startup_entrypoint.rs 调用),调度间隔默认值与 singleflight 刷新保持不变; - 存储邻近监视器不属于控制器工作:
monitor_and_connect_endpoints(crates/ecstore/src/core/sets.rs)与enable_health_check(crates/ecstore/src/disk/disk_store.rs)会改变磁盘状态,必须留在控制器之外; - 延迟 IAM 恢复、可选协议服务器、自动调优器全部留在泛型控制器之外:
spawn_iam_recovery_task(rustfs/src/startup_iam.rs)会发布就绪;可选协议服务器已各自持有ShutdownHandle;init_auto_tuner(rustfs/src/init.rs)会改变运行时并发度。
4.6 演进路线:第一步只做只读状态与关闭排序
文档对后台控制器的演进节奏给出了明确的路线图:
第一个控制器工作应该是只读状态与关闭排序(read-only status and shutdown ordering),而不是行为变更。
结合 background-controller-contract.md 中给出的参考实现——MemoryObservabilityReconcilePlan与reconcile()(rustfs/src/memory_observability.rs)、AllocatorReclaimControllerSnapshot与AllocatorReclaimReconcilePlan(rustfs/src/allocator_reclaim.rs)、MetricsRuntimeReconcilePlan(crates/obs/src/metrics/scheduler.rs)——可以推断出这套模式的落地形态:每个服务拥有自己的类型化快照结构与协调计划类型,计划只报告、不执行变更。这是把"观测"与"干预"彻底分离的设计,也是后续安全演进的前提。
五、三层协同:一张图看懂所有权边界
把三层放在一起,可以提炼出 RustFS 存储层架构的所有权边界:
- 契约层(crates/storage-api):定义封闭的契约类型与能力状态模型,
deny_unknown_fields保证格式不漂移,#[serde(other)]保证未来状态保守兼容; - 门面层(crates/ecstore/src/api/mod.rs):以显式 re-export 把 ECStore 内部实现映射到契约层类型,纯移动期间通过临时 re-export 维持公共兼容路径;
- 控制面(crates/ecstore/src/cluster/control_plane.rs):
ClusterControlPlane从EndpointServerPools投影六类只读快照,不暴露本地路径、不发起探测、不改变所有权与 placement; - 后台控制器:不设泛型抽象,各服务以类型化快照 + 协调计划对外暴露,先统一词汇(Desired/Current/Status/Reconcile/Side effects),第一步演进只做只读状态与关闭排序。
贯穿四者的共同底线是:观测与干预分离、快照无副作用、兼容路径不漂移。这套纪律既体现在文档的 no-drift 清单中,也体现在control_plane.rs的单元测试(如"序列化 JSON 不含本地路径"断言)与 readiness-matrix.md 的行为保持基线中。
六、实践建议:如何在 RustFS 中落地这套约束
如果你正准备在 RustFS 中新增一个存储 API 面或后台状态面,可以按以下清单自检:
- 新增 trait/类型:放入 crates/storage-api,只声明契约,不引用 ECStore 内部类型;结构体加
#[serde(deny_unknown_fields)]; - 新增能力状态:使用
CapabilityState四态(Supported/Unsupported/Disabled/Unknown),附reason字符串说明,不发明新分类; - 新增集群快照:通过
ClusterControlPlane的投影方法生成,快照中不得出现本地磁盘路径字符串,不得触发任何 probe 或 RPC; - 新增后台状态面:先参考 background-controller-contract.md 的词汇表定义类型化快照与协调计划,计划只报告
none变更;如需改动生产行为,用 feature gate 包裹并在 readiness-matrix.md 中登记就绪影响; - 守卫回归:为新的投影逻辑补充与 control_plane.rs 中同风格的单元测试,把架构约束固化为机械化断言。
遵循上述清单,你的改动就能与 RustFS 既有的"契约—门面—控制面—控制器"四层边界保持一致,避免在演进过程中破坏分布式锁仲裁、磁盘故障语义与 S3 数据面兼容性这三条不可触碰的底线。
【免费下载链接】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),仅供参考