vivo AI计算平台是在 2018 年底开始着手建设的,致力于解决统一高性能训练环境、大规模分布式训练、计算资源的高效利用调度等痛点。经过将近四年的持续迭代,平台建设和落地取得了很大进展,由当初服务深度学习为主,到现在演进成包含 VTraining、VServing、VContainer 三大模块,对外提供模型训练、模型推理和容器化等能力,成为 vivo AI 领域的核心基础平台。VTraining 是一站式的通用训练平台,基于 Kubernetes 集群搭建,支持多种框架的大规模分布式训练,并配备 PB 级别规模的分布式存储。现在 VTraining 的用户已经超过 700 人,覆盖了推荐,语音,视觉,NLP,影像等方向核心业务。本文将分享 VTraining 的轩辕文件存储实践心得。
当前 VTraining 对存储系统的需求主要体现在以下方面。
共享:为了避免数据在节点之间来回拷贝,所以需要一个共享的存储空间来收集和存储数据。业界一般采取计算和存储分离的分案,使用远程分布式存储来存储训练数据。
高性能:深度学习任务需要大量的数据来训练模型,因此存储系统要提供高带宽来确保数据的流动足够快,来满足需求。特别是随着 GPU 的计算能力越突出,往往会造成存储系统更大的性能瓶颈。
易管理:数据管理简单,存储集群易维护,容量扩缩容方便。
VTraining 建设初期,存储方面的建设并不是一帆风顺的。从开始到现在,存储设计也踩过一些坑,底层数据存储系统也经历过由 glusterfs 到轩辕文件的迁移。在前期使用 glusterfs 作为底层存储系统时,遇到过以下问题:
理论和实践上分析,GlusterFS 目前主要适用大文件存储场景,VTraining 训练场景比较复杂,在多个场景下性能存储瓶颈影响训练任务,常见的如下:
遍历包含海量文件的目录效率低下
GlusterFS 使用弹性哈希算法(https://blog.csdn.net/liuaigui/article/details/70219377)代替传统分布式文件系统中的集中或分布式元数据服务,这种访问方式的特点就是当知道文件名,查找和定位会非常快,但是,如果事先不知道文件名,要列出文件目录(ls 或 ls -l),性能就会大幅下降。VTraining 很多训任务需要大量的文件进行训练,比如图片。这样会造成有海量文件目录场景很常见,在训练加载数据时,是必须先遍历目录,十分影响训练任务效率,极端情况下会卡死训练任务。
小文件访问性能低
其实海量小文件的性能问题一直是工业界和学术界公认的难题,一般在针对这个问题的优化上会从元数据管理、缓存机制、合并小文件等方面展开。glusterfs 本身没有对小文件作额外的优化措施,这样在碰到海量小文件的情况下 glusterfs 显得很被动,但是海量小文件在深度学习训练中是很常见,如果不做额外的优化,训练效率会极低。
大规模训练时存储性能问题
随着 VTraining 的用户越来越多,相对应的任务数量也在变多,这使得远端分布式存储集群的 IO 压力与日俱增。一方面多个客户端同时访问会形成流量竞争,glusterfs 客户端没有很好的限流策略会造成部分训练任务的 IO 处于饥饿状态,训练任务长时间效率低下;一方面大量的客户端直接访问分布式存储服务器,会使存储服务器负载过高,IO 时延过高,不仅会造成客户端访问过慢,严重的时候可能会压垮存储集群,造成存储不可用。一般针对这种情况,很多设计会在客户端和存储集群之间来加缓存来缓解存储服务器压力,glusterfs 客户端也有缓存设计,但是缓存的存储介质是基于内存,对训练数据总量达到 PB 级别计算平台来说,明显是不适用的。
glusterfs 的扩容比较简单,但是 glustefs 的容量负载均衡也没有很好考虑执行的自动化、智能化和并行化。扩容之后,需要手工执行负载均衡,也没有考虑当前系统的负载情况,可能影响正常的业务访问。容量均衡过程中由于采用了 Hash 算法进行数据分布,节点一变会动全身,增加和删除节点增加了大量数据迁移工作,严重影响整个集群性能。但如果不进行容量负载均衡,会使老的节点负载过重,存在局部节点性能问题。所以 Vtraining 为了尽量减少扩容之后数据均衡带来的影响,采用增加集群的方案,但这无疑带了更大管理成本。
轩辕文件存储是 vivo 互联网基础平台存储团队和 juicefs 团队共同合作开发给内部使用的存储系统,是一款面向云原生设计的高性能文件系统。它采用了”数据“与”元数据“分离的存储架构,从而实现文件系统的分布式设计。主要的核心架构如下:
轩辕文件存储架构图
主要包括三个特性组件:
客户端 JuiceFS
客户端JuiceFS提供了丰富的 API,兼容了当前大多数主流应用平台,可以在最上层无缝对接大数据,机器学习,人工智能等平台,为其提供海量、弹性、低价的高性能存储。在数据存储和元数据引擎上采用了可插拔架构,能对接各种各样的对象存储,用户可以根据自己需求选择合适的底层存储。另外 juicefs 的缓存策略也十分灵活,很大程度上能解决存储性能不足的问题,juicefs 具体介绍请参考https://www.juicefs.com/docs/zh/community/introduction/
元数据引擎
由于 JuiceFS 元数据引擎可插拔特性,可以使用户会选择性能较好的元数据引擎来加速元数据访问,比如 redis,Tikv 等高性能数据库。轩辕文件存储的元数据引擎是 vivo 公司自研的一款具备高性能,高稳定性,多数据模型等特性的分布式磁盘 KV 数据库,虽然达不到 redis 那么高的性能,但是会大大减少成本压力,并针对 AI 特征业务做更多的应用场景适配。
数据存储引擎
数据存储引擎使用的是 vivo 公司自研的轩辕对象存储。轩辕对象存储提供了海量,安全,低成本,高性能,高可靠的存储服务解决方案。目前提供了多种语言 SDK,能使开发者快速接入存储集群,也能更好对接 JuiceFS 客户端。
在存储系统选型中,对象存储是能够承载百亿规模文件的,但由于缺少原生目录支持,缺少 POSIX 语句支持,元数据性能弱等方面问题,传统的对象存储并不适合计算平台深度学习特别是海量小文件训练场景。客户端 JuiceFS 可以用任意对象存储作为数据持久层,保存数据内容的同时,JuiceFS 客户端 100%兼容 POSIX,HDFS,S3 三大主流访问协议,能支持所有的机器学习、深度学习框架。这样只要解决元数据性能问题,轩辕文件存储使用对象存储的作为计算平台数据持久层是非常适合的。
相对于 glusterfs 的弹性 hash 算法,轩辕文件存储采用元数据和数据分离存储的架构,用高性能的分布式元数据服务来管理元数据,访问效率大大增加。另外为了进一步提高元数据存储的性能,我们采用了高性能磁盘 nvme 来作为存储介质。
客户端 JuiceFS 提供了多种缓存能力,客户端访问过的数据,可以缓存在指定的存储介质中。通过合理的使用 JuiceFS 缓存机制,不仅可以减少远程网络访问的开销,加速数据读取速度,大幅度提升了训练效率,还能显著减少客户端对远端存储集群的 API 调用,缓解了存储服务器 IO 压力,减少服务器负载,使存储集群更健康。同样,元数据也会自动缓存客户端内存中来提高元数据的访问性能。
JuiceFS 客户端支持读写限速,多用户访问的场景下,可以很好解决客户端 IO 竞争问题,减少饥饿现象。
轩辕文件存储对元数据和数据都是通过统一的中心架构进行数据分布的,相比采用 Hash 算法的 glusterfs 在容量负载均衡具有天然优势。轩辕文件存储容量负载均衡很好的考虑执行的自动化、智能化和并行化,最大化的减少对当前业务的影响。
实践证明,VTraining 在使用轩辕文件存储系统后,原先的痛点都得到了极大的改善或解决。
VTraining 基于 Kubernetes 集群搭建,JuiceFS 提供 CSI(https://www.juicefs.com/docs/zh/csi/introduction/)和 flexvolume 两种插件支持,通过 Kubernetes 原生的存储方案来访问数据,对 Kubernetes 生态友好。当前 VTraining 采用的是 flexvolume,结构如下:
flexvolume 访问
当前 K8s 接入存储的主流方式 CSI,为什么 VTraining 会选择 flexvolume,主要由以下几点原因:
1. 监控方面,JuiceFS 客户端是支持各项运行指标监控的。训练任务如果通过 flexvolume 访问存储,它对 JuiceFS 客户端是独占的;训练任务如果通过 CSI 访问存储,它对 JuiceFS 客户端是共享的(同一节点,如下图所示)。所以在采用 CSI 的情况下,是监控不到一个任务的存储运行的指标情况,因为这个客户端的监控反映的是多个任务融在一起的情况。单个任务在存储运行方面的无法监控,这是我们不希望看到的。
2. 性能方面,JuiceFS CSI 和 flexvolume 虽然使用的方式不一样,但是最终都是通过 JuiceFS 客户端挂载对应的存储卷到训练节点供训练任务访问存储,性能只跟 JuiceFS 客户端有关,CSI 和 flexvolume 只是启动 JuiceFS 客户端程序的方式不一样,所以 CSI 在这方面并不占优势,在资源充足的情况下 CSI 性能可能会低,因为 CSI 多个任务共享一个客户端(如下图所示),单进程资源利用不充分。
3. 故障影响方面,因为 JuiceFS CSI 启用的客户端是共享的(如下图所示),所以客户端异常之后,当前节点上共享该客户端的任务都会受到影响。而独占客户端 flevolume,只会影响独占客户端的任务。另外 JuiceFS CSI 把存储访问集成到了 k8s 集群中,需要与 K8s 的主要组件进行交互,如果设计不合理,有可能会影响到整个 K8s 集群。
4. 管理方面,JuiceFS CSI 把对 JuiceFS 存储的访问集成到 K8s 集群中,所以通过 CSI 访问存储时,得符合 K8s 得一些规范,必须要生成对应的 PV 和 PVC 资源,这需要额外的策略去管理这些资源。而 flexvolume 的直接调用 juicefs 客户端程序进行挂载给任务 pod 使用,简单明了,管理成本少。
5. 缓存方面,JuiceFS CSI 驱动 v0.10.0 及以上版本不支持在 --cache-dir 挂载选项中使用通配符,但通配符其实在 VTraining 平台是很有用的,因为 VTraining 平台集群中的节点有多种类型的机型,不同机型的缓存磁盘资源是不同的,一个磁盘对应一个缓存目录,所以不同机型的节点的缓存目录个数不一样,(如下图所示)。如果参数--cache-dir 不支持使用通配符,那么这个参数是无法设置的。因为你不知道训练任务会调度到哪个节点,这个节点上存在哪些缓存目录。而 flexvolume 是支持在挂载参数中使用通配符。这样你只需要让磁盘的挂载目录放在同一个父目录下,再根据目录名来使用对应的通配符就能适用所有的机型,比如下面的的 V100 和 T4 机型挂载参数--cache-dir 我只要设置/var/lib/kubelet/juicefs_cache/*cache 就能让 juicefs 客户端找到对应的缓存目录而不用去关心任务运行在哪个节点。
上图:V100 机器的缓存目录
下图:T4 机器的缓存目录
综上所述,VTraining 当前没有把轩辕文件存储访问方式从 flexvolume 切换到 CSI。但是这并不是说 JuiceFS CSI 不如 flexvolme,只是 JuiceFS flexvolume 更适合当前 VTraining。JuiceFS 的 CSI 和 flexvolume 的具体使用方式请参考 juicefs 官网https://www.juicefs.com/docs/zh/
为了提高性能,轩辕文件存储的客户端 JuiceFS 实现了多种缓存机制来降低访问的时延和提高吞吐量,包括元数据缓存,数据缓存,以及多个客户端之间的缓存共享。正是因为轩辕文件存储强大的缓存机制大幅度的提升了 VTraining 平台的训练效率。
JuiceFS 支持在内核和客户端内存中缓存元数据以提升元数据的访问性能。
内核中可以缓存属性,文件项,目录项三种元数据,以提高 lookup 和 getattr 的性能。相对应的三个参数控制缓存时间如下
为了减少客户端和元数据服务之间频繁的列表和查询操作,客户端可以把经常访问的目录完整地缓存在客户端内存中,通过设置如下参数
客户端 JuiceFS 的数据缓存策略很灵活,缓存的介质可以是内存和磁盘。对于数据量达到 PB 级别的计算平台来说,内存做缓存肯定不现实,VTraining 平台上的训练任务选择的缓存介质是 SSD 磁盘,完全不用担心缓存 io 性能。当前 VTraining 会根据不同业务使用不同的缓存策略,主要可以分为两种:本地缓存和独立缓存集群
本地缓存就是训练任务和它的缓存数据都在同一节点如下图所示:
本地缓存
这种模式的好处就是能节省远端访问带来的网络开销,最大化提升训练任务 IO 性能,在缓存命中率有保证的情况下训练效率是最高的。在缓存策略全面上线之后,各个业务的用户都反馈良好,训练效率都有显著的提升,下图展示了图像训练任务缓存使用前后效率的对比:
图像训练任务缓存使用前后效率对比
缓存组和独立缓存集群
本地缓存虽然效率提升明显,但是在某些场景下也存在一些问题,特别是大数据集训练,主要有以下几点:
1. 各节点缓存磁盘容量有限,存储的缓存数据是有限的,当节点上的训练任务所需的数据多于缓存磁盘容量,是必然会出现缓存 miss 的,极端情况下会造成节点缓存失效。
2. 相同的训练数据可能会被不同的训练任务共享,这些任务很有可能被调度到不同节点上,这样会造成多个节点的缓存里面有同一份缓存数据,对于集群来说缓存资源利用率不高,缓存的有效数据变少,也会加剧缓存失效。
3. 很多时候需要通过数据集提前预热,以提升第一次访问数据时的性能。本地缓存这种方式就对预热不是很友好。首先你不知道训练任务调度会调度到哪个节点(如果没有指定节点调度),你就不知道该在哪个节点上预热;其次,任务下次训练换了节点,又得再一次预热。
可以使用 JuiceFS 缓存组和独立缓存集群的特性来解决上述问题。
利用 JuiceFS 的”缓存数据共享“特性,可以让不同的训练节点 JuiceFS 客户端可以组成一个缓存组。在这个缓存组中的 JuiceFS 客户端都可以共享缓存数据(所以训练任务要想共享缓存组的数据,它的 juicefs 客户端必须加入缓存集群)。也就是说当训练任务所处的节点没有命中缓存时,能够通过同一缓存组中的其他节点来获取数据,而不用去请求远端的对象存储。但是训练任务不是一个静态资源,一旦运行结束对应的 juicefs 客户端也会结束,会导致缓存组的成员频繁更新。虽然缓存组通过一致性哈希确保集群成员发生变化时,需要迁移的数据尽量少,但是依靠任务的 Juicefs 客户端存组成的缓存组成员变化太快了,这个在实际中无法使用的,除非你的任务永不结束。
那有什么办法解决该问题呢,这就需要 JuiceFS 独有的”独立缓存集群“特性。在同一个缓存组可以分角色,有两种角色,一种不参与缓存组的数据缓存,只从缓存组中读取数据,一种是参与缓存组的数据缓存。我们用后者组成一个缓存集群,供前者使用,这个缓存集群就是 JuiceFS 的独立缓存集群。如图所示:
独立缓存集群
我们可以把训练集群中和独立缓存集群中的 JuiceFS 客户端使用相同的挂载参数--cache-group={cache-group-name}来构建一个缓存组,然后训练集群的 JuiceFS 客户端挂载需要加上--no-sharing 表明不参与数据缓存。这样的话训练任务的客户端由于不参与数据缓存,也就不会影响到缓存组的缓存集群,同时也能共享独立缓存集群中的缓存数据来加速任务训练。这种模式相对于本地缓存的性能虽然低一点,但是却完美的解决本地缓存的问题:
1. 独立缓存集群相当于把集群中节点的磁盘组成一块巨大的磁盘,并且同样的数据在集群中只要缓存一份,就能给不同节点的任务使用,完美解决了缓存磁盘不足和有效数据低的问题。
2. 对于预热数据加速来说也非常友好,只要在独立缓存集群任意节点进行预热,就能把数据预热到集群中,并且任务无论调度到哪个节点都无需第二次预热
当然 JuiceFS 也可以本地缓存和独立缓存集群结合同时来使用,使性能最大化,这里不做描述,读者可以自行了解。
感谢杭州果汁数据科技 JuiceFS 团队的苏锐、davies、朱唯唯等和 vivo 互联网基础平台存储团队的肖博、龚兵、于相洋、韩姜、储敏等对轩辕文件存储在 Vtraining 平台设计和落地过程中的大力支持
作者介绍:
彭毅格,vivo AI 研究院计算平台组的资深工程师,曾就职于华为、深信服等公司;关注 K8s、容器、存储等领域。
专题推荐:
领取专属 10元无门槛券
私享最新 技术干货