Skip to main content

分布式缓存

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

架构

启用了分布式缓存后,多个客户端会构成一个「分布式缓存组」,每一个缓存组实际上都是一个一致性哈希环,缓存块则根据一致性哈希算法分布在组内各个成员节点,并在组内共享。缓存组内的每个成员,其实都是正常挂载的客户端,它们通过 --cache-group 设置组名。这样一来,具有相同组名的成员就构成了一个缓存组,成员节点间的通信也只会发生在组内:任何一个成员在读取文件时,会根据目前缓存组拓扑结构计算出当前数据块所在的节点,并向该节点发起请求,如果数据尚未存在于该节点,也会由该节点从对象存储下载数据并存储在缓存盘中。

分布式缓存架构

以上图为例,节点 D 读取文件 a(由 4 个数据块组成,a1-a4)时,假设根据一致性哈希算法,得出 a1-a4 分别坐落于节点 A-D,因此每个成员节点都会访问对象存储,下载所需的数据块。下载完毕后,节点 A-C 再通过内网通信,将数据块返回给节点 D,完成文件读取。

网络环境和数据安全

目前而言,JuiceFS 分布式缓存服务假定部署在高性能、安全的内网环境。如果需要在公网部署分布式缓存服务,需要重点考虑的事项有:

  • 公网的网络质量通常远逊于对象存储服务,需要提前对网络质量进行验证。如果延迟和吞吐太差,则考虑放弃使用缓存组、直接从对象存储下载;
  • 缓存组的监听服务本身不设有身份认证,直接对公网服务存在数据安全风险,需要妥善管理访问策略。

服务发现

分布式缓存会以元数据服务作为服务发现:客户端会把自身监听的 IP 和端口汇报给元数据服务,也会从元数据服务获取同一个缓存组内的其他成员的连接方式,进而发现其它客户端,并通过节点间通信来共享缓存。

缓存组成员读写数据时会通过节点间通信来完成,在这个过程中如果面临任何通信失败,则会在此次请求中放弃使用缓存组,改为本地执行。仍旧以上方架构图来举例说明,D 读取文件时,如果与 A 节点通信失败(比如请求超时),无法获取数据块 a1,那么 D 将会改为直接从对象存储下载 a1,来完成此次读请求。

如果和某个缓存节点通信多次仍然失败,则会认为这个节点无法连接,将它从缓存组中移除。

信息

面对缓存组异常时,JuiceFS 客户端会优先保证自身的正常服务,这也意味着,如果客户端与缓存节点的通信出现问题,则可能出现大量请求穿透到对象存储、影响性能。因此为了保证缓存组正常运行,需要关注:

扩容和数据均衡

缓存组内如果发生了成员节点增删,数据会向哈希环的临近节点做迁移(为了防止波动,实际会等待成员变更后约 10 分钟,方执行迁移操作)。可见缓存组的成员变更,只影响到部分数据块的缓存命中率。在缓存组一致性哈希环的实现中,也采用了虚拟节点(virtual node)的概念,确保数据分布均衡,避免因数据迁移产生访问热点,影响缓存组性能。关于一致性哈希、虚拟节点的架构,可以阅读这篇文章了解更多。

缓存副本

JuiceFS 的分布式缓存功能默认只将数据缓存一份,并不为其创建副本。在缓存组节点不发生频繁变更、内网通信稳定的前提下,这样便是最高效的安排。

但是在某些特殊场景下,可以为 JuiceFS 客户端设置缓存副本功能,来提升缓存服务的稳定性。因此从 JuiceFS 5.1 开始,客户端挂载时可以指定 --group-backup 参数来启用缓存副本,他的原理如下:

启用了 --group-backup 的缓存组客户端,在面临缓存请求未命中时,会根据缓存块键值(cache key)计算出该数据块对应的“缓存备份节点”,然后将请求转发给该成员节点,由他来代处理该请求。“缓存备份节点”则会根据本地是否已经缓存了该数据,来决定是直接返回数据,还是穿透到对象存储请求。相比没有启用副本的缓存组架构,缓存块途径了 2 个缓存组成员节点才最终到达客户端,因此理想情况下,缓存块也会坐落在在两个成员节点上。

在不过多考虑特殊情况的前提下,缓存副本的架构图如下:

cache group backup

在上图中:

  1. 客户端欲请求数据块 b1,根据缓存组的一致性哈希算出数据块应位于节点 A。
  2. 节点 A 尝试服务该请求但本地尚未缓存 b1,因此再次根据 b1 的键值计算出其备份节点为 B,并转发请求给 B。
  3. 节点 B 收到请求后也是一样未命中,因此一路穿透到对象存储,才最终获得该数据。

如果在「客户端→A→B→对象存储」的任何一个中间环节发现缓存数据已经存在,则请求会提前返回,不会再穿透到下一个环节。也正因此,我们的「缓存副本」功能并不严格保证每一个数据块都建立 2 份本地缓存,如果发生缓存空间不足、数据被淘汰,或者提前命中,都有可能导致缓存数量不达两副本的预期。

缓存副本功能的目标场景是:

  • 缓存成员节点频繁变动,希望每次变动的时候尽可能保证命中率稳定;
  • 缓存组需要扩容,希望扩容的时候尽可能保证命中率稳定;
  • 缓存组节点间网络质量差,波动大,频繁有节点掉线断连,希望尽可能提升命中率。

相应的,以下场景不适合开启缓存副本功能,或者至少不能持续开启:

  • 缓存空间本就不足,考虑到开启副本会进一步增加空间占用,缓存服务能力只会进一步下降;
  • 缓存副本功能不能用来提升吞吐,这也是用户面对该功能的常见误区,缓存组的吞吐上限和节点间通信带宽、缓存盘的能力有关,增加副本数没办法提升吞吐。另一方面,如果你担心访问热点问题,JuiceFS 原本就会将缓存数据打散分布在一致性哈希环上,来均衡访问压力,并不会因为因为副本数增加,而更加分散。

启用分布式缓存

分布式缓存组就是由一个个 JuiceFS 客户端组成的,在内网互通的多个节点上像这样运行挂载命令,就能组建分布式缓存:

# 将 $VOL_NAME 替换为文件系统名,--cache-group 是用户自定义的缓存组名称
juicefs mount $VOL_NAME /jfs --cache-group=mygroup

集群网络往往很复杂,每个节点都绑定了多块性能不同的网卡,为了让缓存组达到最高性能,推荐将性能最好的网卡分配给缓存组,用于节点间通信:

# --group-ip 就是缓存组绑定的网卡,IP 不需要写完整,指定前缀即可,实现绑定网段的效果
# 比方说该节点对应的网卡 IP 是 10.6.6.1,对应的 CIDR 是 10.6.0.0/16
# 那么填写 --group-ip=10.6.0.0 就能智能选中该网卡
juicefs mount $VOL_NAME /jfs --cache-group=mygroup --group-ip=10.6.0.0

挂载完毕以后,可以查看客户端日志,或者直接查看 TCP 连接,来确认缓存组已经启用:

# 在日志中查看「peer」字样,就能看到缓存组相关日志
# 例如 <INFO>: Peer listen at 172.16.255.181:36985 [peer.go:790]
grep peer /var/log/juicefs.log

# 直接打印客户端发起的 TCP 连接,和正在监听的分布式缓存端口,确认缓存组已经在工作
ss -4atnp | grep jfsmount

缓存组搭建完毕,但组内成员节点还没有缓存任何数据,为了提升应用首次访问的速度,常常会事先运行预热命令,将所有需要用到的数据提前缓存下来:

# 在任意缓存组成员节点运行预热命令,效果相同
# 如果规模庞大,使用 -c 增加并发度,加速预热
juicefs warmup /jfs/train-data -c 80

特别地,还可以通过 --fill-group-cache(默认关闭),让客户端在写入数据的同时,将上传到对象存储的数据块同时发给缓存组,让写操作也能参与缓存集群的建设。需要特别注意,发送数据块给缓存组时有可能失败,因此不保证一定能被缓存。

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

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

独立缓存集群

在分布式缓存方案中,每个客户端均参与缓存组的建立。但如果遇到客户端不是常驻的情况,比如 Kubernetes 集群中,客户端不断被销毁、重建,可能导致缓存利用率很低。对于此类动态创建伸缩的计算集群,可以将文件系统挂载在固定机器(或容器)上,组成「独立缓存集群」来给对象存储加速。

独立缓存集群架构

应用侧的客户端会加入缓存组、从缓存组获取数据,但这些客户端会启用 --no-sharing 挂载参数,它的意思也很直白:加入缓存组,但却不分享自己的缓存数据,只会向缓存集群索取数据,避免转瞬即逝的应用侧客户端频繁变动,影响缓存集群的服务能力。

独立缓存集群本质上就是一个分布式缓存组,只不过缓存的提供方和使用方是不同的 JuiceFS 客户端。因此在该场景下,我们常用「缓存集群」来指代这个常驻、稳定的缓存组,用「客户端」来指代应用侧的 JuiceFS 客户端——即使本质上他们都是 JuiceFS 客户端,只是使用了不同的挂载参数。

建设独立缓存集群,首先要搭建一个缓存组,在 Kubernetes 集群中常常用 DaemonSet 或 StatefulSet 来部署缓存集群(详见我们的示范),此处简单起见,直接以上一小节里已经搭建好的缓存组作为独立集群进行示范,那么客户端可以使用下方命令来连接这个缓存集群:

# 应用侧客户端添加 --no-sharing 参数,虽然加入缓存组,但不向其他成员提供分布式缓存服务
juicefs mount $VOL_NAME /jfs --cache-group=mygroup --no-sharing

# 挂载完毕以后,如果有需要,也可以直接在 --no-sharing 挂载点上执行预热,同样能将数据预热到整个缓存组
juicefs warmup /jfs/train-data -c 80

挂载完毕后,这些客户端就会以「只索取,不分享」的方式来使用缓存集群的数据了,注意即便对于这些客户端,多级缓存的设计依然生效:从缓存组获取到的数据,依然会缓存到本地。如果你希望客户端完全不保留任何本地缓存,所有文件访问都通过缓存集群来完成,那么可以通过 --cache-size=0 选项禁用客户端的本地缓存:

# 应用侧客户端禁用本地缓存,让所有读请求都走缓存集群
juicefs mount $VOL_NAME /jfs --cache-group=mygroup --no-sharing --cache-size=0

禁用客户端本地缓存,能够最大程度利用独立缓存集群的缓存盘,节约客户端节点磁盘空间。如果客户端的磁盘吞吐甚至比不上内网带宽,也可以使用这个方法获得更好的性能。

启用了 --no-sharing 的客户端,还有以下注意事项:

  • 客户端节点如果没有命中缓存,会由独立缓存集群负责下载并缓存数据,再提供给客户端。除非节点间通信出现问题导致超时失败,否则客户端自身不会直接从对象存储下载文件。
  • 多个客户端同时访问同一个未缓存的文件,也只会发生一次缓存穿透,对应的缓存组成员会下载好并返回给客户端,不会造成多次请求对象存储。

多级缓存架构

先前在单机缓存介绍过的多级缓存架构,在分布式缓存下同样生效,也就是单节点自身的多级缓存之上,还有来自缓存组节点的多级缓存:

独立缓存集群多级缓存架构

需要注意的是,分布式缓存下的多级缓存,并非单机客户端的多级缓存的简单叠加:客户端通过缓存组读取数据时,由于缓存组一侧并未通过 FUSE 读取数据,而是读取磁盘缓存数据、直接发送给对端成员,因此会在单机文件系统层面建立内核页缓存。而客户端一侧是通过 FUSE 读取数据,因此会通过 FUSE 建立内核页缓存。

如果客户端侧设置了 --cache-size=0,希望最大程度利用缓存集群空间、节约本地空间,这时 JuiceFS 客户端内存会预留 100MiB 的空间作为缓存空间,加上在进程内存之上同样会建立内核页缓存,反复读取的文件一样能获得极致性能。

提示

从 5.0 开始,JuiceFS 客户端删除文件后,如果他加入了缓存组(即便是 --no-sharing),那么其所在的缓存组,也会主动清理本地缓存。

混合部署缓存组

上一小节提到禁用本地缓存的实践,适用于客户端本地盘性能较差的场景,如果情况相反,希望最大程度利用客户端本地缓存盘的 I/O 能力,减少节点间的网络通信,可以考虑在所有节点混合部署两个 JuiceFS 客户端:一个是缓存集群挂载点,另一个则是业务侧实际使用的挂载点。

混和部署缓存组架构

这样的混合部署策略的好处是:

  • 高性能缓存盘同时用作分布式缓存和本地缓存,更充分利用本地高性能 I/O;
  • 所有节点都加入了同一个缓存组,读任何文件,都只需要通过缓存组请求一次。这样一来,对象存储请求被合并到了缓存集群,相比没有分布式缓存的孤立部署方案,大大减少了对象存储请求量。

混合部署需要在同一批节点上分别挂载缓存集群,以及 --no-sharing 客户端。假设缓存盘大小共 1TB(即 1000000MB),挂载命令示范如下:

# 缓存集群挂载点
# --cache-size=500000 表示最多使用 0.5TB 缓存盘空间
# --free-space-ratio=0.1 表示最多使用 SSD 盘 90% 的空间
juicefs mount $VOL_NAME /distributed-cache --cache-group=mygroup --cache-dir=/data/distributed-cache --cache-size=500000 --free-space-ratio=0.1

# 应用挂载点
# --cache-size=1000000 表示最多可以用满 1TB 缓存盘空间
# --free-space-ratio 0.01 表示最多使用 SSD 盘 99% 的空间
juicefs mount $VOL_NAME /jfs --cache-group=mygroup --cache-dir=/data/local-cache --cache-size=1000000 --free-space-ratio=0.01

虽然二者共用同一块缓存盘,但分别管理自己的缓存数据,因此缓存目录(--cache-dir)并不相同,不能混用(否则会产生冲突)。可想而知,这样的规划会产生冗余,随着文件读取,数据会被分别缓存在缓存集群,以及客户端本地,这样的多级冗余也是提升性能的必要开销。不过上方的命令中已经对参数进行了调优,让整体架构更偏好本地 I/O:本地客户端挂载点设置更小的 --free-space-ratio 和更大的 --cache-size,这样就能保证当缓存盘写满时,能够先释放分布式缓存数据。

混合部署方案可以根据集群规格和场景需要灵活调整,比方说节点有大量富余内存,也可以直接使用内存作为缓存盘,获得更快 I/O:

# 缓存集群挂载点
# 根据富余情况调整 --cache-size,避免占用过多内存
juicefs mount $VOL_NAME /distributed-cache --cache-size=4096 --cache-dir=/dev/shm --cache-group=mygroup

# 应用挂载点
# 为了最大化本地缓存盘的利用率,将 --free-space-ratio 调小
juicefs mount $VOL_NAME /jfs --cache-size=102400 --cache-dir=/data --cache-group=video-render --no-sharing --free-space-ratio=0.01

运维

摘盘、换盘操作

从 5.1 开始,JuiceFS 客户端支持「缓存副本」功能,启用该功能后,客户端会将未命中的请求转发给其他节点来“帮忙”,这样的功能允许用户更平滑地应对摘盘、换盘操作,因此对于 5.1 及以上版本客户端,建议用下方步骤操作:

信息

下方步骤假定用户只缓存一份数据,因此 --group-backup 是临时使用、用完关闭。但如果你的集群已经启用了双副本,那么所有的挂载点就都已经启用了 --group-backup 参数,因此下方步骤中关于缓存副本的操作省略即可。

  1. 把待摘除盘中的数据拷贝到其他盘,如果单盘的空间有限放不下待迁移数据,可以对数据进行分组,视空闲容量拷贝到其他盘;
  2. 调整以下挂载参数:
    • 增加 --group-backup 参数,这样一来,挂载点重启以后,客户端面对未命中的请求,会转发给其他客户端,让他们代为服务;
    • 修改 --cache-dir,删去旧盘;
    • 摘盘导致该节点缓存容量总大小发生变化,调整 --group-weight 来降低权重,避免节点间空间不一致导致的利用率不理想。
  3. 参数修改完毕后,平滑重启客户端使这些修改生效;
  4. 重启后的客户端会重新扫描缓存目录并再次加入缓存组。如果权重有所调整,那么也会在缓存组内重新均衡数据;
  5. 数据均衡以后,为了避免缓存副本功能额外占用过多空间,因此删去 --group-backup 参数,再次平滑重启挂载点。

如果仍在使用 5.1 之前的版本,那么 JuiceFS 客户端同样支持在多块盘之间迁移数据,但是由于不支持缓存副本功能,因此可能会产生更大的命中率波动。操作如下:

  1. 把待摘除盘中的数据拷贝到其他盘,如果单盘的空间有限放不下待迁移数据,可以对数据进行分组,视空闲容量拷贝到其他盘;
  2. 调整以下挂载参数:
    • --cache-dir,删去旧盘;
    • 摘盘导致该节点缓存容量总大小发生变化,应该相应调整 --group-weight 来降低权重,避免节点间空间不一致导致的利用率不理想。
  3. 参数修改完毕后,平滑重启客户端使这些修改生效;
  4. 重启后的客户端会重新扫描缓存目录并再次加入缓存组。如果权重有所调整,那么也会在缓存组内重新均衡数据。

依照和上方步骤相同的原理,换盘的过程也是类似的,拷贝数据到新盘,然后视情况调整参数、重新挂载即可。

成员节点变更

对于增加节点的计划运维事件,建议利用 JuiceFS 5.1 的缓存副本功能来减小迁移期间的命中率波动。而对于删除节点的计划运维事件,建议通过调整缓存组权重来减小命中率波动。

信息

下方步骤假定用户只缓存一份数据,因此 --group-backup 是临时使用、用完关闭。但如果你的集群已经启用了双副本,那么由于数据已经缓存 2 份,增删节点本身的影响已经降至最低,不需要额外的特殊操作。

如果你的缓存集群部署在 Kubernetes 集群中,那么由于挂载参数难以针对单个节点进行调整,下方介绍的修改权重的流程将无法得到应用。你需要提前将问题节点切换成宿主机挂载方式,让 JuiceFS 客户端脱离 Kubernetes 的管理,才能利用客户端调整参数、平滑重启的功能,实现缓存组成员的变更。

计划增加节点时,按照以下步骤操作:

  1. 新节点挂载 JuiceFS、加入缓存组,注意对于该节点追加 --group-backup 参数,新节点的缓存盘是空的,用缓存副本功能避免该节点大量穿透到对象存储;
  2. 运行一段时间后,观察对象存储的穿透流量降低至稳定,此时对新节点去除 --group-backup,避免缓存副本持续过多占用空间。

而对于删除节点的场景,JuiceFS 客户端支持通过 --group-weight 调整成员节点权重,该参数不仅可以用来组建异构缓存集群(空间不等的节点组建缓存组),也可以在面对有计划的节点删除时,用来显式迁移缓存数据。按照以下步骤操作:

  1. 对即将删除的节点额外追加 --group-weight=0

    # 将待下线节点权重调整为 0,就能显式触发缓存数据迁移
    juicefs mount myjfs /jfs --cache-group=mygroup --group-weight=0

    这一步需要注意:

    • 将权重调整为 0 以后,数据会在重新挂载 10 秒后开始迁移(而不是如同节点重启或下线等情况,需要等待 10 分钟方启动迁移)。由于数据分布变更,这部分数据迁移期间如果被访问,会产生缓存击穿,具体评估影响的方法请继续阅读下一小节,了解如何评估节点变更产生的冲击;
    • 数据迁移未完成前,被访问到的部分会产生穿透,如果希望尽可能减少穿透,那么不要一次性减为 0,而是在可接受的范围内,渐进式地降低权重;
  2. 数据持续迁移期间,可以同步观察监控面板,通过观察操作节点的缓存数据量(blockcache_bytes)或者缓存写流量(remotecache_putBytes)来判断迁移进度。同时也可以关注对象存储流量和 I/O 延迟,来判断文件系统当前的缓存穿透情况和性能级别。待删除节点的本地缓存量降为 0 或接近 0,并且观察其他节点的缓存数据量增长至稳定,可以删除该节点。

评估成员节点变更的影响

本小节以重启节点为例,介绍缓存组成员节点变更操作时,应该如何评估对缓存组的影响。节点重启操作一般不会超过 10 分钟,因此重启并不会引发数据迁移(参考架构)。但由于节点下线期间,对应的缓存数据无法访问,因此重启一定会引发缓存命中率波动,为了对影响进行量化,需要根据控制台中文件系统「监控」页面的数据进行预估。

假设需要重启的成员是节点 A,那么打开文件系统的监控页,首先关注缓存组的整体用量,可以直接在「分布式缓存」板块看到:

分布式缓存整体监控指标

从上图中的「Number of Distributed Cache Requests」可以看出,缓存组当前整体请求量约为每分钟 4300 次。

接下来查看节点 A 在缓存组中的请求负载,在页面右上角的「所有客户端」下拉框中过滤出节点 A,然后查看节点 A 的本地缓存访问情况:

分布式缓存成员节点监控指标

从上图中的「Average Size of Cached Blocks」可以看出,节点 A 有共计 13TiB 左右的缓存数据(由于已经过滤出了单个节点,这个数值就是该节点的缓存数据量),当前请求量(Cache Hits)约为每分钟 130 次,因此下线该节点后,受影响的请求占比为 130 / 4300 = 3%。也就意味着,在节点 A 重启期间,会有 3% 的读请求穿透到对象存储上,由一致性哈希环上的临近节点代为请求。节点 A 重启以后重新加入缓存组,由于操作时间不满 10 分钟,缓存组的拓扑结构没有任何变化,因此不会发生数据迁移(少量数据会重新在临近节点建立缓存)。

对于缓存组而言,重启节点可以视为「退出缓存组,然后迅速重新加入」。如果你需要从缓存组中永久驱逐某个节点,那么评估方法也是类似的,直接用上述流程计算该节点的请求量占比、本地缓存数据量,就能得出删除节点对集群的影响。

如果缓存组总容量不足(阅读下方「观测与监控」小节了解如何判断当前用量、判断是否需要扩容),决定增加节点,那么当新节点加入集群满 10 分钟以后,集群会开始重新均衡缓存数据,来最大化利用空闲缓存空间和 I/O 能力。在迁移过程中,客户端如果访问受影响的数据,同样会发生穿透。

假设缓存集群总数据大小为 500TiB,共计 40 个缓存节点,需要扩容到 50 个节点,根据不同的节点网络带宽,数据均衡所需的时间如下:

提示

「理论数据均衡时间」在计算时假设能够利用到全部网络带宽,这在真实场景下是不可能的,因此计算结果仅供参考。

缓存集群数据总量当前节点数扩容后节点数网络带宽理论数据均衡时间
500TiB405010Gbps2h26m
500TiB405025Gbps58m

具体计算方法:

  1. 扩容前每个缓存节点的数据量约为 500 / 40 = 12.5TiB,扩容并均衡数据后的数据量约为 500 / 50 = 10TiB,也就是说每个节点需要迁移 12.5 - 10 = 2.5TiB 数据,总共需要迁移 40 * 2.5 = 100TiB 数据。
  2. 由于存在虚拟节点,虽然只新增了 10 个物理节点,但实际上在一致性哈希环中会新增成百上千个「虚拟节点」。原集群的 40 个节点会将这 100TiB 数据均匀地传输给新增的这 10 个节点。
  3. 假设节点间网络带宽为 10Gbps,这个节点总共需要接收 10TiB 的数据,那么传输时间为 10TiB / 10Gbps = 2h26m

观测与监控

我们的 Grafana Dashboard 里已经包含了分布式缓存的相关监控面板:

  • Remote Cache Requests(分布式缓存请求量)
  • Remote Cache Throughput(分布式缓存吞吐)
  • Remote Cache Latency(分布式缓存请求时延)

关于分布式缓存命中率,一个最为重要的注意事项:**客户端如果发生命中率(hit ratio)下降,其实并不意味着缓存组被击穿。**客户端读取文件时,如果本地没有,则会访问缓存组成员获取数据。也正因此,数据访问在消费端记为 miss,而在缓存组服务提供端记为 hit。因此下次如果注意到客户端的命中率降低,可以先检查有没有对象存储流量的波动,来迅速判断是否发生了击穿、影响访问性能。

正因上边谈到的可以通过计算整个缓存组的「对象存储流量 / 总 I/O」来大致估计当前的总体命中率,通过 Prometheus 查询来表示的话,示范如下:

# 取决于你所在的环境是云服务还是私有部署,指标前缀可能有区别,注意替换
# 私有部署用户可以直接查看 volume overview 仪表盘,已经内置了该查询
1 - (sum(rate(mount_get_bytes{owner="", subdir="$name", cache_group!="", cache_group=~"${cache_group:raw}"}[1m])) / sum(rate(mount_read_bytes{owner="", subdir="$name", cache_group!="", cache_group=~"${cache_group:raw}"}[1m])))

上述查询的分母是 read_bytes,也就是通过 FUSE 读取文件系统的总流量,这可能会包含许多非本地盘访问的流量。如果希望更精确计算命中率,也可以用 hits / (hits + object_get),其中 hits 代表缓存组成员节点的本地缓存命中,object_get 代表穿透到对象存储的请求数。请参考具体缓存指标、自行撰写相关查询。

列举一些常见的缓存集群监控操作:

  • 可以检查成员节点的「Block Cache Hit Ratio」以及「Object Requests」的指标来判断缓存组是否发生穿透,如果存在大量穿透,检查「Block Cache Size」来判断当前已用缓存空间,以及是否需要扩容。
  • 缓存集群空间不足时会发生块换出(eviction),可以通过 juicefs_blockcache_evict 等相关指标来监控。详细的监控指标列表见监控项说明
  • 检查缓存组服务端的请求延迟,也就是「Remote Cache Latency (server)」,如果存在部分节点延迟异常高,则说明缓存组可能存在「害群之马」,可能需要检查问题节点的挂载方式,加上 --no-sharing 参数来阻止低性能节点参与缓存组服务。

问题排查

如果在使用分布式缓存时发现性能不佳,比如大量请求穿透到对象存储,那么在开始排查前,我们推荐:

  • 如果方便的话,为所有缓存组成员节点启用 DEBUG 日志,同时注意日志中的 peer 字样——缓存组成员变动时,日志里往往含有这个单词。
  • 在监控页面或 Grafana 查看「Remote cache」相关监控面板。

为什么预热后仍然会访问对象存储?

warmup 命令会将文件用到的所有数据块载入缓存(单机缓存或者分布式缓存),有以下几种情况可能导致之后的访问缓存不命中:

  1. 文件或者数据本身发生变化,有新数据写入,或者存在碎片合并操作产生了新的数据块。
  2. 因为缓存空间不够导致已经缓存的数据块被换出。如果缓存空间已满,达到设定的数据量(--cache-size 指定)或者缓存盘的剩余空间达到设定的阈值(--free-space-ratio),已有缓存数据会被 2-random 算法随机换出,可能包括当前预热命令刚刚载入的数据块。可以通过查看监控面板中缓存集群的缓存淘汰相关指标(「Block Cache Eviction Rate」)来确认。

cache eviction monitor

  1. 预热命令在访问部分数据块时失败了,可以在执行预热命令的客户端日志文件中看到相应的失败日志。可以通过参数 --max-failure 来指定最大允许的失败次数,当有更多数据块预热失败时,它会以非 0 退出码结束。

juicefs warmup 命令作用于当前挂载点上,因此对于该挂载点,增加 --verbose 参数,开启打印 DEBUG 日志,阅读报错日志。比方说:

<ERROR>: xxx could be corrupted: chunk x:x:xxx is not available: read header: EOF

这代表着缓存组成员在下载对象存储块时发生了失败,可以继续查看缓存组成员的客户端日志来进一步明确失败原因。

缓存组成员节点,是否有多块网卡?绑定网卡是否正确?

如果缓存集群成员节点有多块网卡,所属的网络不互通,或者带宽受限,不适合缓存集群使用,这时需要确定合适的网卡,你可以用类似下方的命令,来确认网络互通:

# 进入缓存集群成员节点或容器
# 对于缓存组成员,JuiceFS 客户端会随机监听本地端口,用于组内成员通信
# 默认 pprof 会监听 6060 与 6070,分布式缓存服务的监听端口通常是最大的那一个
lsof -PiTCP -sTCP:LISTEN | grep jfs

# 进入客户端节点或容器,尝试连接缓存集群端口,确认网络连通性
telnet [member-ip] [port]

如果排查发现确实需要更换缓存集群的监听网卡,那么需要在挂载命令用 --group-ip 指定对应的网卡 IP。注意,--group-ip 还可以指定 CIDR 前缀,比方说需要监听的网卡 IP 段为 172.16.0.0/16,那么可以直接指定 --group-ip=172.16.0.0,这样一来,缓存集群节点就可以使用统一的挂载参数,方便管理。

缓存组成员内网带宽是否受限?

组建 JuiceFS 分布式缓存的节点建议至少以万兆网互联,如果带宽不足或者负载太高,则可能发生如下报错:

# 内网带宽太差,或者网络不互通
<INFO>: remove peer 10.8.88.242:40010 after 31 failure in a row [peer.go:532]

# 如果内网互通,但只是网络质量太差,成员被移除出缓存组后,还可能迅速被加回缓存组
<INFO>: add peer 10.8.88.242:40010 back after 829.247µs [peer.go:538]

# 从组内成员获取数据块失败
<WARNING>: failed to get chunks/6C/4/4020588_14_4194304 for 10.6.6.241:38282: timeout after 1m0s [peer.go:667]
<ERROR>: /fio_test/read4M-seq.2.0 could be corrupted: chunk 1:0:4020660 is not available: read header: read tcp 10.8.88.241:34526->10.8.88.242:40010: i/o timeout [fill.go:235]

# 下载数据块太慢
<INFO>: slow request: GET chunks/6E/4/4020590_0_4194304 (%!s(<nil>), 105.068s)

确认组内成员网络互通,并且使用万兆网络。如不确定网络质量,可以使用 iperf3 等工具进行测速。

如果集群的硬件条件无法轻易改善(升级网卡、硬盘),那么务必使用 5.1 及以上版本的 JuiceFS 客户端。这是因为在 5.0 和从前的版本,JuiceFS 的缓存组请求超时时间为 10 秒(header 传输超时)和 1 秒(body 传输超时)。缓存组超时在 5.1 修改为 65 秒,并且改善了并发控制,提升缓存组在网络拥堵情况下的稳定性。

缓存组是否存在「害群之马」?

缓存组能够高性能运作的前提是:各个节点的网络性能、可用磁盘空间相等或接近。如果某一个成员性能或者网络状况很差,则会拖累整个缓存组:

cache group black sheep

如图所示,假设缓存数据均匀分布在三节点的缓存集群上,但有一个节点网络极慢(可能是 --group-ip 不恰当,导致绑定了错误的网卡,也可能是将一台其他网络环境的节点错误地加入了缓存组),那么可想而知,1/3 的请求将会由慢节点来服务,缓存组的性能会系统性下降。

在更大规模缓存集群中,害群之马问题对整体的性能的影响可能会十分难以排查,比方说 200 节点的缓存组中有一个低性能节点,那么总体受影响的请求占比会非常低,容易被忽视。此时需要检查「Remote Cache Latency (server)」,如果存在部分节点延迟明显高于其他节点,那么他们可能正是缓存组性能问题的罪魁祸首:

cache group black sheep monitor

如上图所示,缓存组中有一个成员延迟特别高,就有可能恶化所有客户端的请求延迟。在「Remote Cache Latency (server)」也就是缓存组服务端的监控面板中,对延迟进行排序,如果「Send」一列存在某一个明显异常高的节点,那么需要检查问题节点的挂载方式,加上 --no-sharing 参数来阻止低性能节点参与缓存组服务。

缓存组能否高速访问对象存储服务?

如果缓存组成员无法快速从对象存储上下载数据,现象与报错类似于上一小节中内网带宽受限的情况。可以降低下载并发度来尝试获得更平稳的预热体验,比如 juicefs warmup --concurrent=1

客户端负载是否过高?

客户端负载过高,在特定情况下也能引起分布式缓存命中率低。比方说客户端启用了 --fill-group-cache 选项,将数据写入文件系统的同时,还会将数据贡献给分布式缓存集群。如果这些参与写入的客户端负载过高,虽然仍能够正常写入 JuiceFS 文件系统,但分布式缓存的写入则可能失败(缓存写入不会进行重试),而一旦失败,读取这些文件就会产生穿透到对象存储的请求,造成缓存命中率低。