Skip to main content

缓存

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

元数据缓存

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

内核元数据缓存

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

--attrcacheto=ATTRCACHETO
属性缓存时间,默认 1 秒
--entrycacheto=ENTRYCACHETO
文件项缓存时间,默认 1 秒
--direntrycacheto=DIRENTRYCACHETO
目录项缓存时间,默认 1 秒

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

客户端内存元数据缓存

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

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

开启后,被列表或者频繁访问的目录会在客户端内存中缓存 5 分钟(可通过 --metacacheto 选项调整),对缓存目录的所有修改都会使缓存失效以保障一致性。lookupgetattraccessopen 都能有效地使用这些缓存来提升性能。JuiceFS 客户端默认最多会缓存 500000 个目录的元数据,可通过 --max-cached-inodes 选项调整。

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

特别地,为保证强一致性,open 操作默认需要直接访问元数据服务,不会利用元数据缓存。但在只读场景下(例如 AI 模型训练),可以用 --opencache 来启用 open 缓存,进一步提高读性能。此特性默认关闭。

元数据一致性

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

当多个客户端同时使用时,内核中缓存的元数据只能通过时间失效,客户端中缓存的元数据会根据所有客户端的修改自动失效,但是是异步的。因此极端情况下可能出现在 A 机器做了修改操作,再去 B 机器访问时,B 机器还未能看到更新的情况。

数据缓存

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

读写缓冲区

挂载参数 --buffer-size 控制着 JuiceFS 的读写缓冲区大小,默认 300MiB。读写缓冲区的大小决定了预读(readahead)的数据量,同时也控制着写缓存(pending page)的大小。在面对高并发读写场景的时候,我们推荐对 --buffer-size 进行相应的扩容,能有效提升性能。

你也可以从 JuiceFS 的监控数据中搜索查看 mount_totalBufferUsedmount_readBufferUsed 来确定当前的用量,然后结合场景与宿主机的情况进行调整。

内核中数据缓存

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

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

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

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

客户端读缓存

客户端会根据应用读数据的模式,自动做预读和缓存操作以提高顺序读的性能。数据会缓存到本地文件系统中,可以是基于硬盘、SSD 或者内存的任意本地文件系统。

如果缓存文件系统无法正常工作时能立刻返回错误,JuiceFS 会自动降级成直接访问对象存储。这对于本地盘而言通常是成立的,但如果缓存所在的文件系统异常时体现为读操作卡死(如某些内核态的网络文件系统),那么 JuiceFS 也会随之一起卡住,这就要求你对文件系统的行为进行调优,做到快速失败。

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

以下是缓存配置的关键参数(完整参数列表见 juicefs mount):

  • --cache-dir

    缓存目录,默认为 /var/jfsCache,支持传入多个目录(用 : 分隔)以及通配符,方便挂载多块盘的机器搭建成缓存专用节点。如果有需要,也可以传入 /dev/shm,使用内存作为缓存。

    如果急需释放磁盘空间,你可以手动清理缓存目录下的文件,缓存路径通常为 /var/jfsCache/<vol-name>/raw/

    注意

    如果同时开启了客户端写缓存,务必注意不要删除 /var/jfsCache/<vol-name>/rawstaging/ 下的文件,否则会造成数据丢失。

  • --cache-size--free-space-ratio

    缓存空间大小(默认为 100 GiB)与缓存盘的最少剩余空间占比(默认 0.2)。这两个参数任意一个达到阈值,均会自动触发缓存淘汰,使用的是类似于 LRU 的策略。

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

  • --cache-partial-only

    只缓存小文件和随机读的部分,适合对象存储的吞吐比缓存盘还高的情况。

    读一般有两种模式,连续读和随机读。对于连续读,一般需要较高的吞吐。对于随机读,一般需要较低的时延。当本地磁盘的吞吐反而比不上对象存储时,可以考虑启用 --cache-partial-only,这样一来,连续读虽然会将一整个对象块都会被读取下来,但并不会被缓存。而随机读(例如读 Parquet 或者 ORC 文件的 footer)所读取的字节数比较小,不会读取整个对象块,此类读取就会被缓存。充分地利用了本地磁盘低时延和网络高吞吐的优势。

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

另一个对随机读性能影响较大的是压缩功能(以及加密,原理同)。当 JuiceFS 启用压缩后,即使读取很少的部分,也会将整个 Block 从对象存储上完整下载,解压缩并缓存在本地,这将极大影响首次访问的性能。所以在密集随机读的场景,一般建议关闭压缩功能,这样一来,JuiceFS 可以读取一个对象块的部分数据,降低了读的时延和带宽占用,提升首次访问性能。

数据一致性

JuiceFS 将文件分成一个个对象块(默认 4MB)存储在对象存储上,对于文件的任何修改操作都是将新数据写入新增的对象块,原有块是不会有任何修改的。所以不用担心数据缓存的一致性问题,因为一旦文件被修改过了,JuiceFS 会从对象存储读取新的对象块,不会再读取文件中被覆盖的部分对应的数据块(之后会被淘汰掉)。

客户端写缓存

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

因此,如果你的场景大量写小文件,考虑开启 --writeback 以提高写入性能,但也请注意:

  • 开启 --writeback 时,缓存本身的可靠性与数据写入的可靠性直接相关,对此要求高的场景应谨慎使用。
  • --writeback 模式下,数据写入不会直接进入缓存组,其他节点要访问该文件的时候需要从对象存储下载。请继续阅读下方内容了解缓存组。
  • 如果缓存文件系统出错,则降级为同步写入对象存储,情况类似客户端读缓存

也正因此,--writeback 默认关闭。

客户端缓存数据共享

JuiceFS 默认的缓存策略是单机独享的,即同一个节点上的多个进程可以共享本地目录中的缓存数据,但各个节点之间的缓存相互独立。

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

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

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

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

独立缓存集群

在多机分布式缓存方案中,每个 JuiceFS 客户端均参与缓存组的建立,如果遇到 JuiceFS 客户端不是常驻的情况,比如 Spark on Kubernetes,客户端不断被销毁、重建,导致缓存利用率很低。对于此类动态创建伸缩的计算集群,可以将 JuiceFS 文件系统挂载在固定机器上,创建专门的缓存集群来给对象存储加速,它是在 缓存数据共享 的基础上,给计算集群增加挂载参数 --no-sharing 实现的。增加 --no-sharing 参数且拥有相同 --cache-group 配置的计算节点将不会参与建立缓存集群,仅会从缓存集群读取数据。

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

当需要访问固定的数据集时,可以通过 juicefs warmup 将该数据集提前预热,以提升第一次访问数据时的性能。juicefs warmup 不一定必须在缓存集群内执行,在启用了 --no-sharing 的客户端节点上执行,同样能为缓存集群进行预热。

搭建了缓存集群后,便需要对重点参数做好监控,并根据情况适时扩容集群:

  • 在缓存集群容量足够的情况下客户端的大部分请求应该都能由缓存集群来响应,少部分请求可能会发送到对象存储。你可以通过类似 Block Cache Hit Ratio 以及 Objects Requests 的指标来进行观测,这些均已包含在我们提供的 Grafana Dashboard 模板中,详见监控
  • 缓存集群空间不足时会发生块换出(eviction),可以通过 blockcache.evict 等相关指标来监控。详细的监控指标列表见监控项说明