在计算与存储分离的架构下,使用Fluid数据缓存技术,能够有效解决在Kubernetes集群中访问存储系统数据时容易出现的高延迟及带宽受限问题,从而提升数据处理效率。本文从性能维度、稳定性维度、读写一致性维度介绍如何使用Fluid数据缓存策略。
使用场景
缓存技术依赖局部性原理提升多次数据访问的平均性能,许多数据密集型应用场景均存在这样的局部性特征,例如:
AI模型训练场景:AI模型训练过程中,相同的数据集将被按轮次(Epoch)周期性读取,用于AI模型的迭代收敛过程。
AI模型推理服务启动场景:当发布或更新某AI模型推理服务时,大量推理服务实例被并发拉起,各推理服务实例并发读取存储系统中相同的模型文件,并加载到推理服务实例的GPU内存中。
大数据分析场景:在大数据分析过程中,部分数据会被一个或多个数据分析任务共同使用。例如:在使用SparkSQL分析用户画像和商品信息时,订单数据将被这些分析任务共同使用。
因此,以上这些场景,均可以使用Fluid数据缓存技术提升数据处理效率。
性能维度的Fluid数据缓存优化策略
为了使用Fluid构建的数据缓存有效提升数据访问的效率,您需要根据业务的性能需求以及预算成本合理配置Fluid数据缓存使用的ECS机型、缓存介质、缓存系统参数配置、缓存管理策略等。另外,还需要考虑客户端数据读取方式对性能的影响。您可以通过上述策略的综合运用,有效提升Fluid数据缓存性能表现,本节将详细介绍这几种策略。
策略一:选择缓存系统ECS机型
性能估算
分布式文件缓存系统能够聚合多个节点上的存储资源和带宽资源,为上层应用提供更大的缓存容量和更高的可用带宽。关于缓存容量、可用带宽以及应用数据访问的最大带宽的理论上限值可根据以下公式估算:
缓存可用容量 = 每个分布式缓存Worker Pod提供的缓存容量 * 分布式缓存Worker Pod副本数
缓存可用带宽 = 分布式缓存Worker Pod副本数 * min{Worker Pod所在ECS节点的最大可用带宽, Worker Pod使用的缓存介质I/O吞吐}。另外,缓存可用带宽不代表数据访问过程中的实际带宽,数据访问过程的实际带宽受客户端ECS节点可用带宽,以及数据访问模式(顺序读、随机读)影响。
应用Pod数据访问过程的理论最大带宽 = min{应用Pod所在ECS节点的可用带宽,缓存可用带宽}。另外,当多个应用Pod并发访问数据时,缓存可用带宽将被这些应用Pod共享占用。
示例
例如,在ACK集群中扩容2台ecs.g7nex.8xlarge
规格的ECS实例用于构建分布式缓存集群,分布式缓存集群包含2个缓存Worker Pod,每个Pod设置的缓存容量为100 GiB内存,两个Pod分别运行于两台ECS实例上。应用Pod部署于一台ecs.gn7i-c8g1.2xlarge
(8 vCPU,30 GiB内存,16 Gbps带宽)规格的ECS实例,其缓存容量、可用带宽以及最大带宽的理论值如下所示:
缓存可用容量 = 100GiB * 2 = 200 GiB
缓存可用带宽 = 2 * min{40 Gbps, 内存访问I/O吞吐} = 80 Gbps
缓存命中时,应用Pod数据访问最大可用带宽 = min{80Gbps, 16Gbps} = 16 Gbps
推荐ECS机型规格
分布式缓存集群的可用带宽取决于集群中各个ECS节点的最大可用带宽和所使用的缓存介质,为提升分布式缓存系统性能,推荐您使用高带宽的ECS实例规格,并使用内存或本地HDD或本地SSD盘作为缓存介质。推荐使用的ECS实例规格如下所示。
ECS实例规格族 | ECS实例规格 | ECS实例配置 |
网络增强通用型实例规格族g7nex | ecs.g7nex.8xlarge | 32 vCPU, 128GiB内存, 40 Gbps带宽 |
ecs.g7nex.16xlarge | 64 vCPU, 256 GiB内存, 80 Gbps带宽 | |
ecs.g7nex.32xlarge | 128 vCPU, 512 GiB内存, 160 Gbps带宽 | |
本地SSD型实例规格族i4g | ecs.i4g.16xlarge | 64 vCPU, 256 GiB内存, 2 * 1920 GB本地SSD盘存储,32 Gbps带宽 |
ecs.i4g.32xlarge | 128 vCPU, 512 GiB内存, 4 * 1920 GB本地SSD盘存储, 64 Gbps带宽 | |
网络增强通用型实例规格族g7ne | ecs.g7ne.8xlarge | 32 vCPU, 128 GiB内存, 25 Gbps带宽 |
ecs.g7ne.12xlarge | 48 vCPU, 192 GiB内存,40 Gbps带宽 | |
ecs.g7ne.24xlarge | 96 vCPU, 384 GiB内存,80 Gbps带宽 | |
通用型实例规格族g8i | ecs.g8i.24xlarge | 96 vCPU, 384 GiB内存, 50 Gbps带宽 |
ecs.g8i.16xlarge | 64 vCPU, 256 GiB内存,32 Gbps带宽 |
关于ECS实例的详细信息,请参见实例规格族。
策略二:选择缓存介质
分布式缓存集群的可用带宽取决于集群中各个ECS节点的最大可用带宽和所使用的缓存介质,为提升分布式缓存系统性能,推荐您使用高带宽的ECS实例规格,并使用内存或本地SSD盘作为缓存介质。在Fluid中,可以通过配置Runtime资源对象的spec.tieredstore
参数来设置不同的缓存介质以及缓存容量。
使用ESSD云盘作为缓存介质往往无法满足数据密集型应用场景的高性能数据访问需求。例如:PL2云盘的单盘最大吞吐量为750 MB/s,这意味着如果仅使用一块PL2云盘作为缓存介质,即使选择可用带宽大于750 MB/s的ECS机型,该ECS节点所能提供的缓存可用带宽上限也仅为750 MB/s,浪费ECS节点的最大可用带宽。
使用内存作为缓存介质
如果使用内存作为缓存介质,配置Runtime资源对象的spec.tieredstore
为如下配置:
spec:
tieredstore:
levels:
- mediumtype: MEM
volumeType: emptyDir
path: /dev/shm
quota: 30Gi # 为单个分布式缓存Worker副本所能提供的缓存容量。
high: "0.99"
low: "0.95"
使用本地存储作为缓存介质
如果使用本地系统盘存储作为缓存介质:
spec: tieredstore: levels: - mediumtype: SSD volumeType: emptyDir # 使用emptyDir确保缓存数据的生命周期与分布式缓存Worker Pod一致,避免缓存数据残留。 path: /var/lib/fluid/cache quota: 100Gi # 为单个分布式缓存Worker副本所能提供的缓存容量。 high: "0.99" low: "0.95
如果使用额外挂载的SSD数据盘作为缓存介质:
spec: tieredstore: levels: - mediumtype: SSD volumeType: hostPath path: /mnt/disk1 # /mnt/disk1为宿主机的本地SSD盘挂载路径。 quota: 100Gi # 为单个分布式缓存Worker副本所能提供的缓存容量。 high: "0.99" low: "0.95"
如果同时使用多块SSD数据盘作为缓存介质:
spec: tieredstore: levels: - mediumtype: SSD volumeType: hostPath path: /mnt/disk1,/mnt/disk2 # /mnt/disk1和/mnt/disk2均为宿主机的数据盘挂载路径。 quota: 100Gi # 单个分布式缓存Worker副本所能提供的缓存容量,容量将均分在多个缓存路径上(例如:/mnt/disk1和/mnt/disk2各分配50Gi容量)。 high: "0.99" low: "0.95"
策略三:配置数据缓存与应用间调度亲和性
当数据访问请求命中缓存时,应用Pod从缓存系统中读取数据。因此,如果应用Pod与缓存系统分别部署在不同的可用区内,应用需要跨可用区访问缓存数据。如果希望降低跨可用区网络波动对数据访问过程的影响,需要考虑应用Pod与缓存系统相关Pod之间的调度亲和性,具体而言:
缓存系统Worker Pod尽量亲和部署在相同可用区内。
应用Pod与缓存系统Worker Pod尽量亲和部署在相同可用区内。
将多个应用Pod与缓存系统Worker Pod部署在单一可用区内会降低应用和相关服务的容灾能力,您可以根据自身业务SLA平衡选择性能影响和服务可用性。
在Fluid中,可以通过配置Dataset资源对象的spec.nodeAffinity
,设置缓存系统Worker Pod的调度亲和性,如下所示:
apiVersion: data.fluid.io/v1alpha1
kind: Dataset
metadata:
name: demo-dataset
spec:
...
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values:
- <ZONE_ID> # 所在的可用区,例如cn-beijing-i。
上述配置将分布式缓存系统Worker Pod均部署在<ZONE_ID>可用区的ECS节点。
另外,Fluid可以在应用Pod中自动注入该应用Pod所需缓存的亲和性信息,实现应用Pod与缓存系统Worker Pod尽量同可用区亲和部署。更多信息,请参见数据缓存亲和性调度优化。
策略四:大文件全量顺序读场景参数配置优化
许多数据密集型场景中涉及大文件全量顺序读的数据访问模式,例如,基于TFRecord或Tar格式的数据集进行模型训练、AI模型推理服务启动时加载1个或多个模型参数文件、读取Parquet文件格式进行分布式数据分析等。针对此类场景,可以使用更加激进的预读策略,提升数据访问性能,例如适当增大缓存系统预读的并发度、预读数据量等。
在Fluid中,不同分布式缓存运行时在预读策略方面的配置需要有不同的参数配置,如下所示:
使用JindoRuntime作为缓存运行时
如果使用JindoRuntime作为缓存运行时,您需要配置spec.fuse.properties
参数,自定义Jindo Fuse的行为和性能。
kind: JindoRuntime
metadata:
...
spec:
fuse:
properties:
fs.oss.download.thread.concurrency: "200"
fs.oss.read.buffer.size: "8388608"
fs.oss.read.readahead.max.buffer.count: "200"
fs.oss.read.sequence.ambiguity.range: "2147483647" # 约2G。
fs.oss.download.thread.concurrency
:Jindo客户端预读的并发线程数,每个线程会用于预读一个缓冲区。fs.oss.read.buffer.size
:单个buffer的大小。fs.oss.read.readahead.max.buffer.count
:Jindo客户端单流预读的最大buffer数量。fs.oss.read.sequence.ambiguity.range
:Jindo客户端判定进程是否顺序读取文件的判定范围。
使用JuiceFSRuntime作为缓存运行时
如果使用JuiceFSRuntime作为缓存运行时,您可以通过配置Runtime资源对象的spec.fuse.options
和spec.worker.options
分别设置FUSE组件和Worker组件的参数。
kind: JuiceFSRuntime
metadata:
...
spec:
fuse:
options:
buffer-size: "2048"
cache-size: "0"
max-uploads: "150"
worker:
options:
buffer-size: "2048"
max-downloads: "200"
max-uploads: "150"
master:
resources:
requests:
memory: 2Gi
limits:
memory: 8Gi
...
buffer-size:读写缓冲区大小。
max-downloads:预读过程的下载并发度。
fuse cache-size:设置JuiceFS FUSE组件可用的本地缓存容量。
例如,通过设置JuiceFS FUSE组件可用的本地缓存容量为0,并设置FUSE组件内存requests
为较小值(例如2 GiB)。FUSE组件会在Linux文件系统内核中自动利用节点可用内存作为近地缓存(Page Cache),近地缓存未命中时直接读取JuiceFS Worker分布式缓存中的数据,实现高效访问。更多性能调优和参数细节,请参见JuiceFS官方文档。
稳定性维度的Fluid数据缓存优化策略
为了确保Fluid数据缓存系统长期稳定运行,您需要根据业务实际需求,谨慎规避将含有大量文件的目录作为数据缓存的挂载点,以防止单点过载;实施元数据持久化策略,利用持久化存储(如ESSD云盘)加强系统韧性与故障恢复能力;合理配置FUSE Pod资源,确保资源供需平衡,避免资源竞争引起的不稳定;启用FUSE自愈机制,自动响应故障。整合运用这些策略,以增强Fluid数据缓存的稳定性和可靠性。本节将详细介绍如何运用以上这些策略。
策略一:避免挂载包含大量文件的目录作为底层数据源
缓存系统需要维护挂载底层存储目录下全部文件的元信息,并记录各文件的缓存状态等额外元信息。如果缓存系统挂载了包含大量文件的底层存储目录(例如:大规模存储系统的根目录),缓存系统必须使用大量内存资源存储相关元信息,并使用更多CPU资源处理相关的元信息访问请求。
Fluid定义了Dataset的抽象概念,Dataset是面向上层特定应用,逻辑上是一组相关数据的集合。一个Dataset会对应启动一个分布式缓存系统,并且一个Dataset应当仅为一个或某些相关的数据密集型任务提供数据访问加速服务。因此,推荐您创建多个Dataset,并在每个Dataset中分别定义底层数据源的不同子目录,具体子目录级别与业务应用所需的数据集合相关,不同的业务应用可以使用不同的Dataset绑定的缓存系统访问数据,这将使得各业务应用间有着更好的隔离性,保证系统稳定性和性能。Dataset配置示例如下:
apiVersion: data.fluid.io/v1alpha1
kind: Dataset
metadata:
name: demo-dataset
spec:
...
mounts:
- mountPoint: oss://<BUCKET>/<PATH1>/<SUBPATH>/
name: sub-bucket
创建多个缓存系统可能导致额外的运维复杂性,可根据实际业务需求灵活选择架构方案,例如:
如果所需缓存的数据集存储在单一后端存储系统中,且规模不大,文件数量不多,有较强的相关性,推荐创建单个Fluid Dataset和分布式缓存系统提供服务。
如果所需缓存的数据集规模较大、文件数量较多,推荐根据数据目录的业务含义,分拆为多个Fluid Dataset和分布式缓存系统提供服务,应用Pod可声明挂载一个或多个Fluid Dataset到指定目录下。
如果所需缓存的数据集来自于多个用户的不同的存储系统,并且业务要求保证用户间的数据隔离性。推荐为每个用户或用户的一系列数据相关的作业创建有着较短生命周期的Fluid Dataset,通过开发Kubernetes Operator灵活运维集群中多个Fluid Dataset。
策略二:使用元信息持久化提升缓存系统稳定性
许多分布式缓存系统采用Master-Worker的分布式架构,它们依赖于Master Pod组件维护挂载的后端存储系统目录的文件元信息,并记录各文件的缓存状态。当应用通过此类缓存系统访问数据时,首先需要从Master Pod组件中获取文件元信息,接着从底层文件存储或缓存Worker组件中获取数据。因此,缓存Master Pod组件的稳定性对缓存系统的高可用性至关重要。
如果使用JindoRuntime作为缓存运行时,在Fluid中推荐使用以下配置提升缓存Master Pod组件的可用性:
apiVersion: data.fluid.io/v1alpha1
kind: JindoRuntime
metadata:
name: sd-dataset
spec:
...
volumes:
- name: meta-vol
persistentVolumeClaim:
claimName: demo-jindo-master-meta
master:
resources:
requests:
memory: 4Gi
limits:
memory: 8Gi
volumeMounts:
- name: meta-vol
mountPath: /root/jindofs-meta
properties:
namespace.meta-dir: "/root/jindofs-meta"
在上述YAML示例中,demo-jindo-master-meta
为提前创建的PVC,该PVC使用ESSD云盘作为存储卷,这意味着Master Pod组件维护的元信息能够持久化存储,并能够随Pod迁移。更多信息,请参见通过配置JindoRuntime实现Master Pod组件状态持久化存储。
策略三:合理配置FUSE Pod资源
缓存系统客户端程序运行于FUSE Pod中,FUSE Pod在节点上挂载FUSE文件系统,该文件系统将被挂载到应用Pod的指定路径上,并暴露POSIX文件访问接口。这使得应用Pod往往不需要修改应用代码,即可像访问本地存储一样访问远程存储系统中的数据,并享受到缓存带来的加速效果。FUSE程序维持了应用和缓存系统之间的数据通道,因此推荐对FUSE Pod所使用的资源进行配置。
在Fluid中,可以通过配置Runtime资源对象的spec.fuse.resources
设置FUSE Pod的资源使用。为避免因FUSE程序OOM导致的文件挂载点断裂问题,推荐您对FUSE Pod不设置或设置较大的内存限制。例如,可以设置FUSE Pod的内存限制接近ECS节点的可分配内存大小。
spec:
fuse:
resources:
requests:
memory: 8Gi
# limits:
# memory: <ECS_ALLOCATABLE_MEMORY>
策略四:开启FUSE自愈功能提升数据访问客户端可用性
FUSE程序维持了应用和缓存系统之间的数据通道。默认情况下,如果FUSE程序因某些预期之外的行为崩溃,即使FUSE程序重启后恢复正常,应用容器也无法再通过该FUSE文件系统挂载点访问数据。为解决该问题,应用容器必须重启(重新触发FUSE文件系统挂载点到应用Pod的绑定挂载逻辑)来恢复数据访问,这将影响应用Pod的可用性。
Fluid提供了FUSE自愈机制,开启该机制后应用容器无需重启,即可在FUSE程序重启后的一段时间后,自动恢复应用容器内FUSE挂载点的数据访问。
在FUSE程序崩溃到重启后的短暂时间内,应用Pod内挂载点仍然会处于不可访问的状态,这意味着应用必须自行处理此类I/O错误,避免应用崩溃,并包含重试逻辑。
如需开启并使用FUSE自愈功能,请参见如何开启FUSE自动恢复能力。FUSE自愈能力存在较多限制,推荐仅在某些具有明确需求且业务适合的应用场景选择性开启此能力,例如:交互式编程开发场景,即使用在线Notebook或其他环境交互式开发调试某些应用程序。
缓存读写一致性最佳实践
缓存系统帮助应用提升了数据访问的效率,但相对地也会引入缓存一致性问题。更强的一致性往往意味着性能损失或运维复杂度,因此推荐您根据需求选择满足业务场景需求的缓存读写一致性配置策略。本节介绍如何在多种场景下配置缓存读写一致性。
场景一:数据只读且后端存储数据无变化
应用案例:单次AI模型训练过程中,读取固定数据集中的数据样例,执行AI模型的迭代训练,训练完成后数据缓存即被清理。
配置方案:该场景为Fluid默认支持的应用场景,Fluid Dataset使用默认配置或显式设置为只读即可。
配置示例:
apiVersion: data.fluid.io/v1alpha1
kind: Dataset
metadata:
name: demo-dataset
spec:
...
# accessModes: ["ReadOnlyMany"] ReadOnlyMany为默认值
通过设置accessModes: ["ReadOnlyMany"]
确保数据集以只读形式挂载,可以避免训练过程中对数据集的意外修改,同时也简化了数据管理和缓存策略。
场景二:数据只读且后端存储数据周期性规律变化
应用案例:缓存系统常驻于Kubernetes集群中,业务相关数据每日被采集并存储到后端存储系统中。每日凌晨需要定时执行数据分析任务,对当天新增的业务相关数据进行分析处理和汇总,汇总结果不通过缓存,直接写入后端存储系统。
配置方案:Fluid Dataset使用默认配置或显式设置为只读;按周期规律定时执行数据预热,同步后端存储系统数据变化。
配置示例:
apiVersion: data.fluid.io/v1alpha1
kind: Dataset
metadata:
name: demo-dataset
spec:
...
# accessModes: ["ReadOnlyMany"] ReadOnlyMany为默认值。
---
apiVersion: data.fluid.io/v1alpha1
kind: DataLoad
metadata:
name: demo-dataset-warmup
spec:
...
policy: Cron
schedule: "0 0 * * *" # 每日0点执行数据预热。
loadMetadata: true # 数据预热时同步后端存储系统数据变化。
target:
- path: /path/to/warmup # 指定了需要预热的后端存储系统路径。
通过设置
accessModes: ["ReadOnlyMany"]
,确保数据集主要服务于读取操作,即业务数据的写入直接发生于后端存储系统,而不会干扰到缓存层,保证了数据的一致性与完整性。通过设置
policy: Cron
和schedule: "0 0 * * *"
,确保了每天凌晨0点自动执行数据预热操作,实现了数据的周期性预热。同时,loadMetadata: true
确保了在预热过程中元数据也会被同步。
场景三:数据只读但后端存储数据根据业务事件变化
应用案例:某模型推理服务允许上传自定义的AI模型,并使用自定义模型执行推理。即上传的AI模型不通过缓存,直接写入后端存储系统,模型上传成功后,可以选择该模型执行推理,并查看推理结果。
配置方案:Fluid Dataset使用默认配置或显式设置为只读;Runtime FUSE设置文件元信息超时时间为较小值;禁用缓存系统服务端的元信息缓存。
配置示例:
使用JindoRuntime作为缓存运行时:
apiVersion: data.fluid.io/v1alpha1
kind: Dataset
metadata:
name: demo-dataset
spec:
mounts:
- mountPoint: <MOUNTPOINT>
name: data
path: /
options:
metaPolicy: ALWAYS # 通过设置metaPolicy: ALWAYS,禁用了服务端的元数据缓存。
---
apiVersion: data.fluid.io/v1alpha1
kind: JindoRuntime
metadata:
name: demo-dataset
spec:
fuse:
args:
- -oauto_cache
# 设置元信息超时时间为30s。如果xxx_timeout=0能提供强一致性,但可能会大大降低数据读取效率。
- -oattr_timeout=30
- -oentry_timeout=30
- -onegative_timeout=30
- -ometrics_port=0
通过设置metaPolicy: ALWAYS
,禁用了服务端的元数据缓存。这确保了每次访问都是直接查询后端存储,获取最新元数据,适合需要更强一致性的应用场景。
场景四:数据读取和写入请求位于不同目录
应用案例:大规模分布式AI训练中,训练任务从目录A下读取数据集中的数据样例,并在每轮(Epoch)训练完成后,将模型的Checkpoint写入到目录B下。由于模型Checkpoint可能较大,希望通过缓存写入以提升效率。
配置方案:创建两个Fluid Dataset,一个设置读写模式为只读,一个设置读写模式为读写,两个Fluid Dataset分别挂载于目录A和目录B下,实现读写分离。
配置示例:
apiVersion: data.fluid.io/v1alpha1
kind: Dataset
metadata:
name: train-samples
spec:
...
# accessModes: ["ReadOnlyMany"] ReadOnlyMany为默认值。
---
apiVersion: data.fluid.io/v1alpha1
kind: Dataset
metadata:
name: model-ckpt
spec:
...
accessModes: ["ReadWriteMany"]
通过将训练数据集train-samples
设置为只读访问模式accessModes: ["ReadOnlyMany"]
(ReadOnlyMany为默认值),从而使所有训练任务只能从该目录读取数据,保证了数据源的不可变性,避免了训练过程中因写操作引入的潜在一致性问题。同时,为模型Checkpoint目录(model-ckpt
)配置了读写模式(accessModes: ["ReadWriteMany"]
),允许训练任务在每轮迭代后安全地写入模型Checkpoint,提升了写入效率。
应用Pod定义参考示例如下:
apiVersion: v1
kind: Pod
metadata:
...
spec:
containers:
...
volumeMounts:
- name: train-samples-vol
mountPath: /data/A
- name: model-ckpt-vol
mountPath: /data/B
volumes:
- name: train-samples-vol
persistentVolumeClaim:
claimName: train-samples
- name: model-ckpt-vol
persistentVolumeClaim:
claimName: model-ckpt
通过挂载两个不同的Volume(train-samples-vol
和 model-ckpt-vol
)到指定路径(/data/A
和 /data/B
),实现了训练数据集与模型Checkpoint目录的物理隔离。
场景五:数据读取和写入请求必须在相同目录
应用案例:在交互式编程开发场景(例如在线Jupyter Notebook开发或在线VSCode开发等),您拥有一个属于自己的工作空间目录。工作空间目录中存储了数据集、代码等文件。您可能会经常在工作空间目录下新增、删除、修改文件。
配置方案:Dataset设置读写模式为读写;推荐使用具有完整POSIX兼容性的存储系统作为后端实现。
配置示例:
apiVersion: data.fluid.io/v1alpha1
kind: Dataset
metadata:
name: myworkspace
spec:
...
accessModes: ["ReadWriteMany"]
通过设置accessModes: ["ReadWriteMany"]
,确保多个用户或进程可以同时读取和写入同一个工作空间目录。