技术架构
如下图所示,JuiceFS 由三大组件构成:JuiceFS 客户端、元数据引擎和数据存储。后续章节将逐一详细介绍。
JuiceFS 客户端(Client)
JuiceFS 采用「富客户端」设计,因此 JuiceFS 客户端是文件系统中的重要组成部分:所有文件读写,包括碎片合并、回收站文件过期删除等后台任务,均在客户端中发起执行。因此 JuiceFS 客户端需要同时与元数据引擎和数据存储进行通信。服务端(也就是下面将会介绍的「元数据引擎」)不会也不能直接修改任何文件系统数据。
客户端支持众多接入方式,包括:
- 通过 FUSE:JuiceFS 文件系统能够以 POSIX 兼容的方式挂载到服务器,将海量云端存储直接当做本地存储来使用。点击此处查看使用详情。
- 通过 Python SDK:在无法通过 FUSE 挂载,或需要在 Python 进程中直接访问文件系统的场景,可以使用 Python SDK 直接读写文件系统。此外,Python SDK 原生实现了 fsspec 便于接入 Ray 等框架。点击此处查看使用详情。
- 通过 Windows 客户端:获得接近本地文件系统的体验。点击此处查看使用详情。
- 通过 Hadoop Java SDK:JuiceFS 文件系统能够直接替代 HDFS,为 Hadoop 提供低成本的海量存储。点击此处查看使用细节。
- 通过 Kubernetes CSI 驱动:JuiceFS 文件系统能够直接为 Kubernetes 提供海量存储。点击此处查看 JuiceFS CSI 文档。
- 通过 S3 网关:使用 S3 作为存储层的应用可直接接入,同时可使用 AWS CLI、s3cmd、MinIO client 等工具访问 JuiceFS 文件系统,已有的支持 S3 应用可直接无缝接入。点击此处查看使用详情。
- 通过 WebDAV 服务:以 HTTP 协议,以类似 RESTful API 的方式接入 JuiceFS 并直接操作其中的文件。点击此处查看使用详情。
元数据引擎 (Metadata Engine)
如上架构图所示,JuiceFS 企业版和云服务采用 Juicedata 自研的高性能元数据引擎。该元数据引擎存储文件元数据,包含以下内容:
- 常规文件系统的元数据:文件名、文件大小、权限信息、创建修改时间、目录结构、文件属性、符号链接、文件锁等。
- JuiceFS 独有的元数据:文件的 Chunk 及 Slice 映射关系、客户端会话等。
Juicedata 已经在大多数公有云都部署了元数据服务,供云服务用户开箱即用。作为 JuiceFS 云服务用户,您将通过公网使用元数据服务(建议在同一区域以获得最佳性能)。若您大规模使用 JuiceFS 并需要更低的访问延迟,可联系 Juicedata 团队,通过 VPC 对等连接提供私有网络支持。
元数据服务是一个基于 Raft 协议的高可用集群,所有元数据操作均以变更日志(changelog)形式进行追加(这也让类似「实时数据保护」这样的高级数据安全功能成为可能)。一个 Raft 组由 3 个节点组成,包含 Leader 和 Follower 两种角色,通过 Raft 共识算法进行数据复制,确保元数据的强一致性和服务的高可用。
一个 Raft 组构成一个 JuiceFS 元数据分区。目前单分区元数据服务适用于索引节点(inode)不超过 2 亿的场景。如需支持更大量级的数据规模,可使用我们的多分区元数据服务(目前仅在私有部署提供),通过增加分区来实现元数据服务的水平拓展 。
在多分区模式下,JuiceFS 能在单一命名空间支撑千亿级文件存储,每个分区内的架构与单分区相同。不同分区的节点可以混部在相同的机器上,分区数支持动态调整,分区间的元数据也支持动态迁移(自动或者手动负载均衡),来满足高负载业务访问场景,从而有效避免数据热点引发的性能问题。这一系列功能均可通过 JuiceFS Web 控制台进行管理和监控,满足企业运维需求。而对于 JuiceFS 客户端,访问多分区元数据服务也与单分区无异,能同时读写不同分区的数据,并自动感知分区拓扑变化。
数据存储(Data Storage)
传统文件系统将文件数据和元数据都存储在本地磁盘上。而 JuiceFS 则将文件数据存储在对象存储中,将对应的元数据存储在元数据引擎中。前文提到 JuiceFS 客户端会分别与元数据引擎和数据存储进行通信,而元数据引擎则与对象存储完全解耦,既不关心也不会访问对象存储服务,所有实际文件数据均存放在您选择的对象存储上。
您可以使用公有云服务提供的对象存储,也可以使用自建的解决方案。JuiceFS 支持几乎所有类型的对象存储,包括 Amazon S3、Google Cloud Storage(GCS)、Azure Blob 等公有云选项,以及 OpenStack Swift、Ceph、MinIO 等私有化方案。这些都在「设置对象存储」中有详细介绍。
JuiceFS 如何存储文件
在 JuiceFS 处理实际文件数据时,Chunk、Slice、Block 是三个重要的概念。
每个文件由一个或多个「Chunk」组成,每个 Chunk 最大为 64 MiB。不论文件有多大,所有读写操作都会根据其偏移量(也就是产生读写操作的文件位置)来定位到对应的 Chunk。正是这种分而治之的设计,让 JuiceFS 面对大文件时也有优秀的性能。只要文件总长度保持不变,不论经历多少修改写入,文件的 Chunk 切分都是固定的。
Chunk 的存在是为了优化查找定位,实际的文件写入则在「Slice」上进行。在 JuiceFS 中,一个 Slice 代表一次连续写入,隶属于某个 Chunk,并且不能跨越 Chunk 边界,因此 Slice 长度也不会超 64MiB。
举例说明,如果一个文件是由一次连贯的顺序写生成,那么每个 Chunk 中仅包含一个 Slice。上方的示意图就属于这种情况:顺序写入一个 160MiB 的文件,最终会产生三个 Chunk,而每个 Chunk 仅包含一个 Slice。
文件写入会产生 Slice,而调用 flush 则会将这些 Slice 持久化。flush 可以被用户显式调用,就算不调用,JuiceFS 客户端也会自动在恰当的时机进行 flush,防止缓冲区被写满。持久化到对象存储时,为了能够尽快写入,会将 Slice 进一步拆分为多个「Block」(默认最大 4MiB)并利用多线程并发写入来提升写性能。上边介绍的 Chunk 和 Slice 其实都是逻辑数据结构,而 Block 则是最终的物理存储形式,是对象存储和磁盘缓存的最小存储单元。
将文件写入 JuiceFS 后,您无法在对象存储中直接找到原始文件。实际上存储桶中只有一个 chunks 目录和一系列以数字编号的目录和文件,让人不禁疑惑「我的文件到底去了哪儿?」。这些以数字编号的对象存储文件正是经过 JuiceFS 拆分存储的 Block。而这些 Block 与 Chunk、Slice 的对应关系,以及其他元数据信息(比如文件名、大小等属性)均存储在元数据引擎中。这种解耦设计让 JuiceFS 文件系统得以高性能运作。
回到逻辑数据结构的话题,如果文件并不是由连贯的顺序写生成,而是多次追加写,每次追加均调用 flush 触发写入上传,就会产生多个 Slice。如果每次追加写入的数据量不足 4MiB,那么最终存入对象存储的数据块,也会是一个个小于 4MiB 的 Block。
取决于写入模式,Slice 的排列模式可以是多种多样的:
- 如果文件在相同区域被反复修改,Slice 之间会发生重叠。
- 如果在互不重合的区域进行写入,Slice 中间会有间隔。
但不论 Slice 的排 列有多复杂,当读取文件时,对于每一处文件位置,都会读到该位置最新写入的 Slice,用下图可以更加直观地理解:Slice 虽然会相互堆叠,但读文件一定是「从上往下看」,因此一定会看到该文件的最新状态。
正是由于 Slice 会相互覆盖,JuiceFS 在 Chunk 与 Slice 的引用关系中标记了各个 Slice 的有效数据偏移范围(内部实现可以参考社区版文档,企业版的设计类似),用这种方式告诉文件系统,每一个 Slice 中的哪些部分是有效的数据。
但也不难想象,读取文件需要查找「当前读取范围内最新写入的 Slice」,在上图所示的大量堆叠 Slice 的情况下,这样的反复查找将会显著影响读性能,我们称之为文件「碎片化」。碎片化不仅影响读性能,还会在各个层面(对象存储、元数据)增加空间占用。因此每当写入发生时,客户端都会判断文件的碎片化情况,并在后台任务中运行碎片合并,将同一个 Chunk 内的所有 Slice 合并为一。
JuiceFS 的存储设计,还有着以下技术特点:


