近期,DeepSeek 开源了其文件系统 Fire-Flyer File System (3FS),使得文件系统这一有着 70 多年历时的“古老”的技术,又获得了各方的关注。在 AI 业务中,企业需要处理大量的文本、图像、视频等非结构化数据,还需要应对数据量的爆炸式增长,分布式文件系统因此成为 AI 训练的关键存储技术。
本文旨在通过深入分析 3FS 的实现机制,并与 JuiceFS 进行对比,以帮助用户理解两种文件系统的区别及其适用场景。同时,我们将探讨 3FS 中的值得借鉴的创新技术点。
01 架构对比
3FS
3FS (Fire-Flyer File System) 是一款高性能的分布式文件系统,专为解决 AI 训练和推理工作负载而设计,该系统使用高性能的 NVMe 和 RDMA 网络提供共享存储层。3FS 由 DeepSeek 在 2025 年 2 月开源。
3FS 主要包括以下模块:
- 集群管理服务(Cluster Manager)
- 元数据服务(Metadata Service)
- 存储服务(Storage Service)
- 客户端 (FUSE Client、Native Client)
所有模块通过 RDMA 网络通信。元数据服务和存储服务向集群管理服务发送心跳信号。集群管理服务负责处理成员变更,并将集群配置分发给其他服务和客户端。为了提高系统的可靠性和避免单点故障,会部署多个集群管理服务,其中一个被选为主节点。当主节点发生故障时,另一个管理器会被提升为主节点。集群配置通常存储在可靠的分布式服务中,例如 ZooKeeper 或 etcd。
当进行文件元数据操作(例如打开或创建文件/目录),请求被发送到元数据服务,以实现文件系统语义。元数据服务有多个,并且是无状态的,它们不直接存储文件元数据,而是依赖支持事务的键值数据库 FoundationDB 来存储这些数据。因此,客户端可以灵活地连接到任意元数据服务。这种设计使得元数据服务可以在没有状态信息的情况下独立运作,进而增强了系统的可伸缩性和可靠性。
每个存储服务管理若干本地 SSD,并提供 chunk 存储接口。存储服务采用 CRAQ ( Chain Replication with Apportioned Queries)来确保强一致性。3FS 中存储的文件被拆分为默认 512K 大小相等的块,并在多个 SSD 上进行复制,从而提高数据的可靠性和访问速度。
3FS 客户端提供两种接入方式: FUSE Client 和 Native Client。 FUSE Client 提供常见 POSIX 接口的支持,简单易用。Native Client 提供更高的性能,但是用户需要调用客户端 API ,具有一定的侵入性,下文我们还将对此作更详尽的解析。
JuiceFS
JuiceFS 是一个云原生分布式文件系统,其数据存储在对象存储中。社区版可与多种元数据服务集成,适用场景广泛,于 2021 年在 GitHub 开源。企业版专为高性能场景设计,广泛应用于大规模 AI 任务,涵盖生成式 AI、自动驾驶、量化金融和生物科技等。
JuiceFS 文件系统包括三部分组成:
- 元数据引擎:用于存储文件元数据,包括常规文件系统的元数据和文件数据的索引。
- 数据存储:一般是对象存储服务,可以是公有云的对象存储也可以是私有部署的对象存储服务。
- JuiceFS 客户端:提供 POSIX(FUSE)、Hadoop SDK、CSI Driver、S3 网关等不同的接入方式。
架构差异
从模块划分上看两个文件系统差异不大,都采用了元数据与数据分离的设计,各个模块功能也较类似。不同于 3FS 和 JuiceFS 企业版,JuiceFS 社区版兼容多种开源数据库存储元数据,对元数据的操作都封装在客户端,用户不需要再单独运维一个无状态的元数据服务。
存储模块
3FS 使用大量本地 SSD 进行数据存储,为了保证数据存储的一致性,3FS 使用 CRAQ 这一简洁的数据一致性算法 。几个副本被组成一个 Chain,写请求从 Chain 的 Head 开始,一直到达 Chain 的 Tail 时返回写成功应答。读请求可以发送到 Chain 的所有副本,如果读到脏节点的数据,该节点会联系 Tail 节点检查状态。如下图所示。
数据的写入是按顺序逐节点传递,因此会带来比较高的延时。如果 Chain 当中的某个副本不可用, 3FS 会把这个副本移到 Chain 的末尾,等副本可用的时候再做恢复。恢复的时候需要把整个 Chunk 的内容复制到这个副本,而非使用不可用期间的增量数据。如果要做到同步写所有副本和增量恢复数据,那写的逻辑会复杂非常多,比如 Ceph 使用 pg log 保证数据一致性。尽管 3FS 这种设计会导致写延迟,但是对于以读为主的 AI 应用场景,影响不大。
JuiceFS 利用对象存储作为数据存储解决方案,从而可享有对象存储带来的若干优势,如数据可靠性、一致性等。存储模块提供了一组用于对象操作的接口,包括 GET/PUT/HEAD/LIST 等,用户可以根据自己的需求对接具体的存储系统。比如不同云厂商的对象存储,也可以选择私有部署的对象存储比如 MinIO、Ceph RADOS 等系统。社区版 JuiceFS 提供本地缓存来应对 AI 场景下的带宽需求,JuiceFS 企业版使用分布式缓存满足更大的聚合读带宽的需求。
元数据模块
在 3FS 中,文件的属性以 KV 的形式存储在元数据服务中。该服务是一个无状态的高可用服务,依靠 FoundationDB 做支撑。FoundationDB 是 Apple 开源的优秀分布式 KV 数据库,具有很高的稳定性。FoundationDB 所有键值使用 Key 做全局排序,然后均匀拆分到不同的节点上。
为了优化 list 目录的效率,3FS 使用字符 “DENT” 前缀加父目录 inode 号和名字作为 dentry 的 Key。Inode 的 Key 是通过将 “INOD” 前缀与 inode ID 连接而构造的,其中 inode ID 采用小端字节序编码,以便将 inodes 分布到多个 FoundationDB 节点上。这个设计与 JuiceFS 使用的 TKV(Transactional Key-Value Database) 进行元数据服务的存储方式类似。
JuiceFS 社区版的元数据模块,与存储模块类似也提供一组操作元数据的接口,可以接入不同的元数据服务,比如 Redis,TiKV 等 KV 数据库,MySQL,PostgreSQL 等关系型数据库,也可以使用 FoundationDB。JuiceFS 企业版使用自研高性能元数据服务,可根据负载情况来平衡数据和热点操作,以避免大规模训练中元数据服务热点集中在某些节点的问题(比如因为频繁操作临近目录文件的元数据引起)。
客户端
3FS 的客户端除了提供 FUSE 操作外,还提供了一组 API 用于绕过 FUSE 直接操作数据,也就是 Native Client,接口的调用方式有点类似于 Linux AIO。这组 API 的作用是避免使用 FUSE 模块带来的数据拷贝,从而减少 I/O 延迟和对内存带宽的占用。下面将详细解析这组 API 如何实现用户进程与 FUSE 进程之间的零拷贝通信。
3FS 通过 hf3fs_iov
保存共享内存的大小,地址和其他一些属性,使用 IoRing
在两个进程间通信。
当用户调用接口,创建 hf3fs_iov
时,会在 /dev/shm
上分配内存,并创建一个指向这个共享内存的软链接,软链接的地址位于 /mount_point/3fs-virt/iovs/
,这是个虚拟目录。3FS FUSE 进程收到创建软链接请求,并且发现它的地址位于上述虚拟目录后,就会根据软链接的名字解析出这块共享内存的相关参数,并将内存的地址注册到所有 RDMA 设备(除了 IORing
)。ibv_reg_mr
返回的结果被存在 RDMABuf::Inner
数据结构中,用于后续发送 RDMA 请求。
同时,IORing
的内存也使用 hf3fs_iov
保存,只是在创建对应的软链接时,文件名中会有更多的 IORing
相关的信息。如果 FUSE 进程发现这个内存是用于创建 IORing
,也会在它的进程内创建对应的 IORing
。这样设置之后,用户进程和 FUSE 进程就可以访问相同的 IORing
了。
进程间协作方面,3FS 在 /mount_point/3fs-virt/iovs/
目录中创建 3 个不同的虚拟文件用于共享 3 个不同优先级的提交信号量 (submit sem ),用户进程将请求放到 IORing
后使用这些信号量通知 FUSE 进程有新的请求。 IORing
尾部包含请求完成信号量,FUSE 进程通过调用 sem_post
通知用户进程 IORing
上有新的请求完成。以上整个机制确保了两个进程间的高效数据通信和操作同步。
3FS 的 FUSE 客户端实现了文件和目录的基本操作,而 JuiceFS FUSE 客户端的实现更加全面。比如,在 3FS 文件系统中文件的长度是最终一致的,这意味着在写的过程中用户可能访问到不正确的文件长度。而 JuiceFS 在每次成功上传对象后会立即更新文件长度。此外,JuiceFS 还提供了以下这些常用的高级文件系统功能:
- 支持 BSD 锁(flock)和 POSIX 锁(fcntl)
- 支持
file_copy_range
接口 - 支持
readdirplus
接口 - 支持
fallocate
接口
除了 FUSE 客户端,JuiceFS 还提供 Java SDK,S3 Gateway,CSI Driver 等接入方式。企业版还提供 Python SDK,Python SDK 将 JuiceFS 客户端在用户进程中运行,避免了通过 FUSE 导致的额外性能开销。具体见文档:Python SDK。
02 文件分布对比
3FS 文件分布
3FS 将每个文件分成固定长度的 chunk,每个 chunk 位于一个上文提到的链上( CRAQ 算法)。用户使用 3FS 提供的一个脚本,生成一个 chain table。然后将这个表提交到元数据服务。创建新文件时,系统会从表中选取特定数量的 chain (数量由 stripe 定义),并将这些 chain 的信息存入文件的元数据中。
因为 3FS 中的 chunk 是固定的,客户端只需要获取一次 inode 的 chain 信息,就可以根据文件 inode 和 I/O 请求 的 offset,length 计算出这个请求位于哪些 chunk 上,从而避免了每个 I/O 都从数据库查询的需求。可以通过 offset/chunk_size
得到 chunk 的索引。 而 chunk 所在的 chain 的索引就是 chunk_id%stripe
。有了 chain 的索引就可以得到 chain 的详细信息(比如这个 chain 由哪些 target 组成)。然后,客户端根据路由信息将 I/O 请求发送到相应的存储服务。存储服务收到写请求后以 copy-on-write (COW)的方式将数据写入新的位置。原来的数据在引用数据清零前仍然是可读的。
为了应对数据不平衡问题,每个文件的第一个 chain 按照轮询( round roubin) 的方式选择。比如当 stripe 为 3 时,创建一个文件,其选择的 chain 为:chain0,chain1,chain2。那么下一个文件的 chain 为:chain1,chain2 和 chain3。系统会将选择的 3 个 chain 做随机排序,然后存储到元数据中。下图为 stripe 为 3 时一个文件的分布示例,chain 随机排序后的顺序是:1,3,2。
JuiceFS 文件分布
JuiceFS 按照 Chunk、Slice、Block 的规则进行数据块管理。每个 Chunk 的大小固定为 64M,主要用于优化数据的查找和定位。实际的文件写入操作则在 Slice 上执行,每个 Slice 代表一次连续的写入过程,属于特定的 Chunk,并且不会跨越 Chunk 的边界,因此长度不超过 64M。Chunk 和 Slice 主要是逻辑上的划分,而 Block(默认大小为 4M)则是物理存储的基本单位,用于在对象存储和磁盘缓存中实现数据的最终存储。更多细节可以参考官网介绍。
JuiceFS 中的 Slice 是在他文件系统中不常见的一个结构。主要功能是记录文件的写入操作,并在对象存储中进行持久化。对象存储不支持原地文件修改,因此,JuiceFS 通过引入 Slice 结构允许更新文件内容,而无需重写整个文件。这与 Journal File System 有些类似,其中写入操作仅创建新对象,而不是覆盖现有对象。修改文件时,系统会创建新的 Slice,并在该 Slice 上传完毕后更新元数据,从而将文件内容指向新的 Slice。被覆盖的 Slice 内容随后通过异步压缩过程从对象存储中删除,导致在某些时刻对象存储的使用量会暂时超过文件系统实际使用量。
此外,JuiceFS 的所有 Slice 均为一次性写入,这减少了对底层对象存储一致性的依赖,并大大简化了缓存系统的复杂度,使数据一致性更易于保证。这种设计还为实现文件系统的零拷贝语义提供了便利,支持如 copy_file_range 和 clone 等操作。
03 3FS RPC (Remote Procedure Call) 框架
3FS 使用 RDMA 作为底层网络通信协议,目前 JuiceFS 尚未支持,下面对此做一些分析。
3FS 通过实现一个 RPC 框架,来完成对底层 IB 网络的操作。除了网络操作外,RPC 框架还提供序列化,小包合并等能力。因为 C++ 不具有反射能力,所以 3FS 还通过模版实现了一个反射库,用于序列化 RPC 使用的 request、response 等数据结构。需要被序列化的数据结构只需要使用特定的宏定义需要序列化的属性。RPC 调用都是异步完成的,所以序列化后的数据只能从堆上分配,等待调用完成后再释放。为了提高内存的分配和释放速度,分配对象都使用了缓存。3FS 的缓存有两部份组成,一个 TLS 队列和一个全局队列。 从 TLS 队列获取缓存时不需要加锁;当 TLS 缓存为空时就得加锁,从全局队列中获取缓存。所以在最优情况下,获取缓存是不需要加锁的。
与 I/O 请求的负载不同,缓存对象的内存都未注册到 RDMA 设备中。因此,当数据到达 IBSocket 后,会被拷贝到一个在 IB 设备注册过的缓冲区中。多个 RPC 请求可能被合并为一个 IB 请求发送到对端。下图为 FUSE Client 调用 Meta 服务的 RPC 过程。
04 特性对比
对比项 | 3FS | JuiceFS 社区版 | JuiceFS 企业版 |
---|---|---|---|
元数据 | 无状态元数据服务+FoundationDB | 独立数据库服务 | 自研高性能分布式元数据引擎(可横向扩展) |
数据存储 | 自主管理 | 使用对象存储 | 使用对象存储 |
冗余保护 | 多副本 | 对象存储提供 | 对象存储提供 |
数据缓存 | 无缓存 | 本地缓存 | 自研高性能多副本分布式缓存 |
数据加密 | 不支持 | 支持 | 支持 |
数据压缩 | 不支持 | 支持 | 支持 |
配额管理 | 不支持 | 支持 | 支持 |
网络协议 | RDMA | TCP | TCP |
快照 | 不支持 | 支持克隆 | 支持克隆 |
POSIX ACL | 不支持 | 支持 | 支持 |
POSIX 兼容性 | 少量子集 | 完全兼容 | 完全兼容 |
CSI 驱动 | 没有官方支持 | 支持 | 支持 |
客户端 | FUSE + Native Client | POSIX(FUSE)、Java SDK、S3 网关 | POSIX(FUSE)、Java SDK、S3 网关、Python SDK |
多云镜像 | 不支持 | 不支持 | 支持 |
跨云和跨区数据复制 | 不支持 | 不支持 | 支持 |
主要维护者 | DeepSeek | Juicedata | Juicedata |
开发语言 | C++, Rust (本地存储引擎) | Go | Go |
开源协议 | MIT | Apache License 2.0 | 商业软件 |
05 总结
大规模 AI 训练中最主要的需求是高读带宽,为此 3FS 采用了性能优先的设计策略,将数据存储在高速磁盘上,并且用户需要自行管理底层数据存储。这种方法提升了性能,但成本较高,维护也更繁重。此外,为了充分发挥底层硬件的性能,其架构实现了客户端到网卡的零拷贝,利用共享内存和信号量减少 I/O 延迟和内存带宽占用。此外,通过带 TLS 的 I/O buffer pool 和合并网络请求,3FS 增强了小 I/O 和文件元数据操作的能力,并引入了性能更优的 RDMA 技术。我们将继续关注 3FS 在性能优化方面的进展,并探索如何将这些技术应用于我们的场景中。
JuiceFS 使用对象存储作为底层数据存储,用户因此可大幅降低存储成本并简化维护工作。为了满足 AI 场景的对读性能的需求,JuiceFS 企业版引入了分布式缓存、分布式元数据服务和 Python SDK,从而提高文件系统的性能和扩展能力,并同时兼顾低存储成本。在接下来发布的 v5.2 企业版中,在 TCP 网络中实现了零拷贝,进一步提升数据传输效率。
JuiceFS 提供完整的 POSIX 兼容性和成熟活跃的开源生态,适应更广泛的使用场景,并支持Kubernetes CSI,极大简化了云平台的部署和运维工作。此外,JuiceFS 还提供了 Quota、安全管理和数据灾备等多项企业级管理功能,让企业可以更便捷地在生产环境中部署和应用 JuiceFS。
希望这篇内容能够对你有一些帮助,如果有其他疑问欢迎加入 JuiceFS 社区与大家共同交流。