简介:AI 应用对存储系统的挑战是全面的,从离应用最近的数据计算如何加速。
今天的分享内容主要分为三个部分:
第一部分简单梳理一下企业的 AI 训练基础设施的发展历程,通过展现云原生 AI 训练的一个完整流程,总结出和其中存储相关的问题。
第二个部分对这些问题做一些简单的分析,分享一下百度智能云是思考和分析这些问题的。
最后一个部分介绍百度智能云的沧海存储在高性能领域的全流程加速的方案,在这个方案里会对上述存储问题有完整的解答。
企业的 AI 训练基础设施是怎样一步一步发展到今天的模样的呢?这个发展过程其实经历了 4 个阶段:
阶段一:一开始企业训练的模型和数据量都不太大,最关心的是训练的性能,对计算之外的其它部分关注比较少,基础设施是怎么能快速跑起来怎么来。这一阶段主要是单机的训练,存储使用本地资源,如内存和本地盘。
阶段二:等到模型、数据量变大之后,单机训练就不能满足企业的需求,企业开始多机训练。对于存储,一方面数据集可能大到单机无法存储,另外一方面数据集需要方便地被多机共享。这个阶段,一个便捷和省心的选择就是去购买商用的网络存储。
阶段三:等到企业用户的训练规模、业务规模继续不断的增长之后,购买了越来越多的机器。从企业的角度来讲,希望能够充分地把计算资源利用率提高上来。企业在这一阶段引入训练平台解决这个问题,让不同业务的训练能够最大限度的并行跑在训练平台上。这个阶段对存储的需求产生了分化。一方面,数据量大了之后,企业成本敏感,大量的数据需要大容量低成本的存储。另一方面,对于训练的时候需要的那部分数据,还是希望存储的性能能够满足训练的要求,这一部分仍然是一个高性能存储的需求。这个阶段企业的规模已经相对比较大了,有足够的动力去自研或基于开源方案二次开发整个存储体系。
阶段四:进入云时代,企业尝试把已经比较成熟的体系往云上搬,所以看起来云时代的 AI 训练基础设施架构就是阶段三的翻版,对于存储而言,仍然是“大容量存储 + 高性能存储”的组合。但是这里其实已经发生了比较重要的一个变化,就是它的数据流向和阶段三时代是不一样的,这在后面的部分有详细的介绍。
现在来看今天的云原生 AI 训练基础设施,最底层是数据湖存储,定位是大容量、高吞吐、高吞吐、低成本、高可靠的存储底座,是企业进行数据流转的核心。
在数据湖之上,针对 AI 训练的高性能需求,提供一个加速层作为数据湖存储的补充,弥补数据湖存储在支撑高性能计算时性能不足的问题。靠近计算的位置有训练平台、各类 AI 框架,和硬件基础设施。
如果我们仔细观察企业的 AI 训练基础设施的发展过程,会发现其中的存储问题是不断的累加的,每一个阶段之前的问题仍然存在,但又出现的新的问题。
例如,在阶段一阶段二的时候,主要关心的是训练效率,这时候对存储的要求是不能拖计算的后端,必须能支撑好计算,这个问题无论是什么时候都是最核心的问题。因此,云原生时代面临的存储问题,涵盖了之前所有阶段的问题,我们只需要分析这一个阶段即可得到所有的疑惑和答案。
需要注意的是,从最开始的本地盘,到后来的商用网络存储、冷热分层、数据湖,存储在这个发展过程中呈现出来的一个大趋势就是,存储离计算的距离越来越远。这就导致今天我们去做一个云原生的 AI 训练,会经历一个很长的流程:
云原生时代的业务数据首先是集中积累到数据湖存储中,这里就包括未来用于训练的数据。我们面临的第一个问题就是数据湖存储如何选型,以保证能够可靠的存放企业的海量数据,这就是“ ①海量数据”问题。
准备训练前,因为数据湖存储不能满足训练时的性能要求,所以训练数据需要转移到一个速度更快的加速层中,这个转移过程就是“ ②数据流转”问题。
临近训练,训练平台负责协调训练要求的各类资源,这里当然也包括存储资源,那么存储和调度器如何才能配合好,产生了“ ③资源调度”问题。
经历了漫长的流程链之后,终于可以跑训练了,这个时候存储要达到一个什么样的性能,才不至于成为计算的瓶颈,这是“ ④计算加速”问题。
上面的流程梳理下来是“ ①海量数据 -> ②数据流转 -> ③资源调度 -> ④计算加速”这样一个顺序,但后面的部分会按照“ ④计算加速 -> ③资源调度 -> ①海量数据 -> ②数据流转”这样的顺序讲述,从大家最关注的计算加速问题讲起。
在分析这个问题之前,让我们通过一个简单的例子来了解一下一个典型的训练究竟长什么样:
训练的计算会经过很多很多轮,每一轮称为一个 epoch,每一轮 epoch 重复以下过程:
首先,为了让算法达到比较好的鲁棒性、加快收敛速度,把整个训练使用的数据集随机打散,类似于我们打牌前先把牌洗一遍,这个过程称之为 shuffle。至于样本集包含哪些样本,是从存储系统读取的。
shuffle 确定了数据集样本的读取顺序,接下来,数据会进一步分成很多个批次(batch),算法接下来就读取一个 batch 训练一次,直到整个样本集都处理完。这个过程涉及到大量的读操作。
一次训练持续的时间可能非常长,数个小时甚至数天数个月都有可能。所有的机器都没有办法保证在训练的过程百分之百没有故障发生,故障发生后,用户不会想从头开始计算,能恢复到一个较近时间点,从那个时间点重新开始计算是最理想的。因此需要有故障恢复的机制,通常的做法是周期性的把训练的状态保存下来,故障发生后,加载保存的状态继续计算。这个机制叫 checkpoint。checkpoint 对存储系统来说是写操作。
图片
从上面的分析,可以看到和存储有关的操作包含 3 种,shuffle、读 batch、checkpoint,前两者是读,后者是写。那么这些操作究竟对训练产生什么影响呢?
为了解答这个问题,在下图中列出一个简单的公式。衡量存储在这个过程中间是不是好,我们需要看的是整个训练过程中,真正用于计算的那部分时间在总时间中所占的比例。这个比例越高,说明存储的影响就越小。在不考虑其它因素、假设计算时间不变的情况下,这就是要求让存储的影响时间变短。
checkpoint 不一定每个 epoch 都保存,且是对存储系统比较友好的顺序大 I/O,整体耗时占比较小,一般分析时会忽略它的影响。如果遇到 checkpoint 慢的情况,可以做细致的分析。
读操作的部分,我们首先看 shuffle。前面说到 shuffle 需要把整个数据集做一次打散,在这个打散完成前,其实是是没有数据可以计算的,也就意味着这是一个纯粹等待的时间。这个时间从根本上无法完全消除,只能通过更高效的实现方法、更好的存储性能,把它在整体时间中的占比降到一个可接受的比例。
接下来看读 batch 的部分。对于 “读 batch 然后训练” 这个不断重复的过程,现代的一些训练框架其实已经做了很多的优化,目前在大部分框架里,“读 batch” 的这个过程,由所谓的 Data Loader 模块来完成,它的思路是让读的过程和计算本身并行起来。这对 GPU 训练的效果更明显一些。
在GPU训练中,计算和读数据的工作分别是由 GPU 和 CPU 来完成的,在 GPU 忙于一个 batch 的计算的时候,CPU 其实是空出来的,可以提前开始读取后面一个或多个 batch 的数据。整个过程呈现出一种流水线的感觉。第一个 batch 开始读的时候还没有数据可以计算,只能等待,但是从第二个 batch 开始,花费在存储上的时间如果短于前一个 batch 计算的时间,就可以被计算完全掩盖掉。因此,这个阶段对于存储的要求是快到不拖累计算、读等待时间接近 0 即可。
具体到这两个阶段,我们可以拿起放大镜再仔细观察一下这里到底包含哪些操作。
在 shuffle 阶段,需要把样本集包含哪些样本列出来,一种典型的朴素实现是将所有的样本按照目录分类存放,我们就可以通过文件系统提供的 ls 功能来枚举目录获取完整的文件列表,这时候就产生了大量的 open、getdents、close 操作,这些操作都可以归类为元数据操作。
多个 Data Loader 会启动多个进程,读取这个 batch 所有的样本文件,对于每一个文件都是 “ open -> stat -> read -> close” 这样的流程,其中 open、stat、close 是元数据操作,read 是数据操作。
我们很容易观察到一个现象,对于一个样本数量确定的数据集,元数据操作的数量是确定的,只有 read 操作可能会因为文件大小而变化,对于较小的(几百 KB 以下)文件,一次 read 操作就可以完成,对于较大的文件(数 MB 以上),需要调用多次 read。因此,样本大小影响了元数据操作耗时的占比,元数据操作的耗时占比进一步决定了元数据还是数据对训练的影响更大。
根据一些统计的结果,可以发现很多训练的样本集面临的情况是,样本数量非常大,但样本的平均大小又很小。以 ImageNet 数据集为例,整个数据集包含几百万(ImageNet 1K)、上千万(ImageNet 22k)的图片,平均一个图片大小仅为一百多 KB。这个大小对存储系统来说是非常小的。
因此,很多 AI 训练都面临海量小文件的问题。如果大家对存储系统的架构有一定了解,就会知道,在一个存储系统里,元数据的扩展性和性能是远比数据部分差的。这个结论无论对单机的存储系统还是分布式的存储系统都成立。
那应该如何来解决这里面临的计算加速问题呢?
假设要解决的这个问题是我们的敌人,我们要打败它,有几个方向可以尝试的方向:
方向一:我发现敌人太强大了,能不能削弱它的实力,让它变得更弱呢
前面说到海量小文件场景的关键是元数据操作的占比较高,同时也说到,元数据操作的性能在存储系统里是更差的。那么我们是不是能把较难的元数据操作转化成更容易的数据操作呢,如果能做到这一点,就可以把问题简化。
这里采用的优化措施主要是软件层面的。对于 shuffle,可以为样本集维护一个单独的列表文件,或者采用更合适的元数据系统。对于读数据,可以通过 TFRecord、HDF5 等打包格式,对小文件进行打包,变成大文件。这些操作的核心都是将元数据操作转化成更容易优化的数据操作。这类优化措施的主要缺点是需要用户改变使用习惯。
方向二:不管敌人有多强大,让我自己变得更强肯定没错
硬件层面,就是开启 “买买买” 模式,花更多的钱,使用更高规格更快的硬件来支撑训练。例如,存储介质方面使用更多的内存、更快更多的 SSD 硬盘等,网络方面升到 100G/200G 的高速 TCP/RDMA 网络。
软件方面,对软件架构做一些升级来提高整个软件系统的扩展能力,缩短软件栈的 I/O 路径长度。这里大家可能有一定了解的方案就是并行文件系统,这一类文件系统通过 hash、stripe 等方式将元数据和数据尽可能地分散到更多的节点上,以此来提高元数据和数据的并发处理性能。这类系统同时还会实现私有的内核客户端,请求直接发送给存储节点,以此来缩短 I/O 路径。
方向三:在我们和敌人相对实力不变的情况下,想办法让敌人离我更近,让同等力道的拳头对敌人造成更大的伤害这样的方法同样包含硬件和软件上的一些方法。
硬件方面,我们可以在组网的时候让计算节点和存储节点在物理上靠得更近。这件事情和硬件升级本身是独立的,因为如果仅仅是升级硬件,一次操作还是需要跨越很多跳交换机,效率同样会受到影响。对于单机而言,GPU Direct Storage 这样的技术,可以让 GPU 直接去读取存储系统上的数据,消除掉一部分 CPU 处理的开销,本质上是抄了一个近道,少绕了一些路。
软件方面,可以做的一点是让需要访问的数据能够尽可能的搬到计算节点本地,或者和计算节点比较近的地方。这就是缓存的思路,通过利用计算节点本地的冗余的内存、磁盘资源,让一些请求在缓存里处理掉,会比直接访问存储系统的性能更好。
到目前为止,我们的这些优化思路里,已经提出来两个比较重要的软件方案,一个是并行文件系统,一个是缓存。接下来再稍微展开介绍一下这两类系统。
并行文件系统和其它种类的文件系统有什么样的差别呢?今天我们去查看公有云厂商的官网,可以发现文件存储产品通常分为两类:
第一类系统是 NAS。这是一种对标 NetApp 这类传统厂商提供的网络文件存储产品,提供标准的 NFS、SMB 协议访问能力,产品形态上是 serverless 的。这样一类产品的一个特点就是标准,NFS 和 SMB 协议是得到业界认可的标准协议,用户使用时没有兼容性负担和学习成本,主流操作系统也会内置支持,基本上是开箱即用的状态。但这类存在的一个最大缺点就是,为了去兼容标准的协议,需要在处理路径上引入专门的协议处理节点,这些节点负责将请求转化为存储节点可以理解的内部格式。这种架构有两个影响性能的问题。第一个问题是让请求的处理链条变得更长,让延时变大。第二个问题是协议处理节点本身可能成为处理瓶颈,影响并发度。
第二类系统是并行文件系统。并行文件系统走了和 NAS 相反的路线,专门针对特定的高性能场景去做一些极致的优化。这类系统通常会放弃或弱化对标准协议的兼容性,取而代之,实现私有的客户端协议,通常运行在内核态。业务的请求进入客户端后,迅速、直接发往后端的存储节点。这种设计让软件上的 I/O 路径达到最短,充分利用后端节点、设备的并行处理能力。这类系统的代表有 Lustre、BeeGFS、GPFS,和百度智能云PFS。
简单总结一下,想要兼容性更好,选择 NAS 类的产品,想要性能更高,选择并行文件系统产品。
从我们之前的分析可以看出,AI 场景下对数据集的使用是只读的。因此,对于 AI 训练场景,缓存是一个非常好的优化手段。具体做法是将数据缓存到计算节点本地或临近节点的内存、磁盘上,达到就近访问的效果。缓存系统在 cache miss 的情况下需要从远端存储系统读取数据,性能是比较低的,训练中应该尽量避免这样的情况。
目前,本领域业界主流的缓存解决方案分为两大类。一类是 Alluxio 这类相对比较纯粹的缓存系统,不改变数据的格式,忠实地做一个数据的搬运工。另外一类是近些年比较热门的云原生文件系统,如 JuiceFS,这类系统在对象存储之上重新定义了一层数据结构,数据只能通过系统本身访问。无论是哪一类系统,提升计算侧性能的本质仍然是缓存。阿里云 JindoFS、百度智能云 RapidFS 都是兼有这两类缓存系统能力的统一解决方案,用户可以根据自己的实际需求选择不同的工作模式。
目前为止,已经提到了并行文件系统和缓存两大类软件解决方案,那么困扰选择恐惧症患者的问题来了,它们究竟有什么区别,该怎么选。
并行文件系统主要是面向高性能场景(传统 HPC、AI 场景)优化,提供了对 POSIX 标准文件系统接口的完整支持,所以大部分通用文件系统的需求,同样可以用这类文件系统来满足。从长远的发展趋势看,并行文件系统的依然是去兼容 POSIX,将文件系统的标准能力做大做强做好用。
缓存系统的实现则相对灵活很多,完全是为有限的特定场景定制的一类系统,目前在云上的主要服务场景是 AI 和大数据存算分离。这类系统的一个重要发展趋势是继续探索一些非标准的能力,和上层的框架、生态加强融合。
所以,整体上来看,并行文件系统可以适用于需要标准文件系统的场景,缓存系统是配合对象存储使用的定制系统,这两个方案只是在 AI 场景下产生重叠。如果业务尚未没有完成云原生的改造,或者强依赖文件系统的一些语义(如多进程并发写同一文件),并行文件系统是更省心的选择。如果业务已经是数据湖为核心的云原生架构,在 AI 的场景下,可以尝试缓存系统。
从长远看,这两类系统更多的是互补的关系:
性能方面:虽然说缓存系统通常是用来加速一些相对较慢的存储系统,但并不能排除可以用来进一步加速并行文件系统。例如,元数据请求即使通过并行文件系统处理比较快,但仍然产生很多轮的网络访问开销,如果能在其上叠加一层元数据缓存,对提高性能也是很有帮助的。
功能方面:缓存系统可以更好的实现一些非标准的能力,配合一个标准的高速文件系统,可以有一些好玩的用法,这些用法其实也是业界探索的方向。例如,前面提到一种优化 shuffle 的方法是使用文件列表替代 ls,缓存系统完全可以实现一种功能,将列表映射成文件系统目录的结构,并缓存下来,而数据访问继续由并行文件系统处理。这样的方式可以让之前的训练代码继续使用,不感知其中的变化。
所有的训练平台都存在一个终极的梦想,就是让计算资源利用率维持在一个较高的水位。这个水位如果能到 60%+、70%+ 是一个很了不起的事情。这儿有些同学可能不能理解,平时的训练很容易达到 90%+ 的利用率呀,为什么上了训练平台就下降了呢?
主要的原因是在一个完整的训练流程里,除了计算的部分,还有很多其它的操作和步骤,训练之间也可能存在串行、并行等关系。这些操作如果调度上处理不好,让计算资源处于等待的状态,就会拉低整体的利用率。此外,还有低峰期这样的因素进一步拉低利用率。
回到我们今天的主题,存储这部分在平台化训练和云原生训练阶段面临的问题是一致的。在这两个阶段,我们会发现很多时候数据的来源是速度较慢的大容量存储或者数据湖。这就导致在真正开始训练前,需要一个数据准备的阶段,将数据搬到速度更快的存储里。
我们来看一个简化的例子,Task 1 和 Task 2 申请同一批 GPU 资源,数据准备时间和 GPU 计算时间一样长。
如果不在调度上做优化,直接按照算力需求,将任务作为整体调度,即下图中的调度策略 1,那么 Task 1 和 Task 2 只能串行执行,中间有很长的时间被数据等待浪费掉了,整个时间段的 GPU 利用率平均下来只有 46%。这样的情况显然是不理想的。
让我们换一个角度看这个问题。回想一下 Data Loader 是如何解决让读数据和计算并行的,显然这里的问题也是类似的。我们完全可以让数据准备的时间和训练的时间并行起来。如图中的调度策略 2 展示的那样,在Task 1 训练的期间,就可以开始 Task 2 的数据准备工作。在这个例子里,刚好 Task 2 的数据准备时间都可以被隐藏掉。这个调度策略可以让整体的 GPU 利用率提升到 62%。
这里的思路具有一定的普适性,对于优化其它的资源利用率问题也有参考意义,本质上就是让不产生竞争的环节最大可能的并行运行,达到工厂流水线的效果。
思路有了,具体应该怎么做呢。有聪明人在开源社区已经先行了一步,实现了一个基于该思路的项目 Fluid。Fluid 自称是一个数据集编排跟加速的这样一个框架,其核心思想就是上面描述的流水线调度。
Fluid 把数据集的准备工作从整个流程里单独抽取了出来,利用底层的 K8s 分配需要的资源,例如缓存系统需要的内存和磁盘资源。资源分配出来后,可以选择性的发起元数据和数据的预热工作。等预热工作完成之后,再由 K8s 调度计算资源运行训练任务。训练任务可以选择是否亲和,所谓亲和是让缓存的节点和计算的节点是同一批节点,这个能带来什么样的好处待会儿会讲到。训练完成后,用户可以视情况而定决定何时回收 Fluid 占用的资源,这个过程和计算也是独立的。
目前,社区对 Fluid 的定位仍然是围绕缓存系统展开的,但我们认为数据准备在 AI 训练里是一个普遍的问题,同样的原理也适用于并行文件系统和其它存储系统。从这个角度看,Fluid 的适用范围可以超出社区对它的定位。
图片
在这个问题的最后,对 Fluid 的亲和调度稍微展开再介绍下。亲和性调度主要对缓存系统有帮助,体现在两个方面。
第一个好处是性能方面的。计算节点和缓存节点是同一批节点,保证了计算节点访问缓存数据的物理路径是比较优的。同时,部分数据通过本地就可以满足,不再需要网络访问。
第二个好处是容错方面的。缓存系统和并行文件系统最大的一个差别是本身的容错能力较弱,缓存节点故障,就意味着其上的缓存数据失效。如果没有亲和调度,即使能够从远端重新拉取数据,也需要忍受一段时间的性能下降,这段时间会拉低计算资源的利用率。有了亲和调度之后,缓存节点和计算节点同时失效,在恢复计算任务前有机会先恢复缓存数据,就规避了性能退化的问题。
前面我们的关注点主要在 AI 训练部分,这部分对存储的性能有极致的要求。但如果我们放眼去观察一下所有 AI 相关的流程,包括数据集管理、项目管理、预处理这些,会发现除了训练和预测,其它的流程对高性能并没有一个很强的诉求,它们的需求归纳下来是一个高吞吐、可共享、大容量、高可靠的存储系统。
在考虑到企业中存在其它各种业务,这些业务可能是 AI 的数据集来源,也可能是 AI 的使用方,这个存储系统需要能够方便地和这些业务进行数据交流。综合来看,统一的数据湖存储是最佳选择。
有必要指出的一点就是,选择数据湖的背后,是业务间的数据流转方式已经发生了翻天覆地的变化。在云原生之前,不同类型的业务处于信息孤岛的状态,大数据的业务使用 HDFS,高性能计算使用并行文件系统,数仓系统自己保存数据。系统 B 需要使用系统 A 的数据,需要把数据从系统 A 里导出来一份。这种点对点的数据交流显然是比较低效和繁琐的。
数据湖、存算分离这些概念的兴起,让业界达成一个共识,那就是建设统一的数据湖存储底座,围绕数据存储进行数据流转,可以有效的解决系统间数据流转的问题。对这部分内容感兴趣的同学可以阅读“百度智能云技术站”公众号的的数据湖系列专题文章。
大家认可的数据湖存储候选系统有两个,一个是大数据领域使用较多的 HDFS,一个是起源自 AWS S3 的对象存储。
HDFS 主要有两个模块,分别是用来提供元数据服务的 NameNode,和用来存储数据的 DataNode。HDFS 的元数据是按照树形结构来组织的,即所谓的层级命名空间。这种元数据组织方式的特点就是除了根目录外,每一个文件或者目录,都从属于一个父目录,从根目录往下看,就像是一颗倒挂的树。HDFS 的数据一般是 3 副本保存的。
对象存储的架构主要分为三个部分。首先因为对象存储提供的接口形式是标准的 HTTP,需要一些 Web Service 服务器来处理接口调用。Web Service 服务器处理的过程中会根据请求的类型拆分成元数据请求和数据请求,分别交由相关的子系统来负责:
元数据子系统:在实现上一般基于分布式 KV 或 NewSQL ,提供平坦命名空间。平坦命名空间的特点是其中的每一个对象(等价于 HDFS 中的文件或目录)没有从属关系,互相独立。数据子系统。
数据子系统:使用纠删码(Erasure Coding,EC)技术存储数据,它的原理是将原始数据切成 N 个等大小的块,并计算出 P 个冗余的校验块。这些数据块和校验块存储到不同的节点上,只要有不少于 N 个块存活,就能恢复出来完整的数据。
我们来简单地从几个方面对比一下这两个系统:
成本:对象存储的成本要比 HDFS 低很多,因为以下几个原因:
首先是副本数。HDFS 自己有 3 副本,直接搬到云上使用云磁盘搭建,还存在副本放大问题。因为云磁盘本身也有 3 副本,叠加起来其实是 9 副本的开销。与之比较,对象存储的纠删码可以做到 1.5 副本甚至更低。这部分的成本就差出来很多。
其次,云厂商的对象存储对不同冷热程度的数据使用不同的纠删码配置和存储介质,最冷的数据可以存储到磁带中,磁带比 HDFS 使用的机械硬盘成本更低。
最后,HDFS 的数据湖是存算一体架构。所谓存算一体是说,同样一批机器既用来跑计算,也用来跑存储。这个架构的优势是可以实现亲和调度,调度的时候让计算任务直接跑到数据所在的节点上,和我们前面说的缓存亲和调度异曲同工。但这也带来了一个问题,就是计算必须和存储同步扩容,现实中很难做到刚好匹配,必然会存在一类资源的浪费。这个问题在对象存储存算分离数据湖架构上不存在,对象存储服务只提供纯粹的存储能力,计算资源是客户按需购买的,计算需求不存在的时候甚至可以不买计算资源,非常灵活。采用存算分离架构,用户可以避免很多计算资源的浪费。
扩展性:层级命名空间和平坦命名空间相比,扩展性要差很多:
这里主要就是因为层级命名空间需要维护父子关系。HDFS 为了简化这个关系的维护,使用了单点 NameNode 的设计,数据还直接放在内存里,这导致这个单点很难扩展,性能上限也比较低,通常一个系统只能保存数亿文件,几十 PB 的数据。虽然社区后来推出了 Federation 的功能,但没有本质上解决问题。
对象存储则不同,平坦命名空间里的每个对象天然没有任何关联,可以作为独立的个体对待,关联性的打破让扩展性更容易做。云厂商的对象存储服务因此可以做到一个集群 EB 级的容量,万亿条元数据,比 HDFS 大很多。
可用性&可靠性:HDFS 是一个单机房的服务,同一个数据只能容忍 2 个副本出现问题。对象存储可以部署到多个机房,EC 编码也可以容忍更多的块丢失,如 18 + 6 的编码可以容忍 6 个块故障,这些保证了对象存储能够提供更好的可用性和可靠性保障,完胜 HDFS。
吞吐:大家对数据湖比较关注的性能指标就是吞吐,这个指标和集群规模是强相关的,集群的节点数越多,磁盘数量越多,能够提供的吞吐就越高。云厂商的对象存储在这方面有天然的优势,一方面集群规模要大很多,另外一方面其中很多数据的访问频率非常低,不会对需要高吞吐的数据产生挤兑,性能容易得到保障。
生态:两个系统都是接受度比较高的。对象存储缺乏对 rename、边写边读等特性的支持,使得它目前还不能满足部分大数据的场景需求。不过云厂商也在积极的解决这些问题,例如百度智能云推出了 RapidFS 和 BOS 层级 Namespace 作为补充。
经过这样的对比,可以得到一个简单直观的结论就是在对象存储是数据湖存储的首选。
数据湖存储成为整个数据流转的 C 位之后,带来的最大的一个变化就是数据流向完全逆转了。在传统的架构里,大容量的冷数据存储和高性能存储是分别维护的,对于 AI 训练的部分,数据的存储、生产、消费都发生在高性能存储中,自成体系,只有转冷的数据才会考虑转移到大容量存储中去。比例较小的反向数据流转(从大容量存储到高性能存储)通过工具来解决。
但到了数据湖里,数据湖存储才是最全量、最权威的数据来源,大部分情况下,数据的第一个落脚点是数据湖,然后才会到高性能的加速层。在存算分离架构中,加速层本身都只是临时的存在,其中的数据生命周期和计算资源同步,略早于计算资源的创建而生成,计算资源销毁时同步删除。这就导致数据湖到加速层的数据同步成为一个高频、核心的需求,需要花大力气解决。
很容易想到的是一种比较朴素的方案,经常被大家用来做数据同步和迁移。简单的讲,这个方案就是准备一个中转机,在同一台机器上把数据湖存储和加速层高性能存储都挂载上,然后使用 cp 或 rsync 之类的工具来搬迁数据。
大家如果使用过这样一类方法,会很容易发现存在一些问题:
第一个问题是同步速度慢。这里受到很多环节的限制,中转机的硬件配置、存储系统单客户端的并发度等等因素。文件系统层也有很多冗余开销,这些冗余开销主要来自元数据,对于读的那方,每个文件都需要 open、stat、close,对于写的那方,每个文件需要 open、setattr、close。前面分析训练效率的时候提过这类操作对海量小文件非常不友好,但通过 cp、rsync 无法优化掉这些开销。
第二个问题是同步策略不可定制,在有一些场景下,我们可能会希望只做元数据的同步,或一些特定条件数据的同步。例如,一些训练里文件都很大,训练任务对延时不敏感,数据湖存储就足以满足吞吐的要求,这时候只需要元数据的缓存。cp 和 rsync 这样的工具无法满足要求。这种方法在处理增量同步的时候也比较低效,需要对比全量的文件列表,而对象存储是支持通知机制的,如果能利用这一点能够极大地改善增量同步的效率。
第三个问题是和调度器的集成困难。朴素的方案在实现上采用脚本和命令行工具实现,复杂的环境下稳定性比较差,状态的获取和解析也不是很灵活。如果操作系统环境变化了,没测试过,真正使用的时候可能会发现跑不起来。这些问题导致和调度器集成时会遇到各种各样的问题,需要人工介入排查,维护成本极高。
最后一个问题是这种方式同步成功还好,同步失败后需要善后,处理残留的垃圾,这些也需要介入或者半人工的方式来处理。
上述诸多因素的叠加,让朴素方案在云原生时代显得非常的笨重和低效。我们认为,加速层的存储系统内置数据同步的能力才是解决这一关键问题的正确思路。
在系统内部实现,可以解决朴素方案存在的各类问题。
对于同步速度慢的问题,充分利用系统的多个节点去并发的同步数据,缩短中间环节,不给中间商赚差价的机会。
至于哪些数据同步,哪些不同步,在系统内部很灵活,容易实现各种各样的策略。
调度器方面可以结合前面说的 Fluid 框架,将同步的发起、状态的展示、容错做到完全自动化。
垃圾回收的问题也同样可以在 Fluid 的框架内解决掉,由 Fluid 的机制保证资源自动释放。
百度沧海·存储根据上面的分析,推出一整套高性能存储的解决方案。
方案的数据湖部分由对象存储 BOS 来承担,除了前文分析的大容量、高吞吐、低成本的特点,还内置了分级存储和智能生命周期功能。分级存储是进一步细分对象存储数据冷热降低成本的手段,配合智能生命周期可以让业务在更高的性能和更低的总持有成本之间取得平衡。
支撑训练的部分由专门的加速层来完成。这一层首先从网络环境保证存储和计算是离得比较近的,让存储能够达到最好的性能。在这基础上,包含两个软件产品,一个是并行文件系统 PFS,一个是数据湖存储加速 RapidFS。每个 PFS 实例部署在托管的裸金属 BBC 上,或者虚拟机 BCC 上,无论是哪种模式,使用的硬件性能均有定量保证。每个 RapidFS 实例的资源则来自计算节点本地的内存和磁盘。PFS 和 RapidFS 均支持 Bucket Link 实现数据湖数据的自动同步,同时也支持 Fluid 调度器。
在我们的方案里,对之前讨论的问题都有解答,具体如下:
海量数据:由 BOS 及周边生态解决。
数据流转:PFS 和 RapidFS 内置的 Bucket Link 能力支持自动从数据湖同步数据。
资源调度:PFS 和 RapidFS 均支持 Fluid 调度器,使用方式接近,用户可灵活切换。
数据加速:PFS 和 RapidFS 作为数据湖加速层,采用高速硬件,近计算部署的方式保证性能。
最后简单看一下 PFS 和 RapidFS 在实际支持用户训练时候的效果。在这个例子里,有三组数据,分别是基于 RapidFS、PFS、对象存储直接来做 GPU训练。可以看出,当 RapidFS 和 PFS 使用 Bucket Link 做完数据预热之后,可以保证训练期间的 GPU 利用率打满。基于对象存储直接来做这个训练的话,中间很大一部分时间是消耗在数据的读取上,整个 GPU 利用率在一个非常低的水位上。通过这样一个实验,我们大致能够看到 RapidFS 跟 PFS 在计算加速这一块的效果。