简介

JuiceFS是一个基于 Apache2.0开放源代码的、以云计算为基础的、高性能的、分享的文档系统。它具有完整的 POSIX兼容性,可以把大部分的物件储存存取到本机的大量的本机硬盘,也可以在跨平台、跨地区的多个主机上进行读取和写入。

JuiceFS是将“资料”和“元资料”分开存放的体系结构,使档案管理的分布化。通过 JuiceFS来保存数据,数据自身将保存在诸如亚马逊S3之类的目标存储器中,并且相应的元数据可以根据需要保存在不同的数据库中,比如 Redis, MySQL, TiKV, SQLite等等。

JuiceFS提供多种 API,可以管理、分析、归档、备份等多种格式的数据,无需更改编码,无缝对接大数据、机器学习、人工智能等应用,提供海量、弹性、低价的高性能数据。操作者不必为可用性、灾难恢复、监控和扩展等问题而苦恼,集中精力发展和提高研究的效能。而操作流程的精简,使得运行人员更易于转变为 DevOps小组。

核心特性

  1. POSIX兼容性:可以象本地的档案系统那样,与现有的程序进行无缝的连接,没有商业入侵;
  2. HDFS兼容性:完全与 HDFS API相结合,具有更好的元资料处理能力;
  3. S3兼容性:S3网关,为S3协议的兼容性提供了接入界面;
  4. 原生云计算:在 Kubernetes中,可以方便地通过 CSI驱动程序来实现 JuiceFS
  5. 分布式设计:可以在数千个不同的伺服器中,使用高效的并行读取和写入,并进行数据的分享;
  6. 强大的连贯性:所有的伺服器都可以即时看到已证实的档案修正,确保了强大的连贯性;
  7. 强大的表现:微秒级的延时,几乎没有限制的吞吐能力(视物件储存大小而定),检视效能的测试;
  8. 资料保密:支援传送中的密码与静态密码,以了解详细资料;
  9. Fcntl:支援 BSD锁定及 POSIX锁定;
  10. 数据压缩:采用LZ4和 ZStandard压缩技术,节约了内存.

应用场景

JuiceFS 为海量数据存储设计,可以作为很多分布式文件系统和网络文件系统的替代,特别是以下场景:

  • 大数据分析:HDFS 兼容,没有任何特殊 API 侵入业务;与主流计算引擎(Spark、Presto、Hive 等)无缝衔接;无限扩展的存储空间;运维成本几乎为 0;完善的缓存机制,高于对象存储性能数倍。
  • 机器学习:POSIX 兼容,可以支持所有机器学习、深度学习框架;共享能力提升团队管理、使用数据效率。
  • 容器集群中的持久卷:Kubernetes CSI 支持;持久存储并与容器生存期独立;强一致性保证数据正确;接管数据存储需求,保证服务的无状态化。
  • 共享工作区:可以在任意主机挂载;没有客户端并发读写限制;POSIX 兼容已有的数据流和脚本操作。
  • 数据备份:在无限平滑扩展的存储空间备份各种数据,结合共享挂载功能,可以将多主机数据汇总至一处,做统一备份。

数据隐私

JuiceFS 是开源软件,你可以在 GitHub 找到完整的源代码。在使用 JuiceFS 存储数据时,数据会按照一定的规则被拆分成数据块并保存在你自己定义的对象存储或其它存储介质中,数据所对应的元数据则存储在你自己定义的数据库中。

核心架构

JuiceFS 文件系统由三个部分组成:

  • JuiceFS 客户端:协调对象存储元数据存储引擎,以及 POSIX、Hadoop、Kubernetes CSI Driver、S3 Gateway 等文件系统接口的实现;
  • 数据存储:存储数据本身,支持本地磁盘、公有云或私有云对象存储、HDFS 等介质;
  • 元数据引擎:存储数据对应的元数据(metadata)包含文件名、文件大小、权限组、创建修改时间和目录结构,支持 Redis、MySQL、TiKV 等多种引擎;

JuiceFS是一个独立的档案系统,它将会独立地将资料和相应的元资料储存到物件存贮器中,并储存于元资料服务的程式码中。在储存上, JuiceFS可以提供任何公开的对象储存,并提供私有的物件储存,如 OpenStack Swift, Ceph, MinIO。

在元资料储存上, JuiceFS是一个多引擎的架构,它现在已经可以使用 Redis, TiKV, MySQL/MariaDB, PostgreSQL, SQLite等多种类型的元资料服务,并且会有更多的应用。请提供 Issue回馈您的需要。

关于文件系统界面的实施:

  • JuiceFS的 FUSE可以与 POSIX标准相结合,可以将大量的云存储作为一个局部存储。
  • JuiceFS的 Hadoop Java SDK可以取代 HDFS,从而为 Hadoop的 Hadoop提供大量的低开销的数据。
  • 有了 Kubernetes CSI驱动程序, JuiceFS可以为 Kubernetes提供大量的存储空间。
  • 在S3网关中,将S3用作存储器的程序可以被直接存取,而 JuiceFS的档案系统可以利用 AWS CLI,s3cmd, MinIO客户机等。

如何存储文件

档案系统是使用者与硬碟互动的中介,使档案能正确储存于硬碟中。Windows中常见的档案系统有FAT32, NTFS, Linux中常见的是Ext4, XFS, Btrfs等。

JuiceFS是一种具有强一致性和高性能的文档管理方式的强大的文档管理功能。JuiceFS是将数据格式格式保存到一个物件(云端储存),而与之相适应的档案系统则会储存于诸如 Redis之类的资料库中。

所有存储在 JuiceFS中的档案都会分成“Chunk”,其最大的内存容量为64 MiB。每一个 Chunk包括一个或更多的“Slice”, Slice的长短视写文档的不同而不同。每一个 Slice都被分成了“Block”,缺省值为4 MiB。最终,将块储存在物件储存区。同时, JuiceFS还会把元资料的元资料如 Chunks、 Slices、块等储存到元资料引擎中。

使用 JuiceFS,文件最终会被拆分成 Chunks、Slices 和 Blocks 存储在对象存储。因此,你会发现在对象存储平台的文件浏览器中找不到存入 JuiceFS 的源文件,存储桶中只有一个 chunks 目录和一堆数字编号的目录和文件。不要惊慌,这正是 JuiceFS 文件系统高性能运作的秘诀!

除了挂载文件系统以外,你还可以使用 JuiceFS S3 网关,这样既可以使用 S3 兼容的客户端,也可以使用内置的基于网页的文件管理器访问 JuiceFS 存储的文件。

写入流程

JuiceFS在大的文档中进行了多层分解(参考 JuiceFS是怎样保存这些文档的),从而增加了读取和写入的速度。当写要求被执行时, JuiceFS首先把资料写到客户机的记忆缓冲里,然后以 Chunk/Slice的方式来管理。Chunk是一个连续的逻辑单位,它按照一个文档中的 offset的尺寸划分成64 MiB,并且在各个 Chunk间都被完全隔绝。在每一个 Chunk中都可以按照实际的要求将其分解为 Slices;如果一个新的写入要求是与现有的 Slice相关联或者是重复的,那么它将会被直接地在 Slice上进行升级,或者将会被建立一个新的 Slice。Slice是开始进行数据持久的一个逻辑部件,它首先把一个或多个缺省4 MiB的数据分割为一个或多个相继的块,然后上载到一个物体,每块都有一个目标;元资料再次被重新修改,并写出一个新的 Slice。很明显,在使用连续写入的时候, Slice是不断增加的,并且最后一次 flush;在这个时候,可以将目标的存储器的写能力最大化。以一次简单的 JuiceFS 基准测试为例,其第一阶段是使用 1 MiB IO 顺序写 1 GiB 文件,数据在各个组件中的形式如下图所示:

注意:图中的压缩和加密默认未开启。欲启用相关功能需要在 format 文件系统的时候添加 --compress value 或 --encrypt-rsa-key value 选项。

这里再放一张测试过程中用 stats 命令记录的指标图,可以更直观地看到相关信息:

上图中第 1 阶段:

  • 对象存储写入的平均 IO 大小为 object.put / object.put_c = 4 MiB,等于 Block 的默认大小
  • 元数据事务数与对象存储写入数比例大概为 meta.txn : object.put_c ~= 1 : 16,对应 Slice flush 需要的 1 次元数据修改和 16 次对象存储上传,同时也说明了每次 flush 写入的数据量为 4 MiB * 16 = 64 MiB,即 Chunk 的默认大小
  • FUSE 层的平均请求大小为约 fuse.write / fuse.ops ~= 128 KiB,与其默认的请求大小限制一致

相较于顺序写来说,大文件内随机写的情况要复杂许多;每个 Chunk 内可能存在多个不连续的 Slice,使得一方面数据对象难以达到 4 MiB 大小,另一方面元数据需要多次更新。同时,当一个 Chunk 内已写入的 Slices 过多时,会触发 Compaction 来尝试合并与清理这些 Slices,这又会进一步增大系统的负担。因此,JuiceFS 在此类场景下会比顺序写有较明显的性能下降。

小文件的写入通常是在文件关闭时被上传到对象存储,对应 IO 大小一般就是文件大小。从上面指标图的第 3 阶段(创建 128 KiB 小文件)中也可以看到:

  • 对象存储 PUT 的大小就是 128 KiB
  • 元数据事务数大致是 PUT 计数的两倍,对应每个文件的一次 Create 和一次 Write

需要指出的是, JuiceFS会在一个 Block以下的物件上下载时,试图将其写入到一个原生 Cache (cache-dir所规定,可以是存储器或硬体),以便提高读取的速度。从指数图表还可以看出,当一个小型的档案被建立时, blockcache下的写带宽是一样的,而在第四个步骤里,大多数都是在缓存上,因此,读起来非常快速。

一般情况下, JuiceFS的 Write延迟很小(只有数十微秒),而实际上,它是通过一个内置的程序(一个 Slice太大, Slice太多,缓存太久等等)或者是一个程序启动(文件关闭,调用 fsync等等)。缓冲中的资料必须在保存之后才会被解压,所以在写并发很大或是物件储存能力不够时,会占据缓冲空间,造成写堵塞。特别是,缓冲的尺寸是通过装入的参数–buffer-size来规定的,缺省是300 MiB;实际上,这个时间的数值可以在指数图表中的 usage. buf栏中找到。JuiceFS客户端在使用超出临界点的情况下,会自动增加大约10毫秒的延迟来降低写的速率;如果使用的数量超出了临界点的两次,将会出现一个新的写中止,直到缓冲被解除为止。所以,当 Write延迟增加, Buffer超出临界点很久后,您就必须要试着增加更大的buffer-size。此外,增加-max-uploads (最多将被上传至物件储存的并行数目,缺省值为20)可以提高向物件储存的写入频宽,因此加速了缓冲的释放。

回写(Writeback)模式

当对数据的一致性和可靠性要求并不高时,还可以在挂载时添加 --writeback 以进一步提升系统性能。回写模式开启后,Slice flush 仅需写到本地 Staging 目录(与 Cache 共享)即可返回,数据由后台线程异步上传到对象存储。请注意,JuiceFS 的回写模式与通常理解的先写内存不同,是需要将数据写入本地 Cache 目录的(具体的行为根据 Cache 目录所在硬件和本地文件系统而定)。换个角度理解,此时本地目录就是对象存储的缓存层。

回写模式开启后,还会默认跳过对上传对象的大小检查,激进地尽量将所有数据都保留在 Cache 目录。这在一些会产生大量中间文件的场景(如软件编译等)特别有用。此外,JuiceFS v0.17 版本还新增了 --upload-delay 参数,用来延缓数据上传到对象存储的时间,以更激进地方式将其缓存在本地。如果在等待的时间内数据被应用删除,则无需再上传到对象存储,既提升了性能也节省了成本。同时相较于本地硬盘而言,JuiceFS 提供了后端保障,在 Cache 目录容量不足时依然会自动将数据上传,确保在应用侧不会因此而感知到错误。这个功能在应对 Spark shuffle 等有临时存储需求的场景时非常有效。

读取流程

JuiceFS 在处理读请求时,一般会按照 4 MiB Block 对齐的方式去对象存储读取,实现一定的预读功能。同时,读取到的数据会写入本地 Cache 目录,以备后用(如指标图中的第 2 阶段,blockcache 有很高的写入带宽)。显然,在顺序读时,这些提前获取的数据都会被后续的请求访问到,Cache 命中率非常高,因此也能充分发挥出对象存储的读取性能。此时数据在各个组件中的流动如下图所示:

注意:读取的对象到达 JuiceFS Client 后会先解密再解压缩,与写入时相反。当然,如果未启用相关功能则对应流程会直接跳过。

在执行大型档案中的任意小型 IO时, JuiceFS的这个方法并没有什么效果,相反,由于经常使用内存缓存和读扩展,从而减少了系统的使用。遗憾的是,在这样的情况下,普通的高速缓存政策很少能带来高回报。目前可以思考的一个问题就是尽量提高高速缓存的总能力,以便使所需要的资料可以被高速缓存;而另外一种方法是,可以将高速缓存(cache-size0)直接关掉,并且在最大程度上改善了 Object的读出能力。

而小型的档案就更容易了,一般只需要一次要求就能把整份档案全部读出来。因为在写小型的档案时,会被直接缓冲,所以像 JuiceFS bench这样的存取方式,会很快被写入到本地 Cache的目录,所以它的表现相当不错。