Skip to main content

缓存

对于一个由对象存储和数据库组合驱动的文件系统,缓存是本地客户端与远端服务之间高效交互的重要纽带。读写的数据可以提前或者异步载入缓存,再由客户端在后台与远端服务交互执行异步上传或预取数据。相比直接与远端服务交互,采用缓存技术可以大大降低存储操作的延时并提高数据吞吐量。

JuiceFS 提供包括元数据缓存、数据读写缓存等多种缓存机制。

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

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

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

数据一致性

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

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

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

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

元数据缓存

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

内核元数据缓存

内核中可以缓存三种元数据:属性(attribute)文件项(entry)目录项(direntry),可以通过以下挂载参数控制缓存时间:

--attr-cache value       属性缓存时长,单位秒 (默认值: 1)
--entry-cache value 文件项缓存时长,单位秒 (默认值: 1)
--dir-entry-cache value 目录项缓存时长,单位秒 (默认值: 1)

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

客户端内存元数据缓存

注意

此特性需要使用 0.15.2 及以上版本的 JuiceFS

JuiceFS 客户端在 open() 操作即打开一个文件时,其文件属性(attribute)会被自动缓存在客户端内存中。如果在挂载文件系统时设置了 --open-cache 选项且值大于 0,只要缓存尚未超时失效,随后执行的 getattr()open() 操作会从内存缓存中立即返回结果。

执行 read() 操作即读取一个文件时,文件的 chunk 和 slice 信息会被自动缓存在客户端内存。在缓存有效期内,再次读取 chunk 会从内存缓存中立即返回 slice 信息(查阅「JuiceFS 如何存储文件」以了解 chunk 和 slice 是什么)。

为保强一致性,--open-cache 默认关闭,每次打开文件都需直接访问元数据引擎。但如果文件很少发生修改,或者只读场景下(例如 AI 模型训练),则推荐根据情况设置 --open-cache,进一步提高读性能。

数据缓存

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 0.15.2 及以上,对于已经读过的文件,内核会为其建立页缓存(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。请阅读「缓存位置」了解更多信息。

    如果急需释放磁盘空间,你可以手动清理缓存目录下的文件,缓存路径为 <cache-dir>/<UUID>/raw/

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

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

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

  • --cache-partial-only

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

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

客户端写缓存

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

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

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

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

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

也正由于写缓存的使用注意事项较多,使用不当极易出问题,我们推荐仅在大量写入小文件时临时开启(比如解压包含大量小文件的压缩文件)。

启用 --writeback 模式后,除了直接查看 /var/jfsCache/<UUID>/rawstaging/ 目录,还可以通过以下命令确定文件上传进度:

# 假设挂载点为 /jfs
$ cd /jfs
$ cat .stats | grep "staging"
juicefs_staging_block_bytes 1621127168 # 待上传的数据块大小
juicefs_staging_block_delay_seconds 46116860185.95535
juicefs_staging_blocks 394 # 待上传的数据块数量

缓存位置

取决于操作系统,JuiceFS 的默认缓存路径如下:

  • Linux/var/jfsCache
  • macOS$HOME/.juicefs/cache
  • Windows%USERPROFILE%\.juicefs\cache

对于 Linux 系统,要注意默认缓存路径要求管理员权限,普通用户需要有权使用 sudo 才能设置成功,例如:

sudo juicefs mount redis://127.0.0.1:6379/1 /mnt/myjfs

另外,可以在挂载文件系统时通过 --cache-dir 选项设置在当前系统可以访问的任何存储路径上。对于没有访问 /var 目录权限的普通用户,可以把缓存设置在用户的 HOME 目录中,例如:

juicefs mount --cache-dir ~/jfscache redis://127.0.0.1:6379/1 /mnt/myjfs
提示

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

内存盘

如果对文件的读性能有更高要求,可以把缓存设置在内存盘上。对于 Linux 系统,通过 df 命令查看 tmpfs 类型的文件系统:

$ df -Th | grep tmpfs
文件系统 类型 容量 已用 可用 已用% 挂载点
tmpfs tmpfs 362M 2.0M 360M 1% /run
tmpfs tmpfs 3.8G 0 3.8G 0% /dev/shm
tmpfs tmpfs 5.0M 4.0K 5.0M 1% /run/lock

其中 /dev/shm 是典型的内存盘,可以作为 JuiceFS 的缓存路径使用,它的容量一般是内存的一半,可以根据需要手动调整容量,例如,将缓存盘的容量调整为 32GB:

sudo mount -o size=32000M -o remount /dev/shm

然后使用该路径作为缓存,挂载文件系统:

juicefs mount --cache-dir /dev/shm/jfscache redis://127.0.0.1:6379/1 /mnt/myjfs

除此之外,还可以将 --cache-dir 选项设置为 memory 来直接使用进程内存作为缓存,与 /dev/shm 相比,好处是简单不依赖外部设备,但相应地也无法持久化,一般在测试评估的时候使用。

共享目录

SMB、NFS 等共享目录也可以用作 JuiceFS 的缓存,对于局域网有多个设备挂载了相同 JuiceFS 文件系统的情况,将局域网中的共享目录作为缓存路径,可以有效缓解多个设备重复预热缓存的带宽压力。

以 SMB/CIFS 共享为例,使用 cifs-utils 包提供的工具挂载局域网中的共享目录:

sudo mount.cifs //192.168.1.18/public /mnt/jfscache

将共享目录作为 JuiceFS 缓存:

sudo juicefs mount --cache-dir /mnt/jfscache redis://127.0.0.1:6379/1 /mnt/myjfs

多缓存目录

JuiceFS 支持同时设置多个缓存目录,从而解决缓存空间不足的问题,使用 :(Linux、macOS)或 ;(Windows)字符分隔多个路径,例如:

sudo juicefs mount --cache-dir ~/jfscache:/mnt/jfscache:/dev/shm/jfscache redis://127.0.0.1:6379/1 /mnt/myjfs

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

因此建议不同缓存目录/缓存盘的可用空间保持一致,否则可能造成不能充分利用某个缓存目录空间的情况。例如 --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