当 HPC 作业遇上 AI 训练,存储系统往往被「撕裂」:一边要 POSIX 强一致,一边要海量小文件高吞吐,一边要对象存储语义。传统做法是堆三套系统,结果就是数据孤岛、元数据瓶颈、运维地狱。本文拆解 PowerFS 如何用 Master + Filer Raft + Volume Lease + client 四组件解耦架构,一套系统同时承载 POSIX/KV/S3 三种接口,元数据走 Raft 强一致、数据走 Stripe Lease 强一致,从根上消灭存储碎片化。
一、引言:HPC+AI 混合负载为什么难
在智算中心里,一个典型的工作流是这样的:
- 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 Read(leader 任期内本地读,无 read index)│
│ │ - UpdateInodeSizeChunks:close 时强一致同步 size+chunks │ │
│ │ - Invalidate 通知:变更主动推送给订阅客户端 │ │
│ │ - S3Handler:S3 Gateway 内置于此 │ │
│ └────────────────────────┬─────────────────────────────────┘ │
└────────────────────────────┼─────────────────────────────────────┘
│ Volume 分配 / 路由
┌────────────────────────────▼─────────────────────────────────────┐
│ Master 层(Raft 调度,不碰文件系统元数据) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ VolumeAssigner:卷分配 │ │
│ │ ClusterScheduler:集群拓扑 / 节点心跳 │ │
│ │ ResilientMasterClient:leader 发现 + 故障转移 │ │
│ │ next_file_key 块预分配(Raft batch,1000/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. Filer:Raft 强一致元数据核心
Filer 是 V3 架构的元数据中枢,所有文件系统操作(lookup/mkdir/create/unlink/rmdir/rename/readdir/setattr/getattr/symlink/readlink/link)都由它统一管理。
- 按 bucket 分片:每个顶级目录(bucket)对应一个独立的 Raft 组,组内 3 节点强一致。
- 写走 Raft commit :
ShardCommand序列化后propose到 leader,quorum 复制后才返回。 - 读走 Leader Lease Read:leader 在自己任期内本地读,不走 read index,省掉一次 RTT。
- Invalidate 推送:元数据变更时主动通知订阅的 FUSE 客户端失效缓存。
3. Volume Server:Needle + Stripe Lease 强一致数据
- Needle 格式 O(1) 寻址 :
needle_id = file_key + chunk_idx,定位无需扫描。 - Stripe (64MB) Lease 排他锁:写者独占一个 stripe,60s 内可复用,避免多客户端并发写冲突。
- EC 纠删码:数据冗余与 Lustre/Ceph 同级别。
- Lease 持久化:崩溃后可从持久化 backend 恢复 lease 状态,防止脑裂。
4. Client:MetadataClient trait 调用 Filer
客户端不再自己持有任何元数据真相源,它只是 Filer 的一个 RPC 客户端:
- 通过
MetadataClienttrait 调用 Filer RPC(LAN 约 1-2ms)。 inode_cache:attr 缓存,TTL 2s + callback invalidation。ChunkCache:数据块缓存,预取 4 个 chunk。- 无目录列表缓存 (Phase 1),保证
readdir总是最新。
三、一致性模型:元数据 Raft + 数据 Lease
V3 全面转向强一致:
元数据强一致
所有元数据写操作走 Filer Raft(3 节点 quorum),Leader 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 节点。
跨客户端可见性
这是 HPC+AI 场景最容易踩坑的地方。PowerFS 用两层机制保证:
- open() 强制 getattr :
open时绕过 TTL,强制向 Filer 拉取最新size/chunks,避免读到旧 EOF。 - callback invalidation:Filer 元数据变更时主动推送 Invalidate 给持有该 inode 缓存的客户端,100ms 内全网失效。
四、代码实战:MetadataClient trait 与 Raft 写路径
1. MetadataClient trait------统一的元数据接口
FUSE 客户端通过这一个 trait 访问所有元数据,写走 Raft、读走 Leader Lease Read(来自 powerfs-fuse-core/src/metadata_client.rs):
rust
/// 强一致元数据操作接口。
///
/// 所有方法走 Filer Raft leader:
/// - 写操作:Leader 提交 Raft log 后返回
/// - 读操作:Leader Lease Read(不经 read index)
///
/// 调用方负责传入正确的 shard_id(按 bucket 分片)。
/// Filer leader 切换时由实现内部重试,调用方无感。
pub trait MetadataClient: Send + Sync {
/// lookup:查询目录条目
fn lookup(
&self,
parent_ino: u64,
name: &str,
shard_id: u64,
) -> Pin<Box<dyn Future<Output = Result<MetadataAttr>> + Send + '_>>;
/// mkdir:创建目录
fn mkdir(
&self,
parent_ino: u64,
name: &str,
mode: u32,
uid: u32,
gid: u32,
shard_id: u64,
) -> Pin<Box<dyn Future<Output = Result<MetadataAttr>> + Send + '_>>;
/// create:创建普通文件
fn create(
&self,
parent_ino: u64,
name: &str,
mode: u32,
uid: u32,
gid: u32,
shard_id: u64,
fid_info: Option<(u64, u64, u64)>,
) -> Pin<Box<dyn Future<Output = Result<MetadataAttr>> + Send + '_>>;
/// rename / symlink / readlink / link / readdir / getattr / setattr ...
fn rename(
&self,
parent_ino: u64,
name: &str,
new_parent_ino: u64,
new_name: &str,
shard_id: u64,
) -> Pin<Box<dyn Future<Output = Result<MetadataAttr>> + Send + '_>>;
fn getattr(
&self,
ino: u64,
shard_id: u64,
) -> Pin<Box<dyn Future<Output = Result<MetadataAttr>> + Send + '_>>;
// ... unlink / rmdir / symlink / readlink / link / readdir / setattr / statfs
}
关键点:
shard_id由调用方按 bucket 计算传入,Filer 内部路由到对应 Raft 组。- 写操作返回前,leader 已完成 Raft commit,quorum 节点可见。
- 读操作利用 leader 任期 lease,本地直接读,避免 read index 的额外 RTT。
2. Filer 端 Raft 写路径
mkdir 在 Filer 端的落地逻辑(来自 powerfs-filer/src/meta_shard_manager.rs):
rust
pub async fn create_directory(
&self,
parent_inode: u64,
name: &str,
) -> Result<InodeInfo, String> {
let shard_id = self.shard_strategy.calculate_shard(parent_inode);
let shard_store = self.shard_stores.read().unwrap()
.get(&shard_id)
.ok_or_else(|| format!("shard {} not found", shard_id.0))?
.clone();
let inode = self.generate_inode();
// 1. 构造强一致命令
let cmd = ShardCommand::CreateDirectory {
parent_inode,
name: name.to_string(),
inode,
};
// 2. 提交 Raft:leader 复制到 quorum 后才返回
self.raft_group_manager
.propose(shard_id, cmd.serialize())
.await?;
// 3. 轮询本地 apply 结果(apply 在 Raft commit 之后)
let mut retries = 0;
while retries < 50 {
if let Some(info) = shard_store.get_inode(inode) {
return Ok(info);
}
tokio::time::sleep(Duration::from_millis(10)).await;
retries += 1;
}
Err("failed to create directory: timeout waiting for apply".to_string())
}
ShardCommand 是一个枚举,覆盖全部元数据变更(来自 powerfs-filer/src/raft_group_manager.rs):
rust
pub enum ShardCommand {
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: Option<u64>, mode: Option<u64>, .. },
// close 时强一致同步 size + chunks,保证跨客户端读正确
UpdateInodeSizeChunks {
inode: u64,
size: u64,
chunks: Vec<StoredFileChunk>,
},
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() 落到 RocksDB:
rust
match ShardCommand::deserialize(&entry.data) {
Ok(cmd) => {
let apply_entry = ApplyEntry {
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::new):
rust
let mut cfg = Config {
id,
// tick_interval=50ms, election_tick=30 => 选举超时 [1.5s, 3s),1.5s 随机跨度
election_tick: 30,
heartbeat_tick: 5,
max_size_per_msg: 1 << 20,
max_inflight_msgs: 256,
check_quorum: !peers.is_empty(),
// PreVote:先探测能否胜出,避免多节点同时选举导致 split vote / term 无限增长
pre_vote: true,
..Default::default()
};
check_quorum + pre_vote 是防止网络分区后旧 leader 继续服务、引发脑裂的标准做法。
五、Volume Lease:数据层强一致锁
数据层的强一致靠 Stripe Lease 实现(来自 powerfs-volume/src/range_lease.rs 与 powerfs-lease crate):
rust
/// Stripe 资源键:标识某 inode 的一段 stripe 区间
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub struct StripeKey {
pub inode: u64,
pub stripe_start: u64,
pub stripe_count: u64,
}
impl LeaseKey for StripeKey {
fn group_id(&self) -> u64 { self.inode }
fn conflicts(&self, other: &Self) -> bool {
if self.inode != other.inode { return false; }
let self_end = self.stripe_start + self.stripe_count;
let other_end = other.stripe_start + other.stripe_count;
self.stripe_start < other_end && other.stripe_start < self_end
}
// ...
}
/// 默认 stripe 粒度 64MB
pub fn with_defaults() -> Self {
Self::new(64 * 1024 * 1024)
}
客户端侧 LeaseManager trait(来自 powerfs-lease/src/manager.rs):
rust
pub trait LeaseManager: Send + Sync {
/// 获取 lease(命中缓存则零 RPC 返回)
fn acquire(&self, volume_id: u64, inode: u64, mode: LeaseMode,
stripe_start: u64, stripe_count: u64, duration_ms: u64)
-> Pin<Box<dyn Future<Output = Result<LeaseToken, LeaseError>> + Send + 'static>>;
/// 释放 lease(发 ReleaseLease RPC 并清缓存)
fn release(&self, volume_id: u64, inode: u64, token: &LeaseToken)
-> Pin<Box<dyn Future<Output = Result<(), LeaseError>> + Send + 'static>>;
/// 查询状态(监控用)
fn state(&self, volume_id: u64, inode: u64) -> Option<LeaseState>;
}
LeaseToken 是强类型(非裸字符串),LeaseGuard 提供 RAII 自动释放,LeasePersistence 支持 lease 状态崩溃恢复。这样写者独占、读者可共享,数据层无需 CRDT 即可保证强一致。
六、三接口统一:FUSE / KV / S3 共享同一份元数据
四组件架构天然支持三接口统一,因为它们共享同一个 Filer Raft 元数据层:
| 接口 | 二进制 | 元数据访问方式 | 适用场景 |
|---|---|---|---|
| POSIX/FUSE | powerfs-fuse |
MetadataClient trait → Filer RPC |
HPC 仿真、传统应用 |
| 对象存储 | powerfs-s3 |
Master LockManager + Filer 元数据 |
AI 数据集、备份归档 |
| KV | powerfs-cli kv |
KvCacheProvider → Filer KV Cache |
模型参数、特征存储 |
S3 Gateway 是独立二进制 powerfs-s3,使用 Master 的 LockManager 做分布式锁,元数据仍走 Filer,因此 S3 上传的对象能立刻被 FUSE 端 ls 看到------不再有「数据孤岛」。
七、实际部署:四个二进制一条龙
PowerFS 把每个组件编译成独立二进制,统一用 -c <config> 指定配置文件,组合灵活:
bash
# Master 集群(3 节点 Raft,仅调度)
powerfs-master -c config/master-1.toml
powerfs-master -c config/master-2.toml
powerfs-master -c config/master-3.toml
# Filer 集群(3 节点 Raft,元数据核心)
powerfs-filer -c config/filer-1.toml
powerfs-filer -c config/filer-2.toml
powerfs-filer -c config/filer-3.toml
# Volume Server(数据存储,Needle + Lease)
powerfs-volume -c config/volume-1.toml
powerfs-volume -c config/volume-2.toml
powerfs-volume -c config/volume-3.toml
# FUSE 客户端挂载
powerfs-fuse -c config/fuse-1.toml
# S3 Gateway(独立二进制)
powerfs-s3 -c config/s3.toml
# CLI 工具
powerfs-cli status # 集群状态
powerfs-cli assign # 卷分配
powerfs-cli lookup <inode> # 元数据查询
powerfs-cli fsck # 一致性检查
每个组件都可独立扩缩容:元数据瓶颈就加 Filer,数据瓶颈就加 Volume Server,互不干扰。
八、与三套堆叠方案对比
| 维度 | 传统三套堆叠 | PowerFS 四组件 |
|---|---|---|
| 元数据一致性 | 三份各自管理,需搬运 | Filer Raft 统一强一致 |
| 数据一致性 | 各家锁机制不一 | Volume Stripe Lease 统一 |
| 跨协议可见性 | 分钟级同步 | 秒级(Invalidate + open getattr) |
| 元数据 OPS | 单点瓶颈 | 按 bucket 分片,水平扩展 |
| 运维复杂度 | 三套监控 | 一套监控(powerfs-monitor) |
| 部署 | 三套部署流程 | 一套二进制 -c 启动 |
九、场景案例
某智算中心同时跑 CFD 仿真(MPI 写 checkpoint)和 LLM 训练(读数据集 + 写 ckpt):
- HPC 侧 :
powerfs-fuse挂载,仿真进程以 POSIX 语义写 checkpoint,fsync后立刻跨节点可见(Filer Raft commit 保证)。 - AI 侧 :训练进程通过
powerfs-s3读取 HPC 产出的数据集,元数据来自同一份 Filer Raft,上传即可见。 - 故障切换 :某 Filer 节点宕机,Raft 1.5-3s 内选出新 leader,
pre_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 在 HPC+AI 混合负载下,用一套系统替代了传统的三套堆叠,从架构层面消灭了存储碎片化。
项目地址:https://github.com/powerfs/powerfs
欢迎 Star、Fork、提交 PR!