HPC+AI混合负载存储碎片化严重?PowerFS Filer Raft强一致四组件统一架构一套搞定

当 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 commitShardCommand 序列化后 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 客户端:

  • 通过 MetadataClient trait 调用 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 的 Exclusive lease,独占写入。
  • 60s 内同一写者复用 lease,零开销续写。
  • 写者崩溃后,server 端 grace 期清理,新写者接管。
  • close() 时通过 UpdateInodeSizeChunks 走 Raft 把 size + chunks 强一致同步到所有 Filer 节点。

跨客户端可见性

这是 HPC+AI 场景最容易踩坑的地方。PowerFS 用两层机制保证:

  1. open() 强制 getattropen 时绕过 TTL,强制向 Filer 拉取最新 size/chunks,避免读到旧 EOF。
  2. 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.rspowerfs-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 硬件加速 ------不变(Transport trait 已为 RDMA 预留 BatchTransport 接口)。
  • Rust 原生无 GC 无抖动------不变。
  • Collection 多租户隔离------不变。
  • EC 纠删码数据冗余------不变。

四组件解耦 + Filer Raft 强一致 + Volume Lease 强一致,让 PowerFS 在 HPC+AI 混合负载下,用一套系统替代了传统的三套堆叠,从架构层面消灭了存储碎片化。


项目地址:https://github.com/powerfs/powerfs

欢迎 Star、Fork、提交 PR!

相关推荐
HySpark1 小时前
语音输入法实践:Rust 集成 Paraformer 推理引擎踩坑与优化过程
人工智能·后端·语音识别·熙瑾会悟
不爱土豆唯爱马铃薯1 小时前
用MonkeyCode做一个野外数据采集表 | MonkeyCode实操系列EP06
人工智能·数据分析
董员外1 小时前
RAG 系统进化论(九):RAG 评测,怎样证明系统真的变好了?
人工智能·设计模式·程序员
咩咩啃树皮1 小时前
第63篇【终极整合巨作】从零手写原生中后台管理系统|整合62篇全部前端知识点|从头到尾完整架构+全功能逻辑
前端·架构
IT小盘1 小时前
10-使用Reranker提升RAG回答准确率
人工智能·python
小黑技术栈1 小时前
DevEco Code Plan+Build模式:审方案再执行的技术实践
人工智能
凌云拓界1 小时前
NodeVerdict:Node.js 原生诊断数据可视化工具
信息可视化·架构·typescript·开源·node.js·github·bug
Thom5801 小时前
【聚宽 JoinQuant】聚宽如何获取持仓和订单?get_orders()与get_open_orders()教程
人工智能·经验分享·量化交易·聚宽·量化编程
EasyDSS1 小时前
景区客流遇冷?用EasyDSS企业融媒体平台直播/点播解锁「云上文旅」新玩法
人工智能·媒体·直播·点播·easydss