【阅读原文】戳:解读大模型时代的数据加速:性能、稳定性与一致性一个都不能少
背景
在大模型时代,不论人工智能生成内容(AIGC)和大型语言模型(LLM)技术的发展迅猛异常,推动了各行各业的变革与创新。然而,这种快速发展也带来了严峻的技术挑战,尤其是在数据管理和处理方面。无论是训练大规模语言模型,还是进行复杂的推理计算,以及大数据分析,数据的加速处理都成为了一个关键问题。
谈到数据加速,许多人直觉上认为增加一层缓存便可解决问题。但是在复杂的客户环境和差异化的业务场景中,增加缓存并不一定真正带来性能收益,如何因地制宜,用好缓存充分的发挥效力,才是问题的关键。
而实际上谈到数据加速,不仅仅涉及到如何快速处理和传输海量数据,也要考虑性能、稳定性与一致性上的平衡。在本文中,我们将以阿里云容器服务的ACK Fluid为例深入分析如何在提高性能的同时,确保系统的稳定性和平衡数据的一致性。通过具体的场景分析和技术细节,我们希望帮助大家更好地应对大模型时代的数据管理需求。
性能优化最佳实践
Fluid使用缓存技术提升Kubernetes集群内应用的数据访问效率,在计算存储分离架构下,缓存技术能够有效解决Kubernetes集群内访问存储系统中数据容易出现的延迟过高和带宽受限问题。缓存技术依赖局部性原理提升多次数据访问的平均性能,许多数据密集型应用场景均存在这样的局部性特征,例如:
• AI模型训练场景:AI模型训练过程中,相同的数据集将被按轮次(Epoch)周期性读取,用于AI模型的迭代收敛过程。
• AI模型推理服务启动场景:当发布或更新某AI模型推理服务时,大量推理服务实例被并发拉起,各推理服务实例并发读取存储系统中相同的模型文件,并加载到推理服务实例的GPU内存中。
• 大数据分析场景:大数据分析过程中,部分数据被一个或多个数据分析任务共同使用。例如:使用SparkSQL分析用户画像和商品信息时,订单数据将被这些分析任务共同使用。
为使Fluid构建的数据缓存有效提升数据访问的效率,需要根据业务的性能需求以及预算成本合理配置Fluid数据缓存使用的ECS机型、缓存介质、缓存系统参数配置、缓存管理策略等。另外,还需要考虑客户端数据读取方式对于性能的影响。本文档接下来介绍性能维度的Fluid数据缓存使用最佳实践。
缓存系统ECS机型选择
分布式文件缓存系统能够聚合多个节点上存储资源和带宽资源,为上层应用提供更大的缓存容量和更高的可用带宽,这些参数的理论上限值可根据以下公式估算:
• 缓存可用容量 = 每个分布式缓存Worker Pod提供的缓存容量 * 分布式缓存Worker Pod副本数
• 缓存可用带宽 = 分布式缓存Worker Pod副本数 * min{Worker Pod所在ECS节点的最大可用带宽, Worker Pod使用的缓存介质I/O吞吐}
分布式缓存集群的可用带宽取决于集群中各个ECS节点的最大可用带宽和所使用的缓存介质,为提升分布式缓存系统性能,推荐使用高带宽的ECS实例规格,并使用内存或本地HDD或/本地SSD盘作为缓存介质。
注意:缓存可用带宽不代表数据访问过程中的实际带宽,数据访问过程的实际带宽受客户端ECS节点可用带宽,以及数据访问模式(顺序读、随机读)影响。应用Pod数据访问过程的理论最大带宽 = min{应用Pod所在ECS节点的可用带宽, 缓存可用带宽}。另外,当多个应用Pod并发访问数据时,缓存可用带宽将被这多个应用Pod共享占用。
推荐使用的ECS实例规格以及实例相关配置如下:
示例:
ACK集群中扩容2台ecs.g7nex.8xlarge规格的ECS实例用于构建分布式缓存集群,分布式缓存集群包含两个缓存Worker Pod,每个Pod设置的缓存容量为100GiB 内存,两个Pod分别运行于两台ECS实例上。应用Pod部署于一台ecs.gn7i-c8g1.2xlarge(8vCPU, 30GiB内存, 16Gbps网络)规格的ECS实例,那么:
• 缓存可用容量 = 100GiB * 2 = 200GiB
• 缓存可用带宽 = 2 * min{40Gbps, 内存访问I/O吞吐} = 80Gbps
• 缓存命中时,应用Pod数据访问最大可用带宽 = min{80Gbps, 16Gbps} = 16Gbps
缓存介质选择
分布式缓存集群的可用带宽取决于集群中各个ECS节点的最大可用带宽和所使用的缓存介质,为提升分布式缓存系统性能,推荐您使用高带宽的ECS实例规格,并使用内存或本地HDD或/本地SSD盘作为缓存介质。在Fluid中,可以通过配置Runtime资源对象的spec.tieredstore属性,设置不同的缓存介质以及缓存容量。
注意:使用ESSD云盘作为缓存介质往往无法满足数据密集型应用场景的高性能数据访问需求。例如:PL2云盘的单盘最大吞吐量为750MB/s,这意味着如果仅使用一块PL2云盘作为缓存介质,即使选择可用带宽大于750MB/s的ECS机型,该ECS节点所能提供的缓存可用带宽上限也仅为750MB/s,浪费ECS节点的最大可用带宽。
如果使用内存作为缓存介质,配置Runtime资源对象的spec.tieredstore为如下配置:
spec: tieredstore: levels: - mediumtype: MEM volumeType: emptyDir path: /dev/shm quota: 30Gi # 单个分布式缓存Worker副本所能提供的缓存容量 high: "0.95" low: "0.7"
• tieredstore.levels.quota为单个分布式缓存Worker能够提供的最大缓存容量。
如果使用本地SSD盘作为缓存介质,配置Runtime资源对象的spec.tieredstore为如下配置:
spec: tieredstore: levels: - mediumtype: SSD volumeType: hostPath path: /mnt/disk1 # /mnt/disk1为宿主机的本地盘挂载路径 quota: 100Gi # 单个分布式缓存Worker副本所能提供的缓存容量 high: "0.95" low: "0.7"
如果需要同时使用多块本地盘作为缓存介质:
spec: tieredstore: levels: - mediumtype: SSD volumeType: hostPath path: /mnt/disk1,/mnt/disk2 # /mnt/disk1和/mnt/disk2均为宿主机的本地盘挂载路径 quota: 100Gi # 单个分布式缓存Worker副本所能提供的缓存容量,容量将均分在多个缓存路径上 high: "0.95" low: "0.7"
配置数据缓存与应用间调度亲和性
当数据访问请求命中缓存时,应用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> # e.g. cn-beijing-i
上述配置将分布式缓存系统Worker Pod均部署在所在可用区的ECS节点。
另外,Fluid可以在应用Pod中自动注入与该应用Pod所需缓存的亲和性信息,实现应用Pod与缓存系统Worker Pod尽量同可用区亲和部署。更多细节请参考数据缓存亲和性调度优化[1]。
大文件全量顺序读场景参数配置优化
许多数据密集型场景中涉及大文件全量顺序读的数据访问模式,例如,基于TFRecord或Tar格式的数据集进行模型训练AI模型推理服务启动时加载1个或多个模型参数文件、读取Parquet文件格式进行分布式数据分析等。针对此类场景,可以使用更为激进的预读策略,提升数据访问性能,例如适当增大缓存系统预读的并发度、预发数据量等。
在Fluid中,不同分布式缓存运行时在预读策略方面的配置需要有不同的参数配置,例如:
如果使用JindoRuntime作为缓存运行时:
kind: JindoRuntime metadata: ... spec: fuse: properties: fs.oss.download.thread.concurrency: "200" fs.oss.read.buffer.size: "8388608" # 8M fs.oss.read.readahead.max.buffer.count: "200" fs.oss.read.sequence.ambiguity.range: "2147483647" #约2G
• fs.oss.download.thread.concurrency: Jindo客户端预读的并发线程数,每个线程数会用于预读一个buffer。
• fs.oss.read.buffer.size:单个buffer的大小。
• fs.oss.read.readahead.max.buffer.count: Jindo客户端单流预读的最大buffer数量。
• fs.oss.read.sequence.ambiguity.range:Jindo客户端判定进程是否顺序读取文件的判定范围。
更多Jindo性能调优高级参数,参考JindoSDK高级参数配置[2]。
如果使用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"
• buffer-size: 读写缓冲区大小。
• max-downloads: 预读过程的下载并发度。
• fuse cache-size设置为0。
设置JuiceFS FUSE组件可用的本地缓存容量为0,并设置FUSE组件内存requests为较小值(例如:2Gi)。FUSE组件会在Linux文件系统内核中自动利用节点可用内存作为近地缓存,近地缓存未命中时直接读取JuiceFS Worker分布式缓存中的数据,实现高效访问。更多性能调优和参数细节请参考JuiceFS官方文档[3]。
稳定性最佳实践
避免挂载包含大量文件的目录作为底层数据源
缓存系统需要维护挂载底层存储目录下全部文件的元信息,并记录各文件的缓存状态等额外元信息。如果缓存系统挂载了包含大量文件的底层存储目录(例如:大规模存储系统的根目录),缓存系统必须使用大量内存资源存储相关元信息,并使用更多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 Pod组件维护挂载的底层存储系统目录的文件元信息,并记录各文件的缓存状态。当应用通过此类缓存系统访问数据时,首先需要从Master组件中获取文件元信息,接着从底层文件存储或缓存Worker组件中获取数据。因此,缓存Master Pod的稳定性对缓存系统的高可用性至关重要。
如果使用JindoRuntime作为缓存运行时,在Fluid中推荐使用以下配置提升缓存Master组件的可用性:
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迁移。更多细节请参考JindoRuntime配置Master组件状态持久化[4]。
FUSE Pod资源配置
缓存系统客户端程序运行于FUSE Pod中,FUSE Pod在节点上挂载FUSE文件系统,该文件系统将被挂载到应用Pod的指定路径上,并暴露POSIX文件访问接口。这使得应用Pod往往不需要修改应用代码,即可像访问本地存储一样访问远程存储系统中的数据,并享受到缓存带来的加速效果。FUSE程序维持了应用和缓存系统之间的数据通道,因此推荐对FUSE Pod所使用的资源进行配置。
在Fluid中,可以通过配置Runtime资源对象的spec.fuse.resources设置FUSE Pod的资源使用,为避免FUSE程序OOM导致的文件挂载点断裂问题,推荐对FUSE Pod不设置或设置较大的memory limit,以尽量避免OOM造成的业务影响。例如,设置FUSE Pod的memory limit接近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自动恢复能力[5]。FUSE自愈能力存在较多限制,推荐仅在某些具有明确需求且业务适合的应用场景选择性开启此能力,例如:
• 交互式编程开发场景:用户使用在线Notebook或其他环境交互式开发调试某些应用程序,开启FUSE自愈功能后,用户无需重启整个环境,即可恢复FUSE挂载点的数据访问。
缓存读写一致性最佳实践
缓存系统帮助应用提升了数据访问的效率,但相对地也会引入缓存一致性问题。下文介绍多种场景下的缓存读写一致性配置的最佳实践。更强的一致性往往意味着性能损失或运维复杂度,因此我们推荐根据需求选择满足业务场景需求的缓存读写一致性配置策略。以下介绍常见的应用场景、场景对应的应用案例以及对应场景的Fluid配置。
场景一:数据只读且后端存储数据无变化
应用案例:单次AI模型训练过程中,读取固定数据集中的数据样例,执行AI模型的迭代训练,训练完成后数据缓存即被清理。
配置方案:该场景为Fluid默认支持的应用场景,Fluid Dataset使用默认配置或显式设置为只读即可。
配置示例如下:
apiVersion: data.fluid.io/v1alpha1 kind: Dataset metadata: name: demo-dataset spec: ... # accessModes: ["ReadOnlyMany"] 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
场景三:数据只读但后端存储数据根据业务事件变化
应用案例:某模型推理服务允许用户上传自定义的AI模型,并使用自定义模型执行推理。用户上传的AI模型不通过缓存,直接写入底层存储系统。用户上传成功后,希望选择该模型执行推理,并查看推理结果。
配置方案:Fluid Dataset使用默认配置或显式设置为只读;Runtime FUSE设置文件元信息超时时间为较小值;禁用缓存系统服务端的元信息缓存。
配置示例如下:
如果使用JindoRuntime作为缓存运行时:
apiVersion: data.fluid.io/v1alpha1 kind: Dataset metadata: name: demo-dataset spec: ... # accessModes: ["ReadOnlyMany"] ReadOnlyMany为默认值 --- 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 properties: fs.jindofsx.meta.cache.enable: "false"
场景四:数据读取和写入请求位于不同目录
应用案例:大规模分布式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"]
应用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
场景五:数据读取和写入请求必须在相同目录
应用案例:在交互式编程开发场景,用户在一个开发环境中拥有自己的工作空间目录。工作空间目录中存储了数据集、代码等文件。用户在工作空间目录下频繁新增、删除、修改文件。
配置方案:Dataset设置读写模式为读写;推荐使用具有完整POSIX兼容性的存储系统作为后端实现。
配置示例如下:
apiVersion: data.fluid.io/v1alpha1 kind: Dataset metadata: name: myworkspace spec: ... accessModes: ["ReadWriteMany"]
总结
本文详细介绍了在阿里云 ACK 环境中使用 ACK Fluid 的实践,包括了多个优化策略。在性能优化方面,通过选择合适的 ECS 机型和缓存介质、配置调度亲和性以及优化预读策略,可以显著提升数据访问速度。在稳定性方面,通过避免单点过载、实施元信息持久化、合理配置 FUSE Pod 资源和启用自愈机制,增强系统的可靠性。在数据一致性方面,根据业务场景选择适当的缓存一致性配置,如只读模式、读写模式下的周期性数据同步、禁用元信息缓存和读写分离策略,以结合用户场景实现合理的数据一致性预期和效果。
综上所述,阿里云ACK Fluid提供了一套全面的策略,帮助用户在不同的应用场景下实现最优的数据缓存效果。通过细致的策略选择与配置,用户可以有效地权衡数据处理的性能、稳定性和数据一致性, 真正做到在生产环境上的数据加速。
相关链接:
[1]数据缓存亲和性调度优化
https://aliyuque.antfin.com/fluid/oqylbp/zw50zk6k4pmygs76
[2]JindoSDK高级参数配置
[3]JuiceFS官方文档
https://juicefs.com/docs/zh/cloud/reference/command_reference/#object-storage-options
[4]JindoRuntime配置Master组件状态持久化
https://aliyuque.antfin.com/fluid/oqylbp/pcuvnf0px20vgfc3
[5]如何开启FUSE自动恢复能力
我们是阿里巴巴云计算和大数据技术幕后的核心技术输出者。
获取关于我们的更多信息~