01 JuiceFS 全局架构——元数据引擎与对象存储的分离设计

摘要

JuiceFS 是一款面向云原生场景的高性能分布式文件系统,其核心设计思想是将文件系统的元数据与数据彻底分离:元数据存储在用户选择的数据库(Redis/TiKV/MySQL 等)中,而实际数据以对象(Object)形式存储在任意对象存储(S3/OSS/COS/MinIO 等)上。这种分离设计使 JuiceFS 能够以极低成本为 Hadoop 生态、AI 训练、数据湖等场景提供 POSIX 兼容的文件系统语义,同时彻底解耦存储扩展性和元数据性能。本文剖析这一设计的动机、架构组成以及与 HDFS 和 CephFS 的本质差异。


第 1 章 为什么需要 JuiceFS——云时代的存储困境

1.1 HDFS 的历史遗产与现实困境

Hadoop 生态的基石 HDFS(Hadoop Distributed File System)在十余年前的大数据时代堪称革命性设计:NameNode 管理元数据,DataNode 分布式存储数据块,通过机架感知实现高可用。但随着云计算和容器化的普及,HDFS 的局限性越来越突出:

存算强耦合:HDFS 将计算节点(运行 MapReduce/Spark 的节点)和存储节点(DataNode)绑定在一起,利用”数据本地性”减少网络传输。但在 Kubernetes 等弹性调度环境中,计算节点随时扩缩容,数据本地性几乎无法保证——存算耦合反而成了弹性扩展的障碍。

NameNode 单点与容量限制:HDFS 的 NameNode 将所有文件元数据(inode)存储在 JVM 堆内存中。一个典型配置的 NameNode(128GB 堆)大约能支持 4 亿个文件/目录(每个 inode 约 300 字节)。对于拥有数十亿文件的超大规模集群,NameNode 内存成为硬性瓶颈。Federation 方案可以缓解,但增加了运维复杂度。

运维重量级:HDFS 集群依赖 ZooKeeper 实现 NameNode HA,依赖 JournalNode 同步编辑日志,需要专门的集群运维团队。对于中小规模团队,维护一套 HDFS 集群的成本往往超过其带来的收益。

与对象存储的割裂:云环境下,对象存储(S3/OSS)价格低廉、按量付费、无限扩展,几乎成为数据湖的事实存储层。但 HDFS API 与对象存储 API(PUT/GET/LIST)存在语义差距——POSIX 文件系统的 append、rename、hard link 等操作很难高效地映射到对象存储。

1.2 JuiceFS 的设计出发点

JuiceFS(Juice File System,由 Juicedata 开发,2021 年开源)从一开始就为云原生场景设计,核心洞察是:

元数据与数据的 I/O 特征截然不同

  • 元数据操作(lsstatmkdir):延迟敏感,需要毫秒级响应;数据量小(每个文件的元数据只有几百字节);访问模式是随机的键值查找
  • 数据读写(文件内容):吞吐敏感,延迟要求相对宽松;数据量大;访问模式以顺序读写为主

基于这个洞察,JuiceFS 将两者交给最擅长各自场景的存储系统处理:

  • 元数据 → 专用键值数据库(Redis/TiKV)或关系型数据库(MySQL/PostgreSQL),提供低延迟的随机读写
  • 数据 → 对象存储(S3/OSS/MinIO),提供低成本、高吞吐的块存储

这种”让合适的系统做合适的事”的设计哲学,使 JuiceFS 在性能、成本和运维复杂度上同时达到了较好的平衡。

设计哲学

JuiceFS 不是一个从头设计的分布式存储系统,而是一个文件系统语义适配层——它将 POSIX 文件操作翻译成两类不同的后端操作:元数据操作(写入元数据数据库)和数据操作(写入对象存储)。底层存储系统的选择完全开放,用户可以根据自身场景(延迟要求、成本预算、运维能力)选择最合适的后端。


第 2 章 JuiceFS 的三层架构

2.1 整体架构图


graph TD
    subgraph "应用层"
        APP1["POSIX App</br>(Linux 文件系统调用)"]
        APP2["Hadoop App</br>(HDFS API)"]
        APP3["S3 App</br>(S3 Gateway)"]
    end

    subgraph "JuiceFS 客户端层"
        FUSE["JuiceFS FUSE</br>挂载点(/mnt/jfs)"]
        SDK["JuiceFS SDK</br>(Hadoop/Java)"]
        GW["S3 Gateway</br>(兼容 S3 API)"]
        CACHE["本地磁盘缓存</br>(Block Cache)"]
    end

    subgraph "元数据层"
        REDIS["Redis/TiKV</br>(inode、dentry、chunk)"]
        TIKV["TiKV</br>(大规模元数据)"]
        MYSQL["MySQL/PostgreSQL</br>(小规模部署)"]
    end

    subgraph "数据层(对象存储)"
        S3["AWS S3"]
        OSS["阿里云 OSS"]
        MINIO["MinIO</br>(自建)"]
    end

    APP1 --> FUSE
    APP2 --> SDK
    APP3 --> GW
    FUSE --> CACHE
    SDK --> CACHE
    GW --> CACHE
    FUSE -- "元数据读写" --> REDIS
    SDK -- "元数据读写" --> TIKV
    FUSE -- "数据读写(Block)" --> S3
    SDK -- "数据读写(Block)" --> OSS
    CACHE -- "缓存命中" --> FUSE

    classDef app fill:#6272a4,stroke:#bd93f9,color:#f8f8f2
    classDef client fill:#ff79c6,stroke:#ffb86c,color:#282a36
    classDef meta fill:#44475a,stroke:#50fa7b,color:#f8f8f2
    classDef data fill:#282a36,stroke:#8be9fd,color:#f8f8f2
    class APP1,APP2,APP3 app
    class FUSE,SDK,GW,CACHE client
    class REDIS,TIKV,MYSQL meta
    class S3,OSS,MINIO data

2.2 三个核心组件

JuiceFS 客户端:运行在使用文件系统的机器上,负责:

  • 通过 FUSE 内核模块向应用提供 POSIX 文件系统接口(open/read/write/mkdir 等系统调用透明拦截)
  • 将文件操作分解为元数据操作(写入/读取元数据引擎)和数据操作(写入/读取对象存储)
  • 管理本地磁盘缓存(Block Cache),加速频繁访问的数据块读写

客户端是 JuiceFS 的核心逻辑所在,完全运行在用户空间(Go 语言实现),没有独立的服务端进程——这是 JuiceFS 与 HDFS、CephFS 最大的架构差异。

元数据引擎:存储文件系统的所有元数据,包括:

  • inode:文件/目录的属性(size、mtime、uid、gid、mode 等)
  • dentry:目录条目(父目录 → 子文件/目录的映射关系)
  • chunk:文件数据的逻辑分块信息(每个 chunk 对应哪些 slice,每个 slice 存储在对象存储的哪个位置)
  • session:客户端会话(用于处理文件锁和 FUSE 保活)

元数据引擎是可插拔的(Redis/TiKV/MySQL/PostgreSQL/SQLite),详见第 2 篇文章。

对象存储:存储文件的实际数据内容。JuiceFS 将文件数据按固定大小的 Block(默认 4MB)切割后存储到对象存储中。Block 的对象键名包含了 Volume 名称和 Block 的唯一标识,通过元数据引擎中的 chunk 信息还原文件到 Block 的映射关系。


第 3 章 关键设计:文件数据的存储模型

3.1 Chunk → Slice → Block 的三层映射

JuiceFS 的文件数据存储模型分三层:

Chunk(逻辑段):文件按 64MB 为单位划分为若干 Chunk(类似 HDFS 的块大小概念,但不是物理块)。每个 Chunk 有一个唯一的 Chunk ID,元数据引擎中记录 inode → [Chunk0, Chunk1, ...] 的映射。

Slice(写操作单元):每次 write 操作在 Chunk 内的目标位置生成一个 Slice(包含偏移量 off、长度 len、以及对应的 Block 列表)。允许同一个 Chunk 内有多个 Slice,Slice 之间可能有重叠(用于实现随机写入的覆盖语义)。

Block(对象存储单元):每个 Slice 的数据被切割为若干固定大小的 Block(默认 4MB),每个 Block 作为独立的对象存储在对象存储中,对象键格式为 {volume}/{chunk_id}/{block_index}_{block_size}

文件 /data/train.csv (100MB)
    ↓ 按 64MB 划分为 Chunk
Chunk 0 (0-64MB):
    Slice 0 (off=0, len=4MB)  → Block: jfs-vol/1/0_4194304
    Slice 1 (off=4MB, len=4MB) → Block: jfs-vol/1/1_4194304
    ...(共 16 个 4MB Block)

Chunk 1 (64MB-100MB):
    Slice 0 (off=0, len=4MB)  → Block: jfs-vol/2/0_4194304
    ...(共 9 个 Block,最后一个 Block 可能不足 4MB)

3.2 为什么这样设计——处理随机写入的核心逻辑

传统对象存储只支持 PUT(完整上传)和 GET(完整下载),不支持随机修改(在文件中间某个位置覆盖写)。但 POSIX 文件系统要求支持随机写入(pwrite 系统调用)。

JuiceFS 的解决方案:允许同一个 Chunk 内存在多个 Slice,新的写入生成新的 Slice,通过元数据中的 Slice 覆盖关系来表达随机写入的语义

读取时,按 Chunk 内的 Slice 列表,从后向前查找覆盖关系,找到每个偏移量对应的最新 Slice,然后从对应的 Block 中读取数据。这个逻辑称为 Copy-on-Write(写时复制)——旧 Block 不修改,新写入生成新 Block,通过元数据追踪覆盖关系。

Compaction(碎片整理):当一个 Chunk 内的 Slice 数量积累过多(默认超过 1000 个),JuiceFS 客户端会触发后台 Compaction:将多个 Slice 合并为一个大 Slice,减少读取时的 Slice 查找开销,同时删除不再被引用的旧 Block,释放对象存储空间。


第 4 章 与同类系统的架构对比

4.1 JuiceFS vs HDFS

维度JuiceFSHDFS
元数据架构外部数据库(Redis/TiKV/MySQL)单点 NameNode(内存)
数据存储任意对象存储(S3/OSS/MinIO)专用 DataNode(本地磁盘)
服务端进程无(客户端直连元数据引擎和对象存储)NameNode + DataNode 集群
POSIX 兼容完整 POSIX(通过 FUSE)有限(不支持 append 以外的随机写)
弹性扩缩容存算完全解耦,按需扩展存算耦合,DataNode 扩容同时增加存储和计算
元数据容量受元数据引擎限制(Redis 受内存限制)NameNode 堆内存限制(约 4 亿文件)
运维复杂度低(只需维护元数据引擎和对象存储)高(NameNode HA + JournalNode + ZooKeeper)
适用场景云原生、Kubernetes、AI 训练传统大数据(MapReduce/Spark on YARN)

4.2 JuiceFS vs CephFS

CephFS 是另一种常见的分布式文件系统,同样基于 FUSE 提供 POSIX 语义。主要差异:

底层存储:CephFS 数据存储在 Ceph RADOS(自建分布式块存储),需要专门维护 Ceph 集群(Monitor + OSD)。JuiceFS 数据存储在已有的对象存储上,无需自建存储集群。

元数据架构:CephFS 元数据由 MDS(Metadata Server)管理,MDS 是有状态服务,需要部署和维护。JuiceFS 元数据由外部数据库管理,更灵活,也更容易迁移。

性能特征:CephFS 的数据路径完全绕过操作系统(直接写 OSD),在高并发随机小 IO 场景性能好。JuiceFS 的数据路径经过对象存储(PUT/GET),对小文件随机写入有额外的延迟(每个 4MB Block 需要一次对象存储 PUT),更适合大文件顺序读写(如模型训练数据集)。

云原生适配:JuiceFS 天然适配公有云(直接使用 S3/OSS 等云原生对象存储),CephFS 通常需要自建 Ceph 集群,在公有云上使用成本较高。


第 5 章 JuiceFS 的适用场景

5.1 AI 训练数据集存储

AI 模型训练的典型数据访问模式:大量小文件(图片、文本样本)或大文件(视频、音频),以顺序读为主,多机并发访问。JuiceFS 的 Block Cache 可以在训练节点的本地 SSD 上缓存热门数据,降低对象存储的访问延迟和费用。

典型架构:训练数据存储在 S3(低成本),通过 JuiceFS 挂载到 Kubernetes Pod 上,FUSE 自动管理缓存。

5.2 Hadoop 生态的替代存储

JuiceFS 提供了兼容 HDFS API 的 Hadoop SDK,Spark、Flink、Hive 等框架无需修改代码即可将 JuiceFS 作为存储后端(替换 hdfs:// 路径为 jfs://)。

对于希望从 HDFS 迁移到对象存储但不想放弃 POSIX 语义的团队,JuiceFS 是理想的过渡层。

5.3 Kubernetes CSI 持久化存储

JuiceFS 提供了 Kubernetes CSI Driver,以 PersistentVolume 的形式为 Pod 提供文件系统存储。相比 NFS,JuiceFS 的性能更好(分布式 Block Cache);相比 Ceph CSI,运维更简单(无需自建 Ceph 集群)。


第 6 章 小结

JuiceFS 的核心创新是将文件系统的元数据和数据存储完全解耦,并让每一层都由最擅长该任务的系统来承担:

  • 元数据 → 低延迟的 KV 数据库(Redis/TiKV)
  • 数据 → 低成本的对象存储(S3/OSS/MinIO)

这种设计使 JuiceFS 在无需维护任何专用服务端进程的情况下,提供完整的 POSIX 文件系统语义,兼容 Hadoop 生态,并能在云原生环境(Kubernetes)中无缝运行。

它不是 HDFS 或 CephFS 的功能上的替代,而是在”云原生 + 低运维成本 + 对象存储后端”这个维度上的独特定位。


延伸阅读


思考题

  1. JuiceFS 将元数据和数据分离——元数据存储在 Redis/MySQL/TiKV 等引擎中,数据存储在对象存储(S3/OSS/MinIO)中。这种架构与传统分布式文件系统(如 CephFS,元数据和数据在同一集群)相比,在弹性扩展和运维复杂度方面有什么优势?元数据引擎的选择(Redis vs MySQL vs TiKV)对性能和可靠性有什么影响?
  2. JuiceFS 兼容 POSIX 文件系统语义——可以直接挂载为本地目录。但对象存储的特点是’不可变对象’——没有原地修改,只有覆盖写。JuiceFS 如何在对象存储之上实现 POSIX 的随机写入(seek + write)?数据分块(默认 4MB Chunk → 64KB Block)的设计如何支持部分更新?
  3. JuiceFS 在机器学习训练场景中被广泛使用——多个 GPU 节点同时读取训练数据。JuiceFS 的客户端缓存(本地 SSD 缓存热数据)如何提升读取性能?缓存一致性如何保证——如果数据源被更新,缓存何时失效?