POSIX 文件系统一套、KV 引擎一套、S3 对象存储一套------企业里三套存储系统并存,数据靠搬运互通,元数据三份各自维护,跨协议可见性按分钟计。本文讲清楚 PowerFS 如何用 协议无关的 Needle 数据层 + Filer Raft 强一致元数据层,让 POSIX / KV / S3 三种协议共享同一份元数据、同一池数据,从根上消除数据孤岛。
一、引言:数据孤岛是怎么产生的
企业存储的典型现状是「按协议买系统」:
- 应用要
open/read/write?→ 买一套 POSIX 文件系统(Lustre / GPFS / NFS)。 - AI 训练要读数据集?→ 买一套 S3 对象存储。
- 低延迟模型参数?→ 自研一套 KV 引擎。
三套系统各自维护一份元数据,结果就是:
| 痛点 | 表现 |
|---|---|
| 元数据割裂 | 同一份数据在三套系统里有三个名字、三套属性 |
| 数据搬运 | POSIX 写完要 sync 到 S3,S3 上传完要回灌 KV |
| 跨协议延迟 | 分钟级最终一致,AI 训练读到旧数据 |
| 运维爆炸 | 三套监控、三套扩缩容、三套故障排查 |
根因只有一个:没有一个统一的、强一致的元数据层把三种协议拢到一起。PowerFS 的解法是 Filer Raft------一份元数据,三种协议共享。
二、PowerFS 四组件架构:协议无关的数据底座
PowerFS 把存储拆成四个解耦组件,元数据与数据彻底分层:
┌──────────────────────────────────────────────────────────────────┐
│ 客户端层(三种协议入口) │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ POSIX/FUSE │ │ S3 Gateway │ │ KV Client │ │
│ │ powerfs-fuse│ │ powerfs-s3 │ │ powerfs-cli │ │
│ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │
│ │ MetadataClient │ S3Handler │ KvCacheProvider │
└─────────┼─────────────────┼────────────────┼─────────────────────┘
│ │ │
┌─────────▼─────────────────▼────────────────▼─────────────────────┐
│ Filer 层(Raft 强一致,统一元数据------协议无关的核心) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 按 bucket 分片,每 bucket 独立 Raft 组(3 节点 quorum) │ │
│ │ POSIX/KV/S3 三协议共享同一份 ShardCommand + RocksDB │ │
│ │ - 写:Raft propose → quorum 复制 → apply │ │
│ │ - 读:Leader Lease Read(任期内本地读,无 read index) │ │
│ │ - Invalidate 推送:跨协议缓存失效 │ │
│ └────────────────────────┬─────────────────────────────────┘ │
└────────────────────────────┼─────────────────────────────────────┘
│
┌────────────────────────────▼─────────────────────────────────────┐
│ Master 层(Raft 调度,不碰文件系统元数据) │
│ VolumeAssigner / ClusterScheduler / file_key 块预分配 │
└────────────────────────────┬─────────────────────────────────────┘
│
┌────────────────────────────▼─────────────────────────────────────┐
│ Volume 层(Needle 协议无关数据存储------三种协议同一池) │
│ Needle O(1) 寻址 / Stripe 64MB Lease / EC 纠删码 │
└──────────────────────────────────────────────────────────────────┘
四组件的核心分工:
- Master :Raft 集群,只做 Volume 分配、拓扑、心跳,不管理文件系统元数据(已删除 DirectoryTree 死代码)。
- Filer :Raft 集群,按 bucket 分片,统一管理所有元数据,是三协议归一的支点。
- Volume Server:Needle 格式数据存储,Stripe Lease 强一致,三种协议的数据落在同一池。
- POSIX客户端(FUSE/KERNEL)/ S3 / KV 客户端:通过统一 trait 调 Filer,自身只做协议适配与缓存。其中 POSIX 客户端分两种形态:FUSE 为用户态客户端,部署灵活,适合开发/测试/普通生产环境;KERNEL 为内核客户端,跳过 FUSE 内核→用户态切换,更低延迟,适合 HPC 极致性能场景。
三、Filer Raft:一份元数据,三协议共享
1. 为什么不是 CRDT
PowerFS 早期版本用过 OR-Set CRDT 弱一致元数据,试图靠 CRDT 自动收敛来「免去协调」。实践证明这条路在存储系统里是错的:
- CRDT 只能最终一致,
open()后可能读到旧的size/chunks,导致跨客户端数据损坏。 - delta sync、
.conflicts/目录、三级合并把代码复杂度推到不可维护。 - 弱一致模型无法支撑 POSIX
fsync后立即可见的硬性要求。
V3 已经完全删除 OR-Set CRDT、delta sync、.conflicts/、三级合并等所有弱一致代码,转向 Filer Raft 强一致。代价是写多一次 Raft RTT,换来的是线性一致、跨协议立即可见------这正是消除数据孤岛的关键。
2. 按 bucket 分片,每 bucket 独立 Raft 组
Filer 不是「一个大 Raft」覆盖所有元数据,而是按 bucket(顶级目录)分片,每个 bucket 跑一个独立的 Raft 组:
bucket: /hpc-ckpt → Raft Group 1(filer-1, filer-2, filer-3)
bucket: /ai-dataset → Raft Group 2(filer-1, filer-2, filer-3)
bucket: /kv-cache → Raft Group 3(filer-1, filer-2, filer-3)
好处:
- 水平扩展:bucket 之间互不阻塞,元数据 OPS 随 bucket 数线性增长。
- 故障隔离:一个 Raft 组的 leader 切换不影响其他 bucket。
- 跨 shard 边界返回 EXDEV:类似 Linux mount boundary,语义清晰,避免跨组分布式事务的复杂性。
3. 统一元数据接口:MetadataClient trait
无论客户端是 POSIX、S3 还是 KV,最终都通过 MetadataClient trait 访问 Filer(来自 powerfs-fuse-core/src/metadata_client.rs):
rust
/// 强一致元数据操作接口。
/// - 写操作:Leader 提交 Raft log 后返回
/// - 读操作:Leader Lease Read(不经 read index)
/// 调用方负责传入正确的 shard_id(按 bucket 分片)。
pub trait MetadataClient: Send + Sync {
fn lookup(&self, parent_ino: u64, name: &str, shard_id: u64)
-> Pin<Box<dyn Future<Output = Result<MetadataAttr>> + Send + '_>>;
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 + '_>>;
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 + '_>>;
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 readdir(&self, ino: u64, offset: u64, count: u32, shard_id: u64)
-> Pin<Box<dyn Future<Output = Result<Vec<MetadataDirEntry>>> + Send + '_>>;
fn getattr(&self, ino: u64, shard_id: u64)
-> Pin<Box<dyn Future<Output = Result<MetadataAttr>> + Send + '_>>;
fn setattr(&self, ino: u64, params: &SetattrParams, shard_id: u64)
-> Pin<Box<dyn Future<Output = Result<MetadataAttr>> + Send + '_>>;
// ... unlink / rmdir / symlink / readlink / link / statfs
}
这个 trait 是协议无关的:POSIX客户端(FUSE/KERNEL)、S3 Gateway、KV 引擎都依赖它,因此三种协议看到的是同一份强一致元数据视图。
4. 写路径:Raft propose → apply
Filer 端一次 create 的完整路径(来自 powerfs-filer/src/meta_shard_manager.rs):
rust
pub async fn create_file_with_shard(
&self,
parent_inode: u64,
name: &str,
shard_id: ShardId,
) -> Result<u64, String> {
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();
let cmd = ShardCommand::CreateFile {
parent_inode,
name: name.to_string(),
inode,
};
// 提交 Raft:leader 复制到 quorum 后才返回
self.raft_group_manager
.propose(shard_id, cmd.serialize())
.await?;
// 轮询本地 apply(apply 在 commit 之后,写入 RocksDB)
let mut retries = 0;
while retries < 50 {
if shard_store.get_inode(inode).is_some() {
return Ok(inode);
}
tokio::time::sleep(Duration::from_millis(10)).await;
retries += 1;
}
Err("timeout waiting for apply".to_string())
}
ShardCommand 覆盖全部元数据变更(来自 powerfs-filer/src/raft_group_manager.rs),其中 UpdateInodeSizeChunks 是跨协议可见性的关键------close() 时把 size + chunks 走 Raft 强一致同步,S3 和 FUSE 立刻能读到最新数据:
rust
pub enum ShardCommand {
CreateFile { parent_inode: u64, name: String, inode: u64 },
CreateDirectory { parent_inode: u64, name: String, inode: u64 },
Rename { old_parent_inode: u64, old_name: String,
new_parent_inode: u64, new_name: String },
PutObject { parent_inode: u64, name: String, inode: u64, size: u64,
fid: String, volume_id: u64, etag: String },
UpdateInodeSizeChunks {
inode: u64,
size: u64,
chunks: Vec<StoredFileChunk>,
},
SetAttr { inode: u64, size: Option<u64>, mode: Option<u64>,
uid: Option<u64>, gid: Option<u64> },
CreateSymlink { parent_inode: u64, name: String, inode: u64, target: String },
// ...
}
5. 读路径:Leader Lease Read
读操作不走 read index,而是利用 Leader Lease Read:leader 在自己任期内(已被 quorum 授予 lease)直接本地读,省掉一次 RTT。这保证三种协议读到的都是线性一致视图,不会出现「S3 刚上传、FUSE 读不到」的孤岛现象。
Raft 配置采用生产级参数(来自 RaftGroup::new):
rust
let mut cfg = Config {
id,
election_tick: 30, // tick 50ms × 30 = [1.5s, 3s) 选举超时
heartbeat_tick: 5,
max_size_per_msg: 1 << 20,
max_inflight_msgs: 256,
check_quorum: !peers.is_empty(), // 防旧 leader 脑裂
pre_vote: true, // 防多节点同时选举 split vote
..Default::default()
};
四、协议无关的数据层:Needle O(1) 寻址
三种协议的数据都落在同一池 Volume Server 上,靠 Needle 格式实现协议无关的 O(1) 寻址:
- file_key:文件级标识,由 Master 按 1000/batch 走 Raft 预分配。
- needle_id = file_key + chunk_idx:块级寻址,无需扫描。
- Volume 路由 :Master 维护
volume_id → VolumeServer路由表,三种协议共用。 - Stripe Lease (64MB):数据层强一致锁,写者独占,与协议无关。
FileChunk 结构(来自 powerfs-common/src/traits.rs)就是协议无关的数据定位凭证:
rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileChunk {
pub offset: u64,
pub size: u64,
/// 块级存储键(volume server 上的 needle_id)
/// file_key 是文件级;每个 chunk 的 needle_id = file_key + chunk_idx
pub needle_id: u64,
/// 该 chunk 所在的 volume(支持 stripe 模式)
pub volume_id: u64,
pub crc32: u32,
pub mtime: u64,
}
无论是 FUSE read、S3 GetObject 还是 KV get_block,最终都解析成 (volume_id, needle_id, offset, size) 去 Volume Server 取数据。同一份数据,三种协议都能读,无需搬运。
五、三协议如何共享同一份元数据
| 协议 | 二进制 | 元数据入口 | 数据访问 |
|---|---|---|---|
| POSIX/FUSE | powerfs-fuse |
MetadataClient trait → Filer RPC |
Volume read_blob/write_blob |
| 对象存储 | powerfs-s3 |
Master LockManager + Filer 元数据 |
Volume Needle |
| KV | powerfs-cli kv |
KvCacheProvider → Filer KV Cache |
Volume block |
关键点:
- S3 上传立即可见 :
PutObject是一条ShardCommand,Raft commit 后 FUSE 端readdir立刻看到,没有跨协议延迟。 - FUSE 写完 S3 可读 :
close()走UpdateInodeSizeChunks,size+chunks 全网强一致,S3GetObject立即读到。 - Invalidate 跨协议失效:Filer 元数据变更时,对所有订阅的客户端(无论协议)推送 Invalidate,缓存 100ms 内失效。
六、实际部署:四组件一条命令启动
每个组件独立二进制,统一 -c <config> 配置:
bash
# Master(Raft 调度,3 节点)
powerfs-master -c config/master-1.toml
powerfs-master -c config/master-2.toml
powerfs-master -c config/master-3.toml
# Filer(Raft 元数据核心,3 节点,按 bucket 分片)
powerfs-filer -c config/filer-1.toml
powerfs-filer -c config/filer-2.toml
powerfs-filer -c config/filer-3.toml
# Volume Server(Needle 数据池,可按需扩容)
powerfs-volume -c config/volume-1.toml
powerfs-volume -c config/volume-2.toml
powerfs-volume -c config/volume-3.toml
# 三种协议入口
powerfs-fuse -c config/fuse-1.toml # POSIX 挂载
powerfs-s3 -c config/s3.toml # S3 Gateway
powerfs-cli kv put mykey value # KV 访问
扩容时按瓶颈加节点:元数据瓶颈加 Filer,数据瓶颈加 Volume Server,三协议共享,无需为某个协议单独扩容。
七、对比:三套堆叠 vs PowerFS 一套
| 维度 | 三套堆叠方案 | PowerFS 协议无关 |
|---|---|---|
| 元数据份数 | 3 份(POSIX/KV/S3 各一) | 1 份(Filer Raft) |
| 跨协议可见性 | 分钟级最终一致 | 秒级(Raft commit + Invalidate) |
| 数据搬运 | 必须(sync 工具链) | 无需(同一 Needle 池) |
| 一致性模型 | 各家不一 | Filer Raft 强一致 + Volume Lease |
| 元数据扩展 | 单点瓶颈 | 按 bucket 分片水平扩展 |
| 运维 | 3 套监控 | 1 套(powerfs-monitor) |
八、场景案例
某 AI 平台同时对接三类业务:
- 训练平台 :用
powerfs-s3上传训练数据集,元数据走 Filer。 - 推理服务 :用
powerfs-fuse挂载 POSIX 读取同一批数据集,open()强制 getattr 拉最新 size,上传即可读。 - 特征平台 :用
powerfs-cli kv写模型参数,KvCacheProvider背后仍是 Filer Raft,参数写入对 POSIX 端立刻可见。 - 故障切换 :某 Filer 节点宕机,Raft 1.5-3s 选出新 leader,
pre_vote防脑裂,三协议元数据零丢失。
三种协议,一份元数据,一个数据池------数据孤岛从架构层面被消灭。
九、保留的设计与未来演进
转向 Filer Raft 强一致的同时,PowerFS 保留了核心设计:
- Needle 格式 O(1) 寻址------不变,协议无关的数据底座。
- SPDK NVMe / RDMA / GPU Direct 硬件加速 ------不变,
Transporttrait 已为 RDMA 预留BatchTransport。 - Rust 原生无 GC 无抖动------不变。
- Collection 多租户隔离------不变。
- EC 纠删码数据冗余------不变。
协议无关的数据层 + Filer Raft 强一致元数据层,让 PowerFS 用一套系统承载 POSIX / KV / S3 三种协议,彻底终结数据孤岛。
项目地址:https://github.com/powerfs/powerfs
欢迎 Star、Fork、提交 PR!