缓存

为了提高性能,JuiceFS 实现了多种缓存机制来降低访问的时延和提高吞吐量,包括元数据缓存、数据缓存,以及多个客户端之间的缓存共享。

元数据缓存

JuiceFS 支持在内核和客户端内存中缓存元数据以提升元数据的访问性能。

内核元数据缓存

在内核中可以缓存三种元数据:属性(attribute)、文件项(entry)和目录项(direntry),它们可以通过如下三个参数控制缓存时间:

  1. --attrcacheto=ATTRCACHETO
  2. 属性缓存时间,默认 1
  3. --entrycacheto=ENTRYCACHETO
  4. 文件项缓存时间,默认 1
  5. --direntrycacheto=DIRENTRYCACHETO
  6. 目录项缓存时间,默认 1

默认会缓存属性、文件项和目录项,保留 1 秒,以提高 lookup 和 getattr 的性能。

客户端内存元数据缓存

为了减少客户端和元数据服务之间频繁的列表和查询操作,客户端可以把经常访问的目录完整地缓存在客户端内存中,可以通过如下的参数开启:

  1. --metacache 在客户端中缓存元数据

开启后,被列表或者频繁查询的目录会被在客户端内存中缓存 5 分钟,对缓存目录的所有修改都会使缓存失效以保障一致性。 lookup、getattr、access、open 都能有效地使用这些缓存来提升性能。

此外,客户端还会缓存符号链接的内容,因为符合链接不会被修改(覆盖已有符号链接时是创建新的文件),是一直有效的。

一致性保证

当只有一个客户端访问时,这些缓存的元数据能够根据当前客户端的访问操作自动失效,不会影响数据一致性。

当多个客户端同时使用时,内核中缓存的元数据只能通过时间失效,客户端中缓存的元数据会根据所有客户端的修改自动失效,但是是异步的。

极端情况下可能出现在 A 机器做了修改操作,再去 B 机器访问时,B 机器还未能看到更新的情况。

数据缓存

JuiceFS 对数据也提供多种缓存机制来提高性能,包括内核中的页缓存和客户端所在机器的本地缓存。

内核中数据缓存

对于已经读过的文件,内核会把它的内容自动缓存下来,下次再打开的时候,如果文件没有被更新,就可以直接从内核中的缓存读获得最好的性能。

在 JuiceFS 的元数据服务器中,会跟踪所有最近被打开的文件,同一个客户端要再次打开相同文件时,它会根据该文件是否被修改了告诉该客户端是否可以使用内核中缓存的数据,以保证客户端能够读到最新的数据。

当重复读 JuiceFS 中的同一个文件时,速度会非常快,延时可低至微秒,吞吐量可以到每秒几 GiB。

当前的 JuiceFS 客户端还未启用内核的写入缓存功能,所有来自应用的写操作(write)会直接通过 FUSE 传递到客户端。从 Linux 内核 3.15 开始,FUSE 支持 「writeback-cache 模式」,意味着 write() 系统调用通常可以非常快速地完成。你可以在执行 juicefs mount 命令时通过 -o writeback_cache 选项来开启 writeback-cache 模式。当频繁写入非常小的数据(如 100 字节左右)时,建议启用此挂载选项。

客户端读缓存

客户端会根据应用读数据的模式,自动做预读和缓存操作以提高顺序读的性能。

数据会缓存到本地文件系统中,可以是基于硬盘、SSD 或者内存的任意本地文件系统。

本地缓存可以通过以下参数来调整:

  1. --cache-dir=CACHEDIR
  2. 缓存目录,默认为 /var/jfsCache
  3. --cache-size=CACHESIZE
  4. 缓存空间大小,默认为 1GiB
  5. --free-space-ratio=<free_space_ratio>
  6. 缓存盘的最少剩余空间,默认是 0.2
  7. --cache-partial-only
  8. 是否只缓存小文件和随机读的部分,适合对象存储的吞吐比缓存盘还高的情况。默认为 false

JuiceFS 客户端会尽可能快地把从对象存储下载的数据(包括新上传的数据)写入到缓存目录中,不做压缩和加密。

因为 JuiceFS 会为所有写入对象存储的数据生成唯一的名字,而且所有对象不会被修改,因此不用担心缓存的数据的失效问题。缓存在使用空间到达上限时(或者磁盘空间快满时)会自动进行清理,目前的规则是根据写入时间和大小,优先清理更大和更老的文件。

数据的本地缓存可以有效地提高随机读的性能,建议使用更快的存储介质和更大的缓存空间来提升对随机读性能要求高的应用的性能,比如 MySQL、Elasticsearch、ClickHouse 等。

客户端写缓存

客户端会把应用写的数据缓存在内存中,当一个 chunk 被写满,或者应用强制写入(close() 或者 fsync()),或者一定时间之后再写入到对象存储中。 当应用调用 fsync() 或者 close() 时,客户端会等数据写入到对象存储并且通知元数据服务后才返回,以确保数据安全。在某些情况下,如果本地存储是可靠的,可以通过启用异步上传到对象的方式来提高性能,此时 close() 不会等待数据写入到对象存储, 而是写入到本地缓存目录就返回。

异步上传模式可以通过下面的参数启用:

  1. --writeback 将写文件先写入缓存再异步上传

当需要短时间写入大量小文件时,建议使用 --writeback 参数挂载以提高写入性能,写入完成之后再去掉它重新挂载。或者在有大量随机写时 (比如应用 MySQL 的增量备份时),也建议启用--writeback

这个选项尤其推荐使用跨公网的对象存储时使用,可以大大提高性能。

注意:在 --writeback 开启时,千万不能删除 /var/jfsCache/<fs-name>/rawstaging/ 中的内容,否则会导致数据丢失。

开启 --writeback 时,缓存本身的可靠性与数据写入的可靠性直接相关,对此要求高的场景应谨慎使用。

默认情况下 --writeback 不开启。

客户端缓存数据共享

当同一个集群的客户端需要反复访问同一个数据集时(比如机器学习时需要用同一个数据集反复训练),JuiceFS 提供了缓存共享功能可以有效地提升这个场景下的性能。它可以通过下面的命令启用:

  1. --cache-group=CACHEGROUP 相同组的客户端之间可以相互共享缓存的数据

对于同一个局域网内挂载了同一个文件系统的客户端,如果使用了相同的缓存组名,它们会把监听在内网 IP 的随机端口汇报给元数据服务器,进而发现其他的客户端,并通过内网通信。

当一个客户端需要访问某个数据块时,它会询问某个负责这个数据块的节点,从对方的缓存读(或者从对象存储读并写入缓存)。这些相同缓存组的客户端组成了一个一致性哈希(Consistent Hashing)的环,类似于 Memcached 的用法。在这个组内新加或者减少客户端时,只影响到少量数据块的缓存命中率。

缓存共享功能非常适合使用 GPU 集群进行深度学习训练的场景,通过把训练数据集缓存到集群所有节点的内存中,可以给提供非常高性能的访问,让 GPU 不会因为数据读取太慢而闲置。

独立缓存集群

如果计算集群是动态创建和伸缩的,可以通过创建独立的缓存集群的方法来给对象存储加速,它是在 缓存数据共享 的基础上,给计算集群增加挂载参数 --no-sharing 实现的。增加 --no-sharing 参数且拥有相同 --cache-group 配置的计算节点将不会参与建立缓存集群,仅会从缓存集群读取数据。

假设有一个动态的计算集群 A 和专门用来做缓存的集群 B,他们都需要加上相同的挂载参数 --cache-group=CACHEGROUP 来构建一个缓存组,其中集群 A 的节点挂载时需要加上 --no-sharing 参数,集群 B 需要配置足够多的缓存盘(建议用 SSD)并且需要足够高的网络带宽。

当集群 A 的应用读数据时,如果当前节点的内存和缓存盘上没有该缓存数据,它就会根据一致性哈希从集群 B 中选择一个节点来读取数据。

此时会有 3 级缓存:计算节点的系统缓存、计算节点的磁盘缓存和缓存集群 B 中某个节点的磁盘缓存(系统缓存也有效),可以根据具体应用的访问特点配置各个层级的缓存介质和空间大小。

当需要访问固定的数据集时,可以通过 juicefs warmup 将该数据集提前预热,以提升第一次访问数据时的性能。

新数据写入时,会由写入数据的节点直接写入到底层对象存储中。

常见问题

为什么我设置了缓存容量为 50 GiB,但实际占用了 60 GiB 的空间?

对同样一批缓存数据,很难精确计算它们在不同的本地文件系统上所占用的存储空间, 目前是通过累加所有被缓存对象的大小并附加固定的开销(4KiB)来估算得到的, 与 du 得到的数值并不完全一致。

当缓存目录所在文件系统空间不足时(少于 256MiB),客户端会尽量减少缓存使用量来防止缓存盘被写满。