Skip to main content

缓存

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

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

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

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

数据一致性

分布式系统,往往需要在缓存和一致性之间进行取舍。JuiceFS 由于其元数据分离架构,需要从元数据、文件数据(对象存储)、文件数据本地缓存三方面来思考一致性问题:

对于元数据缓存,JuiceFS 默认的挂载设置满足「关闭再打开(close-to-open)」一致性,也就是说一个客户端修改并关闭文件之后,其他客户端重新打开这个文件都会看到最新的修改。与此同时,默认的挂载参数设置了 1 秒的内核元数据缓存,满足了一般场景的需要。但如果你的应用需要更激进的缓存设置以提升性能,可以阅读下方章节,对元数据缓存进行针对性的调优。特别地,发起修改的客户端(挂载点)能享受到更强的一致性,阅读一致性例外详细了解。

对于对象存储,JuiceFS 将文件分成一个个数据块(默认 4MiB),赋予唯一 ID 并上传至对象存储服务。文件的任何修改操作都将生成新的数据块,原有块保持不变,所以不用担心数据缓存的一致性问题,因为一旦文件被修改过了,JuiceFS 会从对象存储读取新的数据块。而老的失效数据块,也会随着回收站或碎片合并机制被删除,避免对象存储泄露。

本地数据缓存缓存也是以对象存储的数据块做为最小单元。一旦文件数据被下载到缓存盘,一致性就和缓存盘可靠性相关,如果磁盘数据发生了篡改,客户端也会读取到错误的数据。对于这种担忧,可以配置合适的 --verify-cache-checksum 策略,确保缓存盘数据完整性。

元数据缓存

作为用户态文件系统,JuiceFS 元数据缓存既通过 FUSE API,以内核元数据缓存的形式进行管理,同时也直接在客户端内存中维护,也就是说和数据缓存相似,元数据也存在着多级缓存架构。

内核元数据缓存

注意

内核元数据缓存没有任何主动失效机制(本机挂载点除外),只能等待过期淘汰。因此如果未经仔细验证,不要增加内核元数据的缓存时间,否则可能引发的文件一致性问题。

JuiceFS 客户端可以控制这些内核元数据缓存:文件属性(attribute,包含文件名、大小、权限、修改时间等信息)、文件项(包括 entry 和 direntry,用来进一步区分文件和目录类型的文件。包含文件的 inode、名字、类型),在挂载时,可以使用下方参数,通过 FUSE 控制这些元数据的缓存时间:

# 文件属性缓存时间(秒),默认为 1,提升 getattr 性能
--attrcacheto=1

# 文件的缓存时间(秒),默认为 1,提升文件 lookup 性能
--entrycacheto=1

# 目录类型文件的缓存时间(秒),默认为 1,提升目录的 lookup 性能
--direntrycacheto=1

让以上元数据默认在内核中缓存 1 秒,能显著提高 lookupgetattr 的性能。

需要注意,entry 缓存是随着文件访问逐渐建立起来的,不是一个完整列表,因此不能被 readdir 调用或者 ls 命令使用,而只对 lookup 调用有加速效果。这里的 direntry 含义也不等同于「目录项」的概念,他并不用来描述「一个目录下包含哪些文件」,而是和 entry 一样,都是文件,只不过对文件是否目录类型做了区分。

以上元数据缓存项同时还存在于客户端内存中,相当于组成了「内核 → 客户端内存」的多级缓存。考虑到客户端内存中的元数据缓存会默认保留 5 分钟,并且支持主动失效(下方小节中详细介绍),我们不建议将上述内核元数据的缓存时间进一步提高。如果要提高内核元数据缓存时间,文件系统应该满足以下特点:

  • 文件极少变动,或者完全只读
  • 需要 lookup 大量文件,比如对于超大型 Git 仓库运行 git status,希望尽可能避免请求穿透到用户态,获得极致性能

在实际场景中,也很少需要对 --entrycacheto--direntrycacheto 进行区分设置,如果确实要精细化调优,在目录极少变动、而文件频繁变动的场景,可以令 --direntrycacheto 大于 --entrycacheto

客户端内存元数据缓存

为了减少客户端和元数据服务之间频繁的列表(ls)和查询操作,JuiceFS 默认会把访问过的文件和目录完整地缓存在客户端内存中。与内核元数据缓存不同,这部分缓存数据支持主动失效(从元数据服务获取数据变更信息,异步地清理客户端内存中的元数据),因此默认设置更长的缓存时间(5 分钟)。通过如下的参数调节:

# 在客户端中缓存元数据,默认开启
--metacache

# 内存元数据的缓存时间,单位为秒,默认 5 分钟
--metacacheto=300

# 默认最多会缓存 500000 个 inodes
--max-cached-inodes=500000

默认开启的 --metacache,会令访问过的文件在客户端内存中缓存 5 分钟。5 分钟是一个很长的过期时间,但也不必担心一致性问题,因为:

  • 客户端内存中的元数据缓存,只用于文件目录结构的加速访问,lookupgetattraccessopen 能有效地使用这些缓存来提升性能。注意这里的 open 特指打开目录,打开文件则默认不会使用缓存(为了确保强一致性)。如果希望对打开文件也进行缓存,详见下方介绍的 --opencache 选项。
  • 客户端会缓存符号链接的内容,考虑到符号链接不会被修改(修改符号链接,会以创建新文件覆盖的方式进行),所以缓存一直有效。
  • 客户端内存中的元数据缓存支持主动失效,在下方架构图中可以看到,当文件发生修改,元数据服务和客户端之间用事件通知机制来下发文件变动信息,客户端会根据事件监听来主动清理失效的内存缓存,因此即便使用 5 分钟作为默认过期时间,每个客户端内存中的元数据缓存都是实时有效的。

client memory metadata cache

为保证「关闭再打开(close-to-open)」一致性,open 文件(而不是目录)时默认需要直接访问元数据服务,不会利用缓存。也就是说,客户端 A 的修改在客户端 B 不一定能立即看到。但是,一旦这个文件在 A 写入完成并关闭,之后在任何一个客户端重新打开该文件都可以保证能访问到最新写入的数据,不论是否在同一个节点。文件的属性缓存也不一定要通过 open 操作建立,比如 tail -f 会不断查询文件属性,在这种情况下无需重新打开文件,也能获得最新文件变动。

因此,对于写入不频繁的场景(例如 AI 模型训练),可以用 --opencache 来启用 open 缓存(缓存时间同样受 --metacacheto 控制),这样一来,打开文件操作会直接使用客户端本地内存中的文件属性缓存,这里的文件属性缓存,不仅包含内核元数据中的 attribute 比如文件大小、修改时间信息,还包含 JuiceFS 特有的属性,如文件和 chunks、slices 的对应关系。有了这些信息,就能省去每次打开文件都重新访问元数据服务的开销。

就算开启了 --opencache,考虑到客户端内存元数据支持主动失效,事实上依旧能维持不错的一致性,和内核元数据类似,发起修改的客户端同样能享受到客户端内存元数据缓存强一致,只是对于其他客户端,由于需要用订阅文件变更事件的方式来异步淘汰缓存,从提交修改到主动失效有少量的时间间隔,因此不再满足 close-to-open 一致性。与如果你的场景中,被修改的文件需要立刻被其他客户端访问,那么不建议开启 --opencache

一致性例外

上方的「内核元数据缓存」,以及「客户端内存元数据缓存」,主要讨论的是多个客户端读写同一个文件情况下的一致性问题,是“最低限度的一致性保障”。但是对于发起修改的客户端自身,由于信息的变化都发生在“当地”,不难想象,对于变更的文件,发起修改的挂载点能实时地察觉到文件变化,享受到更高的一致性。具体而言:

  • 对于发起修改的挂载点,自身的内核元数据缓存能够主动失效。但对于多个挂载点访问、修改同一文件的情况,只有发起修改的客户端能享受到内核元数据缓存主动失效,其他客户端就只能等待缓存自然过期。
  • 调用 write 成功后,挂载点自身立刻就能看到文件长度的变化(比如用 ls -al 查看文件大小,可能会注意到文件不断变大)——但这并不意味着修改已经成功提交,在 flush 成功前,是不会将这些改动同步到对象存储的,其他挂载点也看不到文件的变动。
  • 作为上一点的极端情况,如果调用 write 写入,并在当前挂载点观察到文件长度不断增长,但最后的 flush 因为某种原因失败了,比方说到达了文件系统配额上限,文件长度会立刻发生回退,比如从 10M 变为 0。这是一个容易引人误会的情况——并不是 JuiceFS 清空了你的数据,而是写入自始至终就没有成功,只是由于发起修改的挂载点能够提前预览文件长度的变化,让人误以为写入已经成功提交。
  • 发起修改的挂载点,其客户端内存中的元数据缓存会立刻主动失效。而其他客户端则需要监听文件修改的事件通知,因此在修改到缓存失效存在短暂时间差。
  • 发起修改的挂载点,能够监听对应的文件变动(比如使用 fswatch 或者 Watchdog)。但范畴也仅限于该挂载点发起修改的文件,也就是说 A 修改的文件,无法在 B 挂载点进行监听。
  • 目前而言,由于 FUSE 尚不支持 inotify API,所以如果你希望监听 JuiceFS 特定目录下的文件变化,请使用轮询的方式(比如 PollingObserver)。

读写缓冲区

读写缓冲区是分配给 JuiceFS 客户端进程的一块内存,通过 --buffer-size 控制着大小,默认 300(单位 MiB)。读和写产生的数据,都会途经这个缓冲区。所以缓冲区的作用非常重要,在大规模场景下遇到性能不足时,提升缓冲区大小也是常见的优化方式。

预读和预取

提示

为了准确描述 JuiceFS 客户端的工作机制,文档中会用「预读」和「预取」来特指客户端的两种不同提前下载数据、优化读性能的行为。

顺序读文件时,JuiceFS 客户端会进行预读(readahead),也就是提前将文件后续的内容下载下来。事实上同样的行为也早已存在于内核:读取文件时,内核也会根据具体的读行为和预读窗口算法,来提前将文件读取到内核页缓存。考虑到 JuiceFS 是个网络文件系统,内核的预读窗口对他来说太小,无法有效提升顺序读的性能,因此在内核的预读之上,JuiceFS 客户端也会发起自己的预读,根据更激进的算法来“猜测”应用接下来要读取的数据范围,然后提前将对象存储对应的数据块下载下来。

readahead

预读窗口大小会根据缓冲区和下载并发度进行推算,算法是在 buffer-size / 5, block-size * max-downloads, block-size * 128MiB 中取最小值(其中 block-size 就是块大小,默认 4MiB)。这样的算法能确保在缓冲区(或并发上限)过大时,大文件的预读不会令资源消耗不受控。如果希望进一步优化单个大文件顺序读的场景,你也可以结合实际情况,用 --max-readahead 手动设置预读窗口的上限(单位为 MiB)。

缓冲区对性能的影响是多方面的,除了上方介绍的预读窗口,他还间接控制对象存储的请求并发度。也就是说,设置更大的 --max-downloads 或者 --max-uploads,并不一定会带来性能提升,还可能要提高缓冲区大小,继续阅读下方小节了解如何观测和调优。

由于 readahead 只能优化顺序读场景,因此在 JuiceFS 客户端还存在着另一种相似的机制,称作预取(prefetch):随机读取文件某个块(Block)的一小段,客户端会异步将整个对象存储块下载下来。

prefetch

预取的设计是基于「假如文件的某一小段被应用读取,那么文件附近的区域也很可能会被读取」的假设,对于不同的应用场景,这样的假设未必成立——如果应用对大文件进行偏移极大的、稀疏的随机读,那么不难想象,prefetch 会带来明显的读放大。因此如果你已经对应用场景的读取模式有深入了解,确认并不需要 prefetch,可以通过 --prefetch=0 禁用该行为。

预读和预取分别优化了顺序读、随机读性能,也会带来一定程度的读放大,阅读「读放大」了解更多信息。

写入

调用 write 成功,并不代表数据被持久化,持久化是 fsync 的工作(或者 fdatasyncclose)。这一点不论对于本地文件系统,还是 JuiceFS 文件系统,都是一样的。在 JuiceFS 中,write 会将数据写入缓冲区,写入完毕以后,你甚至会注意到,当前挂载点已经看到文件长度有所变化,不要误会,这并不代表写入已经持久化(这点也在一致性话题上有更详细介绍)。总而言之,在 flush 来临之前,改动只存在于客户端缓冲区。应用可以显式调用 fsync,但就算不这样做,当写入超过块大小(默认 4M),或者在缓冲区停留超过 5 秒(可以使用 --flush-wait 调整),都会触发自动持久化。

结合上方已经介绍过的预读,缓冲区的总体作用可以一张图表示:

read write buffer

缓冲区是读写共用的,显然「写」具有更高的优先级,这隐含着「写会影响读」的可能性。举例说明,如果对象存储的上传速度不足以支撑写入负载,会发生缓冲区拥堵:

buffer congestion

如上图所示,写入负载过大,在缓冲区中积攒了太多待写入的 Slice,侵占了缓冲区用于预读的空间,因此读文件会变慢。不仅如此,由于对象存储上传速度不足,写也可能会因为 flush 超时而最终失败。

观测和调优

上方小节介绍了缓冲区对读、写都有关键作用,因此在面对高并发读写场景的时候,对 --buffer-size 进行相应的扩容,能有效提升性能。但一味地扩大缓冲区大小,也可能产生其他的问题,比如 --buffer-size 过大,但对象存储上传速度不足,导致上方小节中介绍的缓冲区拥堵的情况。因此,缓冲区的大小需要结合其他性能参数一起科学地设置。

在调整缓冲区大小前,我们推荐使用 juicefs stats 来观察当前的缓冲区用量大小,这个命令能直观反映出当前的读写性能问题。

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

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

如果希望增加顺序读速度,可以增加 --buffer-size,来放大预读窗口,窗口内尚未下载到本地的数据块,会并发地异步下载(并发度受到 --max-downloads 控制)。同时注意,单个文件的预读不会把整个缓冲区用完,限制为 1/4 到 1/2。因此如果在优化单个大文件的顺序读时发现 juicefs statsbuf 用量已经接近一半,说明该文件的预读额度已满,此时虽然缓冲区还有空闲,但也需要继续增加 --buffer-size 才能进一步提升单个大文件的预读性能。

数据缓存

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

JuiceFS-cache

内核页缓存

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

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

页缓存受内核直接管理,如果想要验证读取大文件时的确用到了页缓存,可以使用 juicefs stats 命令进行观测,如果文件读取成功,可是 fuse.read 流量为 0,说明读请求并未到达 FUSE,而是被内核页缓存截获,该请求不再由 JuiceFS 客户端服务。

举例说明,通过宿主机挂载点读取一个大文件,首次读的监控如下:

$ juicefs stats /jfs
------usage------ ----------fuse--------- ----meta--- -blockcache remotecache ---object--
cpu mem buf | ops lat read write| ops lat | read write| read write| get put
1.1% 60M 32M| 143 1.24 15M 0 | 5 1.03 | 128M 0 | 0 0 | 0 0
16.9% 169M 68M|1167 2.38 144M 0 | 2 1.27 | 120M 0 | 0 0 | 0 0
18.4% 169M 0 | 927 2.81 113M 0 | 2 1.26 | 102M 0 | 0 0 | 0 0
7.8% 170M 0 | 12 0.02 0 0 | 2 1.92 | 0 0 | 0 0 | 0 0

顺序读成功后,第二次读取则不会产生 fuse.read 流量,说明请求并未到达 FUSE,而是被内核缓存直接返回:

 0.9%  170M    0 |  14  0.01     0     0 |   0     0 |   0     0 |   0     0 |   0     0
0.9% 170M 0 | 14 0.01 0 0 | 0 0 | 0 0 | 0 0 | 0 0
1.0% 170M 0 | 17 0.09 0 0 | 1 1.25 | 0 0 | 0 0 | 0 0
0.8% 170M 0 | 16 0.02 0 0 | 2 0.64 | 0 0 | 0 0 | 0 0
0.9% 170M 0 | 13 0.02 0 0 | 2 1.89 | 0 0 | 0 0 | 0 0
1.0% 170M 0 | 13 0.02 0 0 | 0 0 | 0 0 | 0 0 | 0 0

因此,在内存富裕的节点运行 JuiceFS 客户端,空闲内存将会被有效利用,大大提升文件系统的性能。不过对于容器场景,有几点额外注意事项:

  • 应用容器访问 JuiceFS,产生的内核页缓存会“记在应用容器的账上”,因此需要根据情况调整应用 Pod 内存限制。阅读相关 Kubernetes issue 了解更多细节;
  • 多个应用容器访问产生的内核页缓存是可以复用的,不会重复占用、浪费内存;
  • 应用 Pod 访问 FUSE 挂载点会在内核产生页缓存,类似地,JuiceFS 客户端访问本地缓存时也会产生内核页缓存,因此 Mount Pod 的内存限制也需要根据实际情况进行调整,阅读 CSI 文档了解如何操作。

内核回写模式

从 Linux 内核 3.15 开始,FUSE 支持内核回写(writeback-cache)模式,内核会把高频随机小 IO(例如 10-100 字节)的写请求合并起来,显著提升随机写入的性能。但其副作用是会将顺序写变为随机写,严重降低顺序写的性能。开启前请考虑使用场景是否匹配。

在挂载命令通过 -o writeback_cache 选项来开启内核回写模式。注意,内核回写与「客户端写缓存」并不一样,前者是内核中的实现,能够在文件存在大量小随机写或追加写的场景下提升性能。后者则发生在 JuiceFS 客户端,主要适用于大量写入小文件的情况。

客户端读缓存

通过 JuiceFS 客户端访问文件系统,数据会缓存到本地文件系统中,不做压缩和加密。缓存盘可以是基于硬盘、SSD 或者内存的任意本地文件系统,因此裸盘必须进行格式化后才能使用。

JuiceFS 会默认将所有读取的数据缓存到本地。而在写的时候,则会默认把小于块大小的数据写入到缓存目录中,之所以只缓存小块数据,是因为 4M 的数据块通常是顺序写大文件所产生的,在大部分场景下缓存价值不高。如果你的场景的确需要缓存这些顺序读写的文件,继续阅读下方的 --cache-large-write

如果希望保证应用程序首次访问数据的时候就能获得已缓存的性能,可以使用 juicefs warmup 命令来对缓存数据进行预热。

注意

JuiceFS 客户端内置坏盘检测机制,如果一段时间内发生大量读盘失败(或超时),客户端将会永久摘除、弃用缓存盘。因此如果本地盘性能太差,不建议用作缓存盘。

如果缓存目录所在的文件系统无法正常工作时,JuiceFS 客户端能立刻返回错误,并降级成直接访问对象存储。这对于本地盘而言通常是成立的,但如果缓存目录所在的文件系统异常时体现为读操作卡死(如某些内核态的网络文件系统),那么 JuiceFS 也会随之一起卡住,因此一般不推荐用网络文件系统作为 JuiceFS 的缓存目录。

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

  • --cache-dir

    缓存目录,默认为 /var/jfsCache$HOME/.juicefs/cache。支持传入多个目录(用 : 分隔)以及通配符。使用多块盘进行缓存,不仅能够增加缓存空间,还可以提升总体缓存 I/O。如果场景匹配,也可以传入 /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/。注意只能删除末尾的 raw 目录,而不能整体删除 /var/jfsCache/<vol-name> 目录,这是因为该目录包含一个 .lock 锁文件,删掉他将会触发缓存数据自动重建,目的是为了在替换缓存盘的时候能够自动重建数据。

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

    缓存空间大小(单位 MiB,默认 102400)与缓存盘的最少剩余空间占比(默认 0.1)。这两个参数任意一个达到阈值,均会自动触发缓存淘汰,淘汰算法使用简单高效的 2-random 策略,也就是随机选取两个数据块,淘汰访问时间更早者。在大部分实际应用场景下,这样的策略接近 LRU,并且开销更低。

    如果不希望自动淘汰缓存,可以通过 --cache-eviction=none 选项禁用缓存淘汰,此时你需要手动管理缓存目录中的数据。

    客户端删除文件后,缓存数据块只会从当前挂载点的缓存目录中清理,而对于其他节点,如果没有到达设定的空间上限,缓存数据不会主动删除。因此如果你删除了 JuiceFS 中的某些文件,对应的缓存数据可能仍存在于其他节点的缓存目录中,只不过由于元数据一致性,残留的数据块不会再被客户端读取。

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

  • --cache-partial-only

    读取数据的时候,仅缓存小于一个块大小(默认 4M)的数据块。相当于缓存不足 4M 的小文件,以及大文件末尾不足一个块大小的数据块。默认为 false,也就是所有读取的数据块都会被缓存。

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

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

    因此该选项适用于以下场景:

    • 对象存储的吞吐比缓存盘还高,导致读取缓存在本地的大文件反而更慢;
    • 大文件只顺序读一次,不会被反复读取。这种场景下,磁盘缓存能够发挥的价值不大,也可以舍弃。
    提示

    不论是否开启该选项,JuiceFS 客户端只缓存完整的对象存储块。如果随机读取大文件中某一小段不足一个块大小的数据,这部分数据并不会被写入缓存目录(但会保存在客户端内存、内核页缓存,因此反复读取性能佳)。如果 --prefetch 大于 0,那么随机读对应的完整数据块会通过预取机制被异步下载到缓存盘。

    --cache-partial-only 选项同时也会对分布式缓存的缓存构建造成影响。当客户端开启了 --fill-group-cache 选项时,客户端会将所有写入的数据都发给缓存集群。此时如果也开启了 --cache-partial-only 选项,会导致客户端仅将小于一个块大小(默认 4M)数据发给缓存集群。

  • --cache-large-write

    上方介绍的 --cache-partial-only 用来控制「读取数据时,哪些数据会被缓存」。而 --cache-large-write 这个参数则用来控制「写数据的时候,哪些数据会被缓存」。因为更多场景下,顺序写的缓存价值不大,因此 JuiceFS 客户端默认不会将大文件顺序写进行缓存,而是只缓存小于块大小的写入。如果开启 --cache-large-write,那么所有完整的数据块,都会随着写入而被缓存。

    如果使用了分布式缓存,并且开启了 --fill-group-cache,那么相应地,顺序写产生的完整对象存储块就会被发送到缓存组,不再存储于本地缓存盘。

客户端写缓存

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

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

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

client write cache

警告

启用写缓存以后,在上传未完成之前,数据是无法被其他客户端读到的(客户端尝试读取未上传完成的数据,请求会卡住直至超时)。由于数据可能会在本地盘停留很长时间,数据的可靠性与缓存盘的可靠性直接相关,如果在上传完成前本地数据遭受损害,意味着数据丢失,因此对数据安全性要求越高,越应谨慎使用。

使用前务必注意:

  • 待上传的文件默认存储在 /var/jfsCache/<vol-name>/rawstaging/,这个目录存放着待上传的文件数据,误删或损坏意味着数据丢失。
  • 写缓存大小由 --free-space-ratio 控制。默认情况下,如果未开启写缓存,JuiceFS 客户端最多使用缓存目录 80% 的磁盘空间(计算规则是 (1 - <free-space-ratio>) * 100)。开启写缓存后会超额使用一定比例的磁盘空间,计算规则是 (1 - (<free-space-ratio> / 2)) * 100,即默认情况下最多会使用缓存目录 90% 的磁盘空间。
  • 写缓存和读缓存共享缓存盘空间,因此会互相影响。例如写缓存占用过多磁盘空间,那么将导致读缓存的大小受到限制,反之亦然。
  • 如果本地盘写性能太差,带宽甚至比不上对象存储,那么 --writeback 会带来更差的写性能。
  • 如果缓存目录的文件系统出错,客户端则降级为同步写入对象存储,情况类似客户端读缓存
  • 如果节点到对象存储的上行带宽不足(网速太差),本地写缓存迟迟无法上传完毕,此时如果在其他节点访问这些文件,则会出现读错误。低带宽场景的排查请详见「与对象存储通信不畅」
  • 新增自 v4.9.22 如果启用了 --fill-group-cache,那么写缓存的数据也会异步写入分布式缓存组,也就是说,就算还未上传至对象存储,缓存组成员也有一定概率能读到尚未持久化完成的数据。

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

分布式缓存

单节点缓存总会存在性能和存储空间的上限,为了突破这个限制,JuiceFS 支持「分布式缓存」(也称作客户端缓存共享),将海量的数据缓存到多个 JuiceFS 客户端,有效提升性能。阅读「分布式缓存」详细了解。

观测与监控

我们的 Grafana Dashboard 中已经包含了众多关键的缓存相关监控图表,对于单机缓存,可以在 Grafana Dashboard 参考以下相关指标:

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

单机缓存指标不仅仅用来观测单机缓存的运行情况,分布式缓存场景下,也需要通过各成员节点的 Block Cache 相关指标,来确定缓存组的整体表现。详见「分布式缓存」