
当 HPC 作业遇上 AI 训练存储系统往往被「撕裂」一边要 POSIX 强一致一边要海量小文件高吞吐一边要对象存储语义。传统做法是堆三套系统结果就是数据孤岛、元数据瓶颈、运维地狱。本文拆解 PowerFS 如何用Master Filer Raft Volume Lease client 四组件解耦架构一套系统同时承载 POSIX/KV/S3 三种接口元数据走 Raft 强一致、数据走 Stripe Lease 强一致从根上消灭存储碎片化。一、引言HPCAI 混合负载为什么难在智算中心里一个典型的工作流是这样的HPC 仿真作业通过 MPI 写出大量 checkpoint 文件要求 POSIX 语义、fsync后跨节点可见、open()必须读到最新 size。AI 训练作业每秒可能创建上千个小文件ckpt 片段、tensor dump对元数据 OPS 极其敏感同时需要从对象存储语义读取数据集。数据流转HPC 产出的 checkpoint 又要被 AI 训练当作数据集消费跨协议访问成为刚需。传统方案只能堆系统负载传统选型痛点POSIX 强一致Lustre / GPFS元数据单点、运维复杂、扩容停服海量小文件自研 KV 引擎与 POSIX 数据不互通需搬运对象存储S3 兼容网关又一套元数据数据孤岛三套系统 三份元数据 三倍运维成本 数据搬运带宽浪费。这就是「存储碎片化」的根源。PowerFS 的解法是用一套元数据层Filer Raft 一套数据层Volume Needle统一承载三种接口让 HPC、AI、对象存储共享同一份强一致元数据。二、PowerFS V3 四组件解耦架构PowerFS 把整个存储系统拆成四个职责清晰、相互解耦的组件┌──────────────────────────────────────────────────────────────────┐ │ 客户端层 │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ POSIX 客户端 │ │ S3 Gateway │ │ KV Client │ │ │ │ (kernel/ │ │ (powerfs- │ │ │ │ │ │ fuse) │ │ s3) │ │ │ │ │ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │ │ │ MetadataClient │ S3Handler │ KvCacheProvider │ └─────────┼────────────────────┼─────────────┼─────────────────────┘ │ │ │ ┌─────────▼────────────────────▼─────────────▼─────────────────────┐ │ Filer 层Raft 强一致元数据核心 │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ MetaShardManager按 bucket 分片每 bucket 独立 Raft 组 │ │ │ │ - 写操作ShardCommand → Raft propose → apply → RocksDB │ │ │ │ - 读操作Leader Lease Readleader 任期内本地读无 read index│ │ │ - UpdateInodeSizeChunksclose 时强一致同步 sizechunks │ │ │ │ - Invalidate 通知变更主动推送给订阅客户端 │ │ │ │ - S3HandlerS3 Gateway 内置于此 │ │ │ └────────────────────────┬─────────────────────────────────┘ │ └────────────────────────────┼─────────────────────────────────────┘ │ Volume 分配 / 路由 ┌────────────────────────────▼─────────────────────────────────────┐ │ Master 层Raft 调度不碰文件系统元数据 │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ VolumeAssigner卷分配 │ │ │ │ ClusterScheduler集群拓扑 / 节点心跳 │ │ │ │ ResilientMasterClientleader 发现 故障转移 │ │ │ │ next_file_key 块预分配Raft batch1000/batch │ │ │ └────────────────────────┬─────────────────────────────────┘ │ └────────────────────────────┼─────────────────────────────────────┘ │ ┌────────────────────────────▼─────────────────────────────────────┐ │ Volume 层Needle O(1) 数据存储 │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Volume Server│ │ Volume Server│ │ Volume Server│ │ │ │ - Needle O(1)│ │ - Stripe Lease│ │ - Compact │ │ │ │ - 64MB Lease │ │ 排他写锁 │ │ - used_bytes │ │ │ │ - EC 纠删码 │ │ - 持久化恢复 │ │ 跟踪 │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ └──────────────────────────────────────────────────────────────────┘1. Master只做调度不碰元数据这是 V3 架构最关键的「减法」。在旧版本里Master 内部维护了一个DirectoryTree把文件系统元数据塞在调度层导致职责混乱、死代码堆积。V3 已经删除了约 7000 行 DirectoryTree 死代码Master 现在只负责Volume 分配VolumeAssigner按 collection / replication 分配 Volume。集群拓扑管理ClusterScheduler维护 rack / data_center 拓扑。节点心跳周期性收集 Volume Server 上报的NodeStats。file_key 块预分配每分配 1000 个 file_key 才走一次 Raft 提交避免每次写都打 Master。Master 自身仍是 Raft 集群3 节点但它不再管理任何文件系统元数据——这是 Filer 的职责。2. FilerRaft 强一致元数据核心Filer 是 V3 架构的元数据中枢所有文件系统操作lookup/mkdir/create/unlink/rmdir/rename/readdir/setattr/getattr/symlink/readlink/link都由它统一管理。按 bucket 分片每个顶级目录bucket对应一个独立的 Raft 组组内 3 节点强一致。写走 Raft commitShardCommand序列化后propose到 leaderquorum 复制后才返回。读走 Leader Lease Readleader 在自己任期内本地读不走 read index省掉一次 RTT。Invalidate 推送元数据变更时主动通知订阅的 FUSE 客户端失效缓存。3. Volume ServerNeedle Stripe Lease 强一致数据Needle 格式 O(1) 寻址needle_id file_key chunk_idx定位无需扫描。Stripe (64MB) Lease 排他锁写者独占一个 stripe60s 内可复用避免多客户端并发写冲突。EC 纠删码数据冗余与 Lustre/Ceph 同级别。Lease 持久化崩溃后可从持久化 backend 恢复 lease 状态防止脑裂。4. ClientMetadataClient trait 调用 Filer客户端不再自己持有任何元数据真相源它只是 Filer 的一个 RPC 客户端通过MetadataClienttrait 调用 Filer RPCLAN 约 1-2ms。inode_cacheattr 缓存TTL 2s callback invalidation。ChunkCache数据块缓存预取 4 个 chunk。无目录列表缓存Phase 1保证readdir总是最新。三、一致性模型元数据 Raft 数据 LeaseV3 全面转向强一致元数据强一致所有元数据写操作走 Filer Raft3 节点 quorumLeader Lease Read 保证线性一致读。一个mkdir的完整路径是FUSE mkdir(parent, name) → MetadataClient::mkdir(parent_ino, name, ..., shard_id) → Filer leader: ShardCommand::CreateDirectory{parent_inode, name, inode} → Raft propose → quorum 复制 → commit → apply_command 写入 RocksDB → 轮询 shard_store.get_inode(inode) 确认可见 → 返回 MetadataAttr数据强一致Volume Server 用 Stripe (64MB) Lease 做排他锁写者acquire一个 stripe 的Exclusivelease独占写入。60s 内同一写者复用 lease零开销续写。写者崩溃后server 端 grace 期清理新写者接管。close()时通过UpdateInodeSizeChunks走 Raft 把size chunks强一致同步到所有 Filer 节点。跨客户端可见性这是 HPCAI 场景最容易踩坑的地方。PowerFS 用两层机制保证open() 强制 getattropen时绕过 TTL强制向 Filer 拉取最新size/chunks避免读到旧 EOF。callback invalidationFiler 元数据变更时主动推送 Invalidate 给持有该 inode 缓存的客户端100ms 内全网失效。四、代码实战MetadataClient trait 与 Raft 写路径1. MetadataClient trait——统一的元数据接口FUSE 客户端通过这一个 trait 访问所有元数据写走 Raft、读走 Leader Lease Read来自powerfs-fuse-core/src/metadata_client.rs/// 强一致元数据操作接口。////// 所有方法走 Filer Raft leader/// - 写操作Leader 提交 Raft log 后返回/// - 读操作Leader Lease Read不经 read index////// 调用方负责传入正确的 shard_id按 bucket 分片。/// Filer leader 切换时由实现内部重试调用方无感。pubtraitMetadataClient:SendSync{/// lookup查询目录条目fnlookup(self,parent_ino:u64,name:str,shard_id:u64,)-PinBoxdynFutureOutputResultMetadataAttrSend_;/// mkdir创建目录fnmkdir(self,parent_ino:u64,name:str,mode:u32,uid:u32,gid:u32,shard_id:u64,)-PinBoxdynFutureOutputResultMetadataAttrSend_;/// create创建普通文件fncreate(self,parent_ino:u64,name:str,mode:u32,uid:u32,gid:u32,shard_id:u64,fid_info:Option(u64,u64,u64),)-PinBoxdynFutureOutputResultMetadataAttrSend_;/// rename / symlink / readlink / link / readdir / getattr / setattr ...fnrename(self,parent_ino:u64,name:str,new_parent_ino:u64,new_name:str,shard_id:u64,)-PinBoxdynFutureOutputResultMetadataAttrSend_;fngetattr(self,ino:u64,shard_id:u64,)-PinBoxdynFutureOutputResultMetadataAttrSend_;// ... unlink / rmdir / symlink / readlink / link / readdir / setattr / statfs}关键点shard_id由调用方按 bucket 计算传入Filer 内部路由到对应 Raft 组。写操作返回前leader 已完成 Raft commitquorum 节点可见。读操作利用 leader 任期 lease本地直接读避免 read index 的额外 RTT。2. Filer 端 Raft 写路径mkdir在 Filer 端的落地逻辑来自powerfs-filer/src/meta_shard_manager.rspubasyncfncreate_directory(self,parent_inode:u64,name:str,)-ResultInodeInfo,String{letshard_idself.shard_strategy.calculate_shard(parent_inode);letshard_storeself.shard_stores.read().unwrap().get(shard_id).ok_or_else(||format!(shard {} not found,shard_id.0))?.clone();letinodeself.generate_inode();// 1. 构造强一致命令letcmdShardCommand::CreateDirectory{parent_inode,name:name.to_string(),inode,};// 2. 提交 Raftleader 复制到 quorum 后才返回self.raft_group_manager.propose(shard_id,cmd.serialize()).await?;// 3. 轮询本地 apply 结果apply 在 Raft commit 之后letmutretries0;whileretries50{ifletSome(info)shard_store.get_inode(inode){returnOk(info);}tokio::time::sleep(Duration::from_millis(10)).await;retries1;}Err(failed to create directory: timeout waiting for apply.to_string())}ShardCommand是一个枚举覆盖全部元数据变更来自powerfs-filer/src/raft_group_manager.rspubenumShardCommand{CreateFile{parent_inode:u64,name:String,inode:u64},UpdateFile{inode:u64,size:u64,mtime:u64},DeleteFile{parent_inode:u64,name:String},CreateDirectory{parent_inode:u64,name:String,inode:u64},DeleteDirectory{parent_inode:u64,name:String},Rename{old_parent_inode:u64,old_name:String,new_parent_inode:u64,new_name:String},SetAttr{inode:u64,size:Optionu64,mode:Optionu64,..},// close 时强一致同步 size chunks保证跨客户端读正确UpdateInodeSizeChunks{inode:u64,size:u64,chunks:VecStoredFileChunk,},CreateSymlink{parent_inode:u64,name:String,inode:u64,target:String},CreateHardLink{inode:u64,new_parent_inode:u64,new_name:String},PutObject{parent_inode:u64,name:String,inode:u64,size:u64,fid:String,volume_id:u64,etag:String},// ...}Raft 事件循环里committed_entries被反序列化成ShardCommand再交给shard_store.apply_command()落到 RocksDBmatchShardCommand::deserialize(entry.data){Ok(cmd){letapply_entryApplyEntry{shard_id:self.shard_id,index:entry.index,command:cmd.clone(),};// 同步投递到 apply channel保证 apply 在 applied_index 更新前入队self.apply_tx.try_send(apply_entry)?;}Err(e)error!(Shard {} deserialize failed: {},self.shard_id.0,e),}3. Raft 配置生产级参数Filer Raft 组采用 etcd / CockroachDB 同款生产配置来自RaftGroup::newletmutcfgConfig{id,// tick_interval50ms, election_tick30 选举超时 [1.5s, 3s)1.5s 随机跨度election_tick:30,heartbeat_tick:5,max_size_per_msg:120,max_inflight_msgs:256,check_quorum:!peers.is_empty(),// PreVote先探测能否胜出避免多节点同时选举导致 split vote / term 无限增长pre_vote:true,..Default::default()};check_quorumpre_vote是防止网络分区后旧 leader 继续服务、引发脑裂的标准做法。五、Volume Lease数据层强一致锁数据层的强一致靠 Stripe Lease 实现来自powerfs-volume/src/range_lease.rs与powerfs-leasecrate/// Stripe 资源键标识某 inode 的一段 stripe 区间#[derive(Clone, PartialEq, Eq, Hash, Debug)]pubstructStripeKey{pubinode:u64,pubstripe_start:u64,pubstripe_count:u64,}implLeaseKeyforStripeKey{fngroup_id(self)-u64{self.inode}fnconflicts(self,other:Self)-bool{ifself.inode!other.inode{returnfalse;}letself_endself.stripe_startself.stripe_count;letother_endother.stripe_startother.stripe_count;self.stripe_startother_endother.stripe_startself_end}// ...}/// 默认 stripe 粒度 64MBpubfnwith_defaults()-Self{Self::new(64*1024*1024)}客户端侧LeaseManagertrait来自powerfs-lease/src/manager.rspubtraitLeaseManager:SendSync{/// 获取 lease命中缓存则零 RPC 返回fnacquire(self,volume_id:u64,inode:u64,mode:LeaseMode,stripe_start:u64,stripe_count:u64,duration_ms:u64)-PinBoxdynFutureOutputResultLeaseToken,LeaseErrorSendstatic;/// 释放 lease发 ReleaseLease RPC 并清缓存fnrelease(self,volume_id:u64,inode:u64,token:LeaseToken)-PinBoxdynFutureOutputResult(),LeaseErrorSendstatic;/// 查询状态监控用fnstate(self,volume_id:u64,inode:u64)-OptionLeaseState;}LeaseToken是强类型非裸字符串LeaseGuard提供 RAII 自动释放LeasePersistence支持 lease 状态崩溃恢复。这样写者独占、读者可共享数据层无需 CRDT 即可保证强一致。六、三接口统一FUSE / KV / S3 共享同一份元数据四组件架构天然支持三接口统一因为它们共享同一个 Filer Raft 元数据层接口二进制元数据访问方式适用场景POSIX/FUSEpowerfs-fuseMetadataClienttrait → Filer RPCHPC 仿真、传统应用对象存储powerfs-s3MasterLockManager Filer 元数据AI 数据集、备份归档KVpowerfs-cli kvKvCacheProvider→ Filer KV Cache模型参数、特征存储S3 Gateway 是独立二进制powerfs-s3使用 Master 的LockManager做分布式锁元数据仍走 Filer因此S3 上传的对象能立刻被 FUSE 端ls看到——不再有「数据孤岛」。七、实际部署四个二进制一条龙PowerFS 把每个组件编译成独立二进制统一用-c config指定配置文件组合灵活# Master 集群3 节点 Raft仅调度powerfs-master-cconfig/master-1.toml powerfs-master-cconfig/master-2.toml powerfs-master-cconfig/master-3.toml# Filer 集群3 节点 Raft元数据核心powerfs-filer-cconfig/filer-1.toml powerfs-filer-cconfig/filer-2.toml powerfs-filer-cconfig/filer-3.toml# Volume Server数据存储Needle Leasepowerfs-volume-cconfig/volume-1.toml powerfs-volume-cconfig/volume-2.toml powerfs-volume-cconfig/volume-3.toml# FUSE 客户端挂载powerfs-fuse-cconfig/fuse-1.toml# S3 Gateway独立二进制powerfs-s3-cconfig/s3.toml# CLI 工具powerfs-cli status# 集群状态powerfs-cli assign# 卷分配powerfs-cli lookupinode# 元数据查询powerfs-clifsck# 一致性检查每个组件都可独立扩缩容元数据瓶颈就加 Filer数据瓶颈就加 Volume Server互不干扰。八、与三套堆叠方案对比维度传统三套堆叠PowerFS 四组件元数据一致性三份各自管理需搬运Filer Raft 统一强一致数据一致性各家锁机制不一Volume Stripe Lease 统一跨协议可见性分钟级同步秒级Invalidate open getattr元数据 OPS单点瓶颈按 bucket 分片水平扩展运维复杂度三套监控一套监控powerfs-monitor部署三套部署流程一套二进制-c启动九、场景案例某智算中心同时跑 CFD 仿真MPI 写 checkpoint和 LLM 训练读数据集 写 ckptHPC 侧powerfs-fuse挂载仿真进程以 POSIX 语义写 checkpointfsync后立刻跨节点可见Filer Raft commit 保证。AI 侧训练进程通过powerfs-s3读取 HPC 产出的数据集元数据来自同一份 Filer Raft上传即可见。故障切换某 Filer 节点宕机Raft 1.5-3s 内选出新 leaderpre_vote防止双节点同时竞选元数据零丢失。并发写两个客户端写同一文件的不同 64MB stripe各自持有不重叠的 Exclusive lease互不阻塞。十、保留的设计与未来演进V3 在转向强一致的同时保留了 PowerFS 一贯的核心设计Needle 格式 O(1) 寻址——不变。SPDK NVMe / RDMA / GPU Direct 硬件加速——不变Transporttrait 已为 RDMA 预留BatchTransport接口。Rust 原生无 GC 无抖动——不变。Collection 多租户隔离——不变。EC 纠删码数据冗余——不变。四组件解耦 Filer Raft 强一致 Volume Lease 强一致让 PowerFS 在 HPCAI 混合负载下用一套系统替代了传统的三套堆叠从架构层面消灭了存储碎片化。项目地址https://github.com/powerfs/powerfs欢迎 Star、Fork、提交 PR