Skip to main content

缓存

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

我的场景真的需要缓存吗?

数据缓存可以有效地提高随机读的性能,对于像 Elasticsearch、ClickHouse 等对随机读性能要求更高的应用,建议将缓存路径设置在速度更快的存储介质上并分配更大的缓存空间。

然而缓存能提升性能的前提是,你的应用需要反复读取同一批文件。如果你确定你的应用对数据是「读取一次,然后再也不需要」的访问模式(比如大数据的数据清洗常常就是这样),可以关闭缓存功能,省去缓存不断建立,又反复淘汰的开销。

数据一致性

JuiceFS 提供「关闭再打开(close-to-open)」一致性保证,即当两个及以上客户端同时读写相同的文件时,客户端 A 的修改在客户端 B 不一定能立即看到。但是,一旦这个文件在客户端 A 写入完成并关闭,之后在任何一个客户端重新打开该文件都可以保证能访问到最新写入的数据,不论是否在同一个节点。

「关闭再打开」是 JuiceFS 提供的最低限度一致性保证,在某些情况下可能也不需要重新打开文件才能访问到最新写入的数据:

  • 多个应用程序使用同一个 JuiceFS 客户端访问相同的文件时,文件变更立即对所有进程可见。
  • 在不同节点上通过 tail -f 命令查看最新数据(需使用 Linux 系统)

至于对象存储,JuiceFS 将文件分成一个个数据块(默认 4MiB),赋予唯一 ID 并保存在对象存储上。文件的任何修改操作都将生成新的数据块,原有块保持不变,包括本地磁盘上的缓存数据。所以不用担心数据缓存的一致性问题,因为一旦文件被修改过了,JuiceFS 会从对象存储读取新的数据块,不会再读取文件中被覆盖的部分对应的数据块(之后会被删除掉)。

元数据缓存

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

内核元数据缓存

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

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

默认会缓存属性、文件项和目录项,保留 1 秒,以提高 lookup 和 getattr 的性能。当多个节点的客户端同时使用同一个文件系统时,内核中缓存的元数据只能通过时间失效。也就是说,极端情况下可能出现节点 A 修改了某个文件的元数据(如 chown),通过节点 B 访问未能立即看到更新的情况。当然,等缓存过期后,所有节点最终都能看到 A 所做的修改。

客户端内存元数据缓存

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

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

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

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

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

数据缓存

JuiceFS 对数据也提供多种缓存机制来提高性能,包括内核中的页缓存和客户端所在机器的本地缓存,以及客户端自身的内存读写缓冲区。读请求会依次尝试内核分页缓存、JuiceFS 进程的预读缓冲区、本地磁盘缓存,当缓存中没找到对应数据时才会从对象存储读取,并且会异步写入各级缓存保证下一次访问的性能。

读写缓冲区

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

如果你希望增加写入速度,通过调整 --max-uploads 增大了上传并发度,但并没有观察到上行带宽用量有明显增加,那么此时可能就需要相应地调大 --buffer-size,让并发线程更容易申请到内存来工作。这个排查原理反之亦然:如果增大 --buffer-size 却没有观察到上行带宽占用提升,也可以考虑增大 --max-uploads 来提升上传并发度。

可想而知,--buffer-size 也控制着每次 flush 操作的上传数据量大小,因此如果客户端处在一个低带宽的网络环境下,可能反而需要降低 --buffer-size 来避免 flush 超时。关于低带宽场景排查请详见「与对象存储通信不畅」。

你可以从 JuiceFS 的监控数据 API 中搜索查看 juicefs_totalBufferUsedjuicefs_readBufferUsed 来确定当前的用量,但视场景不同,可能读写缓冲区内存会一直处于利用率很高的状态,还是需要结合场景与宿主机的情况进行调整。

内核页缓存

对于已经读过的文件,内核会为其建立页缓存(Page Cache),下次再打开的时候,如果文件没有被更新,就可以直接从内核页缓存读取,获得最好的性能。

JuiceFS 元数据服务会跟踪所有最近被打开的文件,同一个客户端要重复打开相同文件时,它会根据该文件是否被修改了告诉该客户端是否可以使用内核页数据,如果文件被修改过,则对应的页缓存也将在再次打开时失效,这样保证了客户端能够读到最新的数据。

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

内核回写模式

从 Linux 内核 3.15 开始,FUSE 支持内核回写(writeback-cache)模式,内核会把高频随机小 IO(例如 10-100 字节)的写请求合并起来,显著提升随机写入的性能。

在挂载命令通过 -o writeback_cache 选项来开启内核回写模式。注意,内核回写与「客户端写缓存」并不一样,前者是内核中的实现,后者则发生在 JuiceFS 客户端,二者适用场景也不一样,详读对应章节以了解。

客户端读缓存

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

JuiceFS 客户端会把从对象存储下载的数据,以及新上传的小于 1 个 block 大小的数据写入到缓存目录中,不做压缩和加密。如果希望保证应用程序首次访问数据的时候就能获得已缓存的性能,可以使用 juicefs warmup 命令来对缓存数据进行预热。

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

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

  • --prefetch

    并发预读 N 个块(默认 1)。所谓预读(prefetch),就是随机读取文件任意一小段,都会触发对应的整个对象存储块异步完整下载。预读往往能改善随机读性能,但如果你的场景的文件访问模式无法利用到预读数据(比如 offset 跨度极大的大文件随机访问),预读会带来比较明显的读放大,可以考虑设为 0 以禁用预读特性。请阅读「读放大」了解更多信息。

    JuiceFS 还内置着另一种类似的预读机制:在顺序读时,会提前下载临近的对象存储块,这在 JuiceFS 内称为 readahead 机制,能有效提高顺序读性能。Readahead 的并发度受「读写缓冲区」的大小影响,读写缓冲区越大并发度越高。

  • --cache-dir

    缓存目录,默认为 /var/jfsCache$HOME/.juicefs/cache,支持传入多个目录(用 : 分隔)以及通配符,方便挂载多块盘的机器搭建成缓存专用节点。如果有需要,也可以传入 /dev/shm,使用内存作为缓存。除此之外,还可以传入 memory 字符串来直接使用进程内存作为缓存,与 /dev/shm 相比,好处是简单不依赖外部设备,但相应地也无法持久化,一般在测试评估的时候使用。

    建议缓存目录尽量使用独立的盘,不要用系统盘,也不要和其它应用共用。共用不仅会相互影响性能,还可能导致其它应用出错(例如磁盘剩余空间不足)。如果无法避免必须共用那一定要预估好其它应用所需的磁盘容量,限制缓存空间大小(详见下方),避免 JuiceFS 的读缓存或者写缓存占用过多空间。

    当设置了多个缓存目录,或者使用多块设备作为缓存盘,--cache-size 选项表示所有缓存目录中的数据总大小。客户端会采用 hash 策略向各个缓存路径中均匀地写入数据,无法对多块容量或性能不同的缓存盘进行特殊调优。

    因此建议不同缓存目录/缓存盘的可用空间保持一致,否则可能造成不能充分利用某个缓存目录/缓存盘空间的情况。例如 --cache-dir/data1:/data2,其中 /data1 的可用空间为 1GiB,/data2 的可用空间为 2GiB,--cache-size 为 3GiB,--free-space-ratio 为 0.1。因为缓存的写入策略是均匀写入,所以分配给每个缓存目录的最大空间是 3GiB / 2 = 1.5GiB,会造成 /data2 目录的缓存空间最大为 1.5GiB,而不是 2GiB * 0.9 = 1.8GiB

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

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

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

    实际缓存数据占用空间大小可能会略微超过设置值,这是因为对同样一批缓存数据,很难精确计算它们在不同的本地文件系统上所占用的存储空间,JuiceFS 累加所有被缓存对象大小时会按照 4KiB 的最小值来计算,因此与 du 得到的数值往往不一致。

  • --cache-partial-only

    只缓存小于 Block Size 的小文件和随机读的部分,默认为 false。适合场景比如:对象存储的吞吐比缓存盘还高,导致缓存在本地的大文件读取反而更慢。或者大文件只读一次,小文件则需要反复读取。

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

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

客户端写缓存

开启客户端写缓存能提升特定场景下的大量小文件写入性能,请详读本节了解。

客户端写缓存默认关闭,写入的数据会首先进入 JuiceFS 客户端的内存读写缓冲区,当一个 Chunk 被写满,或者应用强制写入(调用 close() 或者 fsync())时,才会触发数据上传对象存储。为了确保数据安全性,客户端会等数据上传完成,才提交到元数据服务。

由于默认的写入流程是「先上传,再提交」,可想而知,大量小文件写入时,这样的流程将影响写入性能。启用客户端写缓存以后,写入流程将改为「先提交,再异步上传」,写文件不会等待数据上传到对象存储,而是写入到本地缓存目录并提交到元数据服务后就立即返回,本地缓存目录中的文件数据会在后台异步上传至对象存储。

挂载时加入 --writeback 参数,便能开启客户端写缓存,但在该模式下请注意:

  • 本地缓存本身的可靠性与缓存盘的可靠性直接相关,如果在上传完成前本地数据遭受损害,意味着数据丢失。因此对数据安全性要求越高,越应谨慎使用。
  • 待上传的文件默认存储在 /var/jfsCache/<vol-name>/rawstaging/,务必注意不要删除该目录下的文件,否则将造成数据丢失。
  • 写缓存大小由 --free-space-ratio 控制。默认情况下,如果未开启写缓存,JuiceFS 客户端最多使用缓存目录 80% 的磁盘空间(计算规则是 (1 - <free-space-ratio>) * 100)。开启写缓存后会超额使用一定比例的磁盘空间,计算规则是 (1 - (<free-space-ratio> / 2)) * 100,即默认情况下最多会使用缓存目录 90% 的磁盘空间。
  • 写缓存和读缓存共享缓存盘空间,因此会互相影响。例如写缓存占用过多磁盘空间,那么将导致读缓存的大小受到限制,反之亦然。
  • 如果本地盘写性能太差,带宽甚至比不上对象存储,那么 --writeback 会带来更差的写性能。
  • 如果缓存目录的文件系统出错,客户端则降级为同步写入对象存储,情况类似客户端读缓存
  • 如果节点到对象存储的上行带宽不足(网速太差),本地写缓存迟迟无法上传完毕,此时如果在其他节点访问这些文件,则会出现读错误。低带宽场景的排查请详见「与对象存储通信不畅」
  • --writeback 模式下,数据写入不会直接进入缓存组,其他节点要访问该文件的时候需要从对象存储下载。请继续阅读下方内容了解缓存组。

也正由于写缓存的使用注意事项较多,使用不当极易出问题,我们推荐仅在大量写入小文件时临时开启(比如解压包含大量小文件的压缩文件)。调整挂载参数也非常方便:JuiceFS 客户端支持平滑重新挂载,你只需要调整挂载参数,重新运行挂载命令即可,详见「平滑重启」

分布式缓存

JuiceFS 客户端的缓存默认是单机独享的,但当大量客户端需要反复访问同一个数据集时,可以开启「分布式缓存」功能,让大量客户端共享同一批缓存数据,有效提升性能。分布式缓存非常适合使用 GPU 集群进行深度学习模型训练的场景,通过把训练数据集缓存到集群所有节点的内存或 SSD 中,提供高性能的数据访问能力,让 GPU 不会因为数据读取太慢而闲置。

启用分布式缓存,需要在挂载 JuiceFS 时,通过 --cache-group 设置缓存组名称。对于同一个局域网内相同缓存组的客户端,它们会把监听在内网 IP 的随机端口汇报给元数据服务,进而发现其它客户端,并通过内网通信共享缓存。特别地,还可以通过 --fill-group-cache(默认关闭),让客户端在写入数据的同时,将上传到对象存储的数据块同时发给缓存组,让写操作也能参与缓存集群的建设。

缓存组内的成员客户端,组成了一个一致性哈希环(Consistent Hashing Ring)。每个数据块都会根据一致性哈希计算出负责其存储的成员节点,当客户端发起查询时,会直接从该节点上直接获取数据(如果数据尚未存在于该节点,也会从对象存储下载并缓存)。缓存组内如果发生了成员节点增删,数据会向哈希环的临近节点做迁移(为了防止波动,实际会等待成员变更后约十分钟,方执行迁移操作)。可见缓存组的成员变更,只影响到少量数据块的缓存命中率。在缓存组一致性哈希环的实现中,也采用了虚拟节点(virtual node)的概念,确保数据分布均衡,避免因数据迁移产生访问热点,影响缓存组性能。

构建 JuiceFS 分布式缓存,对节点规格有如下要求(同样适用于下方「独立缓存集群」一节):

  • 推荐用同构机型(至少缓存盘大小相同)来构建缓存组,JuiceFS 缓存组目前的一致性哈希算法并不会考虑到不同节点的可用空间不一致,从而调整各个节点的权重,因此如果缓存组节点的磁盘大小不一致,将会在均匀分配的算法下产生空间浪费。
  • 缓存组内节点均以万兆网络相互连接,如果节点安装了多块网卡,需要使用 --group-ip 指定大带宽的网卡,确保组内成员通信顺畅。
  • 缓存组内节点需要保证能高速访问对象存储服务,如果下载速度太慢,在缓存预热和实际使用时容易发生下载超时,影响使用体验。

独立缓存集群

在分布式缓存方案中,每个 JuiceFS 客户端均参与缓存组的建立。如果遇到 JuiceFS 客户端不是常驻的情况,比如 Kubernetes 集群,客户端不断被销毁、重建,可能导致缓存利用率很低。对于此类动态创建伸缩的计算集群,可以将 JuiceFS 文件系统挂载在固定机器上,创建专门的缓存集群来给对象存储加速,它是在「分布式缓存」的基础上,给计算集群增加挂载参数 --no-sharing 实现的。缓存组内的 --no-sharing 节点,有着以下行为:

  • 如同 --no-sharing 字面意思,这些节点虽然属于同一个缓存组,但却不分享自己的缓存数据,只会向缓存集群索取数据。
  • --fill-group-cache 行为不受 --no-sharing 影响,若 --no-sharing 节点同时启用了 --fill-group-cache,在写入文件时,也会将数据块发给缓存集群,参与缓存集群的建立。
  • 如果启用了 --writeback,那么 --fill-group-cache 将不生效。
  • 如果希望最大程度利用独立缓存集群的数据,节约客户端节点磁盘空间,或者说客户端的磁盘吞吐甚至比不上内网带宽,此时可以考虑禁用节点自身缓存,完全从缓存集群、对象存储获取数据。将 --cache-size 设置为 0,就能禁用节点自身缓存。

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

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

为 Kubernetes 集群搭建独立缓存集群,可以参考「CSI 驱动:独立缓存集群」

缓存调优

无论是单机还是缓存集群,调优的时候都需要首先关注监控,根据相关指标来判断调优方向。你可以下载我们的 Grafana Dashboard,里边已经包含了众多关键监控图表,详见监控

对于单机缓存,可以在 Grafana Dashboard 参考以下相关指标:

  • Block Cache Hit Ratio(缓存命中率)
  • Block Cache Count(本地缓存块数量)
  • Block Cache Size(本地缓存数据量)
  • Grafana Dashboard 中的其他 Block Cache 相关指标

对于缓存集群,有以下关键指标,你需要查看并根据情况适时扩缩容集群:

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