技术

数据湖 高性能计算与存储 Linux2.1.13网络源代码学习 《大数据经典论文解读》 三驾马车学习 Spark 内存管理及调优 Yarn学习 从Spark部署模式开始讲源码分析 容器狂占内存资源怎么办? 多角度理解一致性 golang io使用及优化模式 Flink学习 c++学习 学习ebpf go设计哲学 ceph学习 学习mesh kvm虚拟化 学习MQ go编译器以及defer实现 学习go 为什么要有堆栈 汇编语言 计算机组成原理 运行时和库 Prometheus client mysql 事务 mysql 事务的隔离级别 mysql 索引 坏味道 学习分布式 学习网络 学习Linux go堆内存分配 golang 系统调用与阻塞处理 Goroutine 调度过程 重新认识cpu mosn有的没的 负载均衡泛谈 单元测试的新解读 《Redis核心技术与实现》笔记 《Prometheus监控实战》笔记 Prometheus 告警学习 calico源码分析 对容器云平台的理解 Prometheus 源码分析 并发的成本 基础设施优化 hashicorp raft源码学习 docker 架构 mosn细节 与微服务框架整合 Java动态代理 编程范式 并发通信模型 《网络是怎样连接的》笔记 go channel codereview gc分析 jvm 线程实现 go打包机制 go interface及反射 如何学习Kubernetes 《编译原理之美》笔记——后端部分 《编译原理之美》笔记——前端部分 Pilot MCP协议分析 go gc 内存管理玩法汇总 软件机制 istio流量管理 Pilot源码分析 golang io 学习Spring mosn源码浅析 MOSN简介 《datacenter as a computer》笔记 学习JVM Tomcat源码分析 Linux可观测性 学习存储 学计算 Gotty源码分析 kubernetes operator kaggle泰坦尼克问题实践 kubernetes扩缩容 神经网络模型优化 直觉上理解深度学习 如何学习机器学习 TIDB源码分析 什么是云原生 Alibaba Java诊断工具Arthas TIDB存储——TIKV 《Apache Kafka源码分析》——简介 netty中的线程池 guava cache 源码分析 Springboot 启动过程分析 Spring 创建Bean的年代变迁 Linux内存管理 自定义CNI IPAM 共识算法 spring redis 源码分析 kafka实践 spring kafka 源码分析 Linux进程调度 让kafka支持优先级队列 Codis源码分析 Redis源码分析 C语言学习 《趣谈Linux操作系统》笔记 docker和k8s安全访问机制 jvm crash分析 Prometheus 学习 Kubernetes监控 容器日志采集 Kubernetes 控制器模型 容器狂占资源怎么办? Kubernetes资源调度——scheduler 时序性数据库介绍及对比 influxdb入门 maven的基本概念 《Apache Kafka源码分析》——server Kubernetes类型系统 源码分析体会 《数据结构与算法之美》——算法新解 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 ansible学习 Kubernetes源码分析——从kubectl开始 jib源码分析之Step实现 jib源码分析之细节 线程排队 跨主机容器通信 jib源码分析及应用 为容器选择一个合适的entrypoint kubernetes yaml配置 《持续交付36讲》笔记 mybatis学习 程序猿应该知道的 无锁数据结构和算法 CNI——容器网络是如何打通的 为什么很多业务程序猿觉得数据结构和算法没用? 串一串一致性协议 当我在说PaaS时,我在说什么 《数据结构与算法之美》——数据结构笔记 PouchContainer技术分享体会 harbor学习 用groovy 来动态化你的代码 精简代码的利器——lombok 学习 《深入剖析kubernetes》笔记 编程语言那些事儿 rxjava3——背压 rxjava2——线程切换 spring cloud 初识 《深入拆解java 虚拟机》笔记 《how tomcat works》笔记 hystrix 学习 rxjava1——概念 Redis 学习 TIDB 学习 如何分发计算 Storm 学习 AQS1——论文学习 Unsafe Spark Stream 学习 linux vfs轮廓 《自己动手写docker》笔记 java8 实践 中本聪比特币白皮书 细读 区块链泛谈 比特币 大杂烩 总纲——如何学习分布式系统 hbase 泛谈 forkjoin 泛谈 看不见摸不着的cdn是啥 《jdk8 in action》笔记 程序猿视角看网络 bgp初识 calico学习 AQS——粗略的代码分析 我们能用反射做什么 web 跨域问题 《clean code》笔记 《Elasticsearch权威指南》笔记 mockito简介及源码分析 2017软件开发小结—— 从做功能到做系统 《Apache Kafka源码分析》——clients dns隐藏的一个坑 《mysql技术内幕》笔记 log4j学习 为什么netty比较难懂? 递归、回溯、动态规划 apollo client源码分析及看待面向对象设计 学习并发 docker运行java项目的常见问题 OpenTSDB 入门 spring事务小结 分布式事务 javascript应用在哪里 《netty in action》读书笔记 netty对http2协议的解析 ssl证书是什么东西 http那些事 苹果APNs推送框架pushy apple 推送那些事儿 编写java框架的几大利器 java内存模型和jvm内存布局 java exception Linux IO学习 netty内存管理 测试环境docker化实践 netty在框架中的使用套路 Nginx简单使用 《Linux内核设计的艺术》小结 Go并发机制及语言层工具 Linux网络源代码学习——数据包的发送与接收 《docker源码分析》小结 docker namespace和cgroup zookeeper三重奏 数据库的一些知识 Spark 泛谈 链式处理的那些套路 netty回顾 Thrift基本原理与实践(二) Thrift基本原理与实践(一) 回调 异步执行抽象——Executor与Future Docker0.1.0源码分析 java gc Jedis源码分析 深度学习泛谈 Linux网络命令操作 JTA与TCC 换个角度看待设计模式 Scala初识 向Hadoop学习NIO的使用 以新的角度看数据结构 并发控制相关的硬件与内核支持 systemd 简介 quartz 源码分析 基于docker搭建测试环境(二) spring aop 实现原理简述 自己动手写spring(八) 支持AOP 自己动手写spring(七) 类结构设计调整 分析log日志 自己动手写spring(六) 支持FactoryBean 自己动手写spring(九) 总结 自己动手写spring(五) bean的生命周期管理 自己动手写spring(四) 整合xml与注解方式 自己动手写spring(三) 支持注解方式 自己动手写spring(二) 创建一个bean工厂 自己动手写spring(一) 使用digester varnish 简单使用 关于docker image的那点事儿 基于docker搭建测试环境 分布式配置系统 JVM执行 git maven/ant/gradle/make使用 再看tcp kv系统 java nio的多线程扩展 《Concurrency Models》笔记 回头看Spring IOC IntelliJ IDEA使用 Java泛型 vagrant 使用 Go常用的一些库 Python初学 Goroutine 调度模型 虚拟网络 《程序员的自我修养》小结 Kubernetes存储 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件 Go基础 JVM类加载 硬币和扑克牌问题 LRU实现 virtualbox 使用 ThreadLocal小结 docker快速入门

架构

controller-runtime细节分析 finops学习 kubevela多集群 kubevela中cue的应用 基于k8s的工作流 容器和CPU那些事儿 kubevela源码分析 数据集管理fluid 应用管理平台kubevela karmada支持crd 多集群及clusternet学习 AutoML和AutoDL 特征平台 实时训练 分布式链路追踪 helm tensorflow原理——python层分析 如何学习tensorflow 数据并行——allreduce 数据并行——ps 机器学习中的python调用c 机器学习训练框架概述 embedding的原理及实践 tensornet源码分析 大模型训练 X的生成——特征工程 tvm tensorflow原理——core层分析 模型演变 《深度学习推荐系统实战》笔记 keras 和 Estimator tensorflow分布式训练 分布式训练的一些问题 基于Volcano的弹性训练 图神经网络 pytorch弹性分布式训练 从混部到统一调度 RNN pytorch分布式训练 CNN 《动手学深度学习》笔记 pytorch与线性回归 多活 volcano特性源码分析 推理服务 kubebuilder 学习 mpi 学习pytorch client-go学习 tensorflow学习 提高gpu 利用率 GPU与容器的结合 GPU入门 AI云平台梳理 tf-operator源码分析 k8s批处理调度 喜马拉雅容器化实践 Kubernetes 实践 学习rpc BFF openkruise学习 可观察性和监控系统 基于Kubernetes选主及应用 《许式伟的架构课》笔记 Admission Controller 与 Admission Webhook 发布平台系统设计 k8s水平扩缩容 Scheduler如何给Node打分 Scheduler扩展 controller 组件介绍 openkruise cloneset学习 controller-runtime源码分析 pv与pvc实现 csi学习 client-go源码分析 kubelet 组件分析 调度实践 Pod是如何被创建出来的? 《软件设计之美》笔记 mecha 架构学习 Kubernetes events学习及应用 CRI 资源调度泛谈 业务系统设计原则 grpc学习 元编程 以应用为中心 istio学习 下一代微服务Service Mesh 《实现领域驱动设计》笔记 概率论 serverless 泛谈 《架构整洁之道》笔记 处理复杂性 那些年追过的并发 服务器端编程 网络通信协议 架构大杂烩 如何学习架构 《反应式设计模式》笔记 项目的演化特点 反应式架构摸索 函数式编程的设计模式 服务化 ddd反模式——CRUD的败笔 研发效能平台 重新看面向对象设计 业务系统设计的一些体会 函数式编程 《左耳听风》笔记 业务程序猿眼中的微服务管理 DDD实践——CQRS 项目隔离——案例研究 《编程的本质》笔记 系统故障排查汇总及教训 平台支持类系统的几个点 代码腾挪的艺术 abtest 系统设计汇总 《从0开始学架构》笔记 初级权限系统设计 领域驱动理念入门 现有上传协议分析 移动网络下的文件上传要注意的几个问题 推送系统的几个基本问题 做配置中心要想好的几个基本问题 不同层面的异步 分层那些事儿 性能问题分析 当我在说模板引擎的时候,我在说什么 用户认证问题 资源的分配与回收——池 消息/任务队列

标签

controller-runtime细节分析 finops学习 kubevela多集群 kubevela中cue的应用 基于k8s的工作流 容器和CPU那些事儿 kubevela源码分析 数据集管理fluid 应用管理平台kubevela karmada支持crd 多集群及clusternet学习 helm 从混部到统一调度 volcano特性源码分析 kubebuilder 学习 client-go学习 tf-operator源码分析 k8s批处理调度 喜马拉雅容器化实践 Kubernetes 实践 openkruise学习 基于Kubernetes选主及应用 Admission Controller 与 Admission Webhook k8s水平扩缩容 Scheduler如何给Node打分 Scheduler扩展 controller 组件介绍 openkruise cloneset学习 controller-runtime源码分析 pv与pvc实现 csi学习 client-go源码分析 kubelet 组件分析 调度实践 Pod是如何被创建出来的? Kubernetes events学习及应用 CRI 资源调度泛谈 如何学习Kubernetes 以应用为中心 kubernetes operator kubernetes扩缩容 serverless 泛谈 自定义CNI IPAM docker和k8s安全访问机制 Kubernetes监控 Kubernetes 控制器模型 Kubernetes资源调度——scheduler Kubernetes类型系统 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 Kubernetes源码分析——从kubectl开始 kubernetes yaml配置 CNI——容器网络是如何打通的 当我在说PaaS时,我在说什么 《深入剖析kubernetes》笔记 Kubernetes存储 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件

高性能计算与存储

2022年07月28日

前言

面向高性能计算场景的存储系统解决方案高性能计算场景存储需求的总结

高性能计算 接口 吞吐 延时 IO类型
传统HPC POSIX 数十 GB/s 甚至数百 GB/s 数百us 以大文件为主,大量随机写,读写性能均重要
AI HPC POSIX,少量对象存储等接口
K8s CSI,GDS
数十 GB/s 甚至数百 GB/s 数百us 大量的读 I/O,如果样本都很小,就是大量的小 I/O
大数据 HPC HDFS 或兼容 Hadoop 的存储接口HCFS 数十 GB/s 甚至数百 GB/s 对延时不是特别的敏感 以大文件为主,顺序读写,读写性能均重要

补充一个场景:音频训练时是完全随机的,比如有1万条语音文件,平均时长5分钟,随机选一条其中的3s。但是这个随机到最后,会确保每个文件的每一个片段,都会取到。这个场景就是随机小文件读取,而且还重cpu。分布式存储一般吞吐很牛,但随机访问不如本地存储。后续:通过提高线程数来弥补随机访问性能的不足。

  1. 存储底座,整个高性能计算的过程实际上是分为很多个环节的,假如不同环节的数据分散在不同的存储系统上的话,对使用效率和使用方便程度上来讲,是一个比较大的挑战。所以百度内部使用了一个统一的存储底座来简化数据的管理和流转。存储底座的核心能力是高可靠、低成本、高吞吐。在这个统一存储底座的基础之上,会去支持一些高性能计算常用的接口需求,包括POSIX 文件接口以及 HCFS 大数据接口。一些对性能有更高要求的一些业务,愿意去做一些定制开发的话,它可以直接用存储底座提供的 SDK。
  2. 运行时存储,是近计算部署的,主要目的是为了让它们能够达到最好的 I/O 性能。解决和对象存储数据湖/底座之间的高效数据流转的问题,以及在调度平台上如何更简单地使用它们的问题。数据访问入口在 PFS或RapidFS,用户自己其实是不需要去关心数据到底是在对象存储还是在 PFS 里面。

AI HPC 中的存储问题

AI 应用的全流程存储加速方案技术解析和实践分享企业的 AI 训练基础设施是怎样一步一步发展到今天的模样的呢?这个发展过程其实经历了 4 个阶段:

  1. 一开始企业训练的模型和数据量都不太大,最关心的是训练的性能,对计算之外的其它部分关注比较少,基础设施是怎么能快速跑起来怎么来。这一阶段主要是单机的训练,存储使用本地资源,如内存和本地盘。
  2. 等到模型、数据量变大之后,单机训练就不能满足企业的需求,企业开始多机训练。对于存储,一方面数据集可能大到单机无法存储,另外一方面数据集需要方便地被多机共享。这个阶段,一个便捷和省心的选择就是去购买商用的网络存储。
  3. 等到企业用户的训练规模、业务规模继续不断的增长之后,购买了越来越多的机器。从企业的角度来讲,希望能够充分地把计算资源利用率提高上来。企业在这一阶段引入训练平台解决这个问题,让不同业务的训练能够最大限度的并行跑在训练平台上。这个阶段对存储的需求产生了分化。一方面,数据量大了之后,企业成本敏感,大量的数据需要大容量低成本的存储。另一方面,对于训练的时候需要的那部分数据,还是希望存储的性能能够满足训练的要求,这一部分仍然是一个高性能存储的需求。这个阶段企业的规模已经相对比较大了,有足够的动力去自研或基于开源方案二次开发整个存储体系。
  4. 进入云时代,企业尝试把已经比较成熟的体系往云上搬,所以看起来云时代的 AI 训练基础设施架构就是阶段三的翻版,对于存储而言,仍然是“大容量存储 + 高性能存储”的组合。但是这里其实已经发生了比较重要的一个变化,就是它的数据流向和阶段三时代是不一样的

读取性能

  1. AI HPC 实际上跟传统 HPC 相似的地方在于一个典型的计算也是分为很多轮。在每一轮计算中,不同的计算节点会去从存储系统把数据先预读上来,进行一些预处理,预处理完了之后由 GPU 来进行运算。在这个读的过程中,每个计算的节点其实只是负责了整个大的训练样本集合中的一小部分。这个读取工作实际上是通过训练框架内置的 Data Loader 机制来完成。整个过程中存在大量的读 I/O,如果样本都很小,就是大量的小 I/O。
  2. 在大规模的训练中,训练任务会周期性地做一些状态的保存,叫做 checkpoint,这里状态的保存主要起到故障恢复的作用。如果一个训练的耗费的时间非常长,在训练中间遇到一些机器故障重新做计算的代价就会很高。假如说有一些已经保存好的状态可以加载上来,接着这个状态把丢失掉的数据重新算一遍,这样会比完全重新计算要快很多。因此,在训练过程中产生的 checkpoint,可以减少需要故障恢复的数据量。这个过程以写 I/O 为主,在训练中的占比较小。

可以看到和存储有关的操作包含 3 种,shuffle、读 batch、checkpoint,前两者是读,后者是写。那么这些操作究竟对训练产生什么影响呢? 为了解答这个问题,在下图中列出一个简单的公式。衡量存储在这个过程中间是不是好,我们需要看的是整个训练过程中,真正用于计算的那部分时间在总时间中所占的比例。这个比例越高,说明存储的影响就越小。

  1. checkpoint 不一定每个 epoch 都保存,且是对存储系统比较友好的顺序大 I/O,整体耗时占比较小,一般分析时会忽略它的影响。当然,这里的结论不绝对,如果遇到 checkpoint 慢的情况,可以做细致的分析。
  2. 读操作的部分,我们首先看 shuffle。前面说到 shuffle 需要把整个数据集做一次打散,在这个打散完成前,其实是是没有数据可以计算的,也就意味着这是一个纯粹等待的时间。这个时间从根本上无法完全消除,只能通过更高效的实现方法、更好的存储性能,把它在整体时间中的占比降到一个可接受的比例。
  3. 对于 “读 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 训练都面临海量小文件的问题。如果大家对存储系统的架构有一定了解,就会知道,在一个存储系统里,元数据的扩展性和性能是远比数据部分差的。这个结论无论对单机的存储系统还是分布式的存储系统都成立。

接口

尽管越来越多的框架开始支持对象存储等接口类型,但这些接口形式使用门槛较高,被广泛接受还需要时间。文件接口依然是这个领域最主流的接口形式,被数据科学家、平台工程师、用户熟悉。在 GPU 训练中,读写存储系统工作通常是由操作系统内核来完成的。具体落实的时候是内核使用 CPU 先将数据拷贝到内存里,GPU 使用数据的时候再拷贝到显存中。硬件厂商如 NVIDIA 在探索的一个优化是让 GPU 直接去读取存储系统,从而减少 CPU 和 GPU 间的这一次拷贝开销,同时降低 CPU 使用率。这个技术叫 GPU Direct Storage(GDS),如果能够支持它的话,能够让 GPU 训练在数据读写速度方面有更好的表现。不过目前这个技术在业界使用得并不广泛,在未来它的发展前景怎么样,我们还有待观察。AI HPC 对存储接口的需求是 POSIX、K8s CSI,可选支持 GDS。

接口层面之外,不同的 AI 训练在负载方面有不同的特点,但是很多的 AI 训练会有海量小文件的需求。例如,在一些图片相关的训练中,每一个图片都很小,但是整个图片的样本集,假如展开来看的话会有非常多的小文件,一个样本集文件数多达几百万上千万都是有可能的。在整个的训练过程中,实际上对于那些样本集里的图片数据是不会做任何修改的,所以它对存储系统的 I/O 需求实际上是以读为主的。跟 HPC一样,为了读取的高效率,需要满足高吞吐和低延时的要求。此外,部分训练涉及到海量小文件的问题,海量小文件的特点就是元数据请求占比较高,存储系统的元数据性能至关重要。

需求汇总

  1. 首先在性能方面,我们会发现,这些计算对存储的需求,绝大部分情况下是发生在一批计算前面数据加载的阶段,和最后的数据保存阶段,这两个阶段如果存储性能跟不上的话,会导致 GPU、CPU 在那里等待,无事可做,空转浪费算力。GPU、CPU 在成本上来说是远高于存储成本的,存储一定要减少它们的等待时间。这些场景共性的要求是高吞吐,对一些 HPC 或者 AI HPC 的场景,还有额外的低延时要求。
  2. 第二点是文件大小的方面。这里面 AI HPC 场景比较特殊,它有海量小文件的需求,海量小文件实际上对整个存储系统的扩展性以及元数据性能方面的挑战是比较大的
  3. 第三点是接口方面。到目前为止,POSIX 文件接口还是最主流的。HCFS 是 POSIX的一个子集,满足了 POSIX 要求后很容易开发 HCFS SDK。HPC 比较特殊,除了完整的 POSIX 兼容性之外,还需要去适配 MPI-I/O 框架。
  4. 第四点是对于所有存储系统的一个通用需求。对于非常重要的数据,我们有数据持久化要求,确保数据不会丢失。对于一些特殊的计算场景,这个要求可以放松,在一些多轮计算中,部分结果是中间生成的临时结果,这些临时结果可以通过计算重新生成。对于这些临时的结果,可以选择使用一些临时存储空间来存放,以换取更高的运算速度,更低的成本。这种临时存储空间需求在 HPC 和 AI HPC 中比较普遍,存储系统可以使用单副本来满足。

存算分离/对象存储有哪些问题

面向大数据存算分离场景的数据湖加速方案存算分离相对于存算一体有哪些优势呢?

  1. 第一个优势就是弹性。从字面意思来理解,计算和存储分离开之后,计算资源和存储资源可以分别扩容。在集群最开始规划的时候,可能很容易设计出来一种机型,能够使计算资源和存储资源是匹配的,但是随着业务模式的变化,一定会出现计算资源和存储资源不匹配的情况。如果存算一体的话,扩容一定是计算和存储一起扩容,一定会出现某种资源的浪费。但是存算分离之后,计算资源有缺就扩计算资源,存储资源缺就扩存储资源,可以实现很好的弹性。
  2. 第二个优势是经济。
    1. 把数据存到对象存储中可以很容易的实现数据的冷热分层,对象存储 BOS 有 6 级存储体系,最冷的一层归档存储,它的目录价是标准存储的1/8,也就是说长期不使用的数据下沉到最冷的归档存储当中去,这个时候它的成本直接会减少 87.5%,这是一个很大的节省力度。如果是存算一体,那么数据仍然要存储在 3 副本的 HDFS 中。
    2. 另外一个层面是计算资源可以动态的扩缩容,按使用付费。因为使存算分离之后,数据等有状态的服务都是采用对象存储或者 RDS 等一些有状态的服务存储,这时候的计算资源完全是无状态的,可以有任务的时候把它启动起来,没有任务的时候把计算资源关闭,不需要长期的维持一个常驻的计算集群。如果是存算一体的话,因为有 HDFS 的存在,那必须要维持一个跑 HDFS 存储集群的机器,没有办法实现资源的动态缩容。
  3. 第三个优势是运维成本低。如果数据是基于对象存储,那么运维压力已经转嫁给了云服务的厂商,云服务厂商自己有专业的运维团队去运维。HDFS 的运维压力在超大数据规模的情况下是非常大的,有一些他难以弥合的一些缺陷,比如说 Namenode 的瓶颈,HDFS Namenode 的架构是基于全内存的,单 Namenode 最大也就是十亿左右的规模,也就是说如果整个的计算规模在百亿甚至千亿的时候,那么就是需要 10 个甚至 100 个以上的 HDFS 集群,这样的集群对于运维压力是非常大的。另外从数据面上看,如果数据达到百 PB 以上,数据的扩容和均衡也是一个非常大的挑战。

存算分离有它的挑战。元数据、数据面有一些挑战。

  1. 首先就是对象存储的平坦 Namespace 的问题。因为对象存储为了极高的扩展性,要支持万亿的文件数的规模,一般本来说它是一个平坦的 Namenode,就是说一个文件的元信息存储,在有序 table 里面存储了从根目录到最终文件的完整的 key,key 和 key 之间并没有联系,比如/a/b/c目录下有五个文件,对象存储的元数据存了五条元数据信息,这五条信息之间没有任何关系。这个时候如果要去实现 HDFS 的目录的 rename 接口,比如把 a/b/c rename 成 a/b/c.1,这时候要转换成对象存储的操作是这样的:首先要把 a/b/c 下面的文件都 list 出来,拿到这个列表之后,再去逐条的做 rename,把这些文件一条一条的 mv 到目标文件夹去。
    1. 这个时候它的耗时跟原目录下的文件数成正相关的,如果这个单目录有上万的文件,那么耗时就在数分钟以上。这个时候对于大数据任务的耗时是不可接受的。
    2. 还有另外一个问题,因为有多次的单个文件操作,中间可能会因为网络或者某个存储节点的原因导致一部分文件操作失败。如果中间出现失败,那就会有一部分的文件在原目录,有一部分的文件在目的目录,这时候会导致任务执行失败。
  2. 数据面第一个问题是I/O 路径长。前面提到的对象存储架构,数据流从计算节点访问至少要经过四层的负载均衡设备 Load Balance、Webservice、再去访问元数据,拿到 blob 数据的实际的存储位置,然后再去实际的存储节点拿到数据,至少要经过四个步骤。而 HDFS Client 直接通过访问 Namenode,知道这些数据在哪些 Datanode 上面去,然后再去访问 Datanode 上面的数据,它其实只有两跳。对象存储要比 HDFS 访问的路径要长一倍。这个注定单流的性能是要比 HDFS 差的。
  3. 数据面第二个问题是带宽消耗大。因为计算节点是一个集群,而存储节点是另一个集群,两个集群通过网络设备光纤连接起来。现在一个大的离线作业,都会在 TB 级别的读写,如果有几十个这样的业务,那带宽会到 10TB 以上。这么大的东西向的流量,对中间的网络交换机和光纤都有一个非常大的成本压力,如果出现陡增,对其他业务会产生影响。
  4. 计算节点无法就近访问数据。因为中间经过了一层 Proxy 的代理,计算节点是没有办法真正地感知到数据分布的。而 HDFS 是可以做到这一点。对象存储无法做到直接感知数据位置,那么它需要无差别的去访问所有数据,这样的特性进一步的增加了 I/O 消耗。

面对前面的这些挑战,百度智能云的数据湖加速方案是如何做的呢?也分为了两个部分:

  1. 第一个部分是针对 BOS 平坦 Namespace 带来的性能和原子性问题,BOS 开发了原生的层级 Namespace,也就是把原来平坦的目录数转化成一个层级的目录数,这样就能够以更高效的方式供大数据应用使用。
  2. 第二个优化是我们在进计算节点增加了元数据和数据的缓存产品 RapidFS。

百度内部的高性能存储实践

百度内部实践经验中,最核心的一点就是有一个统一的存储底座来做数据的流转中心。大家可以想一下,整个高性能计算的过程实际上是分为很多个环节的。比如说自动驾驶,要从很多的全国的道路采集路况信息,数据收集完了需要做一些预处理,例如给行人、机动车、交通标示牌做标注之类的。做完标注之后,才是真正的训练过程,训练完了之后会产生一些需要部署到生产系统上的模型,所以还要去做模型的管理、模型的部署。假如这些数据分散在不同的存储系统上的话,对使用效率和使用方便程度上来讲,是一个比较大的挑战。所以百度内部使用了一个统一的存储底座来简化数据的管理和流转。在这个统一存储底座的基础之上,会去支持一些高性能计算常用的接口需求,包括POSIX 文件接口以及 HCFS 大数据接口。一些对性能有更高要求的一些业务,愿意去做一些定制开发的话,它可以直接用存储底座提供的 SDK。

到了关键的训练环节,也就是计算环节,百度内部采用了不同的运行时解决方案来满足业务多样化的诉求,主要分为两类:

  1. 第一类解决方案解决 AI 训练中存在的海量小文件问题。对于存储底座来说,它首先要保证的是高吞吐和低成本,会采用一些相对比较廉价的存储介质,虽然单个设备的能力较差,但规模上来之后就有了规模效应,可以提供很大的吞吐。但这也导致它在处理小文件的时候,在元数据方面以及 I/O 方面的性能是不太理想的,这个时候就需要有一些速度更快的解决方案作为弥补。在百度内部,根据数据集的大小,业务可以有两种选择,包括本地盘和并行文件系统。
    1. 如果数据集比较小,计算节点的本地盘足够放下整个数据集,那训练任务完全就可以把数据先存到本地盘上,然后基于本地盘来做计算。
    2. 在另外一些场景下面,本地盘的大小远远不够存放下整个数据集,那这个时候就需要一个规模更大的共享文件系统来做这个事情,这是并行文件系统要解决的问题。这个并行文件系统会有自己独立的集群,多租户共享,支持 RDMA 网络、NVMe SSD,软硬件都做了很多的优化,来保证在海量小文件场景下,能够有比较好的元数据性能和 I/O 性能。
  2. 第二类解决方案针对那些训练时长非常长、重复较少的一些训练,这类训练主要的要求是能够把数据源源不断地从存储底座读取出来,吞吐比延时更重要。这个时候很多业务的选择就是去直接访问存储底座,利用存储底座比较好的高吞吐能力,来服务计算。

在整个使用过程中,还面临一些关键的使用问题。例如,数据怎么在不同系统(存储底座和并行文件系统、存储底座和本地盘)之间做流转;在使用的过程中,怎么简化不同类型存储的挂载、卸载和初始化、容量分配等工作。

解决方案

在百度内部实践的基础上,孵化出了百度沧海存储在高性能计算领域的整体解决方案。由大容量、高吞吐、低成本的存储底座,和更快的运行时存储 PFS、RapidFS 组成。通过并行文件系统和分布式缓存技术,与对象存储数据双向流动,实现数据集的管理和读写加速。

  1. 对象存储作为存储底座:对象存储已经是业界共识的云上数据湖的事实标准,满足存储底座的所有条件,百度智能云的对象存储 BOS 还具备分级存储智能生命周期能力。这两个能力可以让数据根据访问频次等条件,自由地在不同成本的层级间流转,达到整体成本最优的状态。举个例子,现在经常要使用的一些数据可以放到标准存储里面,这样的话它在访问的时候速度是比较快的,随着这些数据逐渐转冷,可能很长时间都不会再用到,那就可以把这些数据通过生命周期策略,自动地往更低频、更廉价的存储分级去沉降,最后可以一直沉降到磁带介质的归档存储上,以此来达到一个访问性能、成本之间比较好的均衡。

运行时存储 PFS、RapidFS 的最大特点就是这两个产品是近计算部署的,主要目的是为了让它们能够达到最好的 I/O 性能。

  1. PFS 是一个典型的并行文件系统,和业界的 Lustre、GPFS 这些系统在架构上是比较接近的。系统主要的一个特点就是整个 I/O 路径会非常的短,请求从内核态客户端出来之后,根据它的类型的,是元数据的请求还是 I/O 的请求,直接发给对应的元数据节点 MDS 或者数据节点 OSS 处理。这样,对于读 I/O 来说,I/O 路径只有一跳。这个是软件架构上的性能保证,在硬件上我们同样有一些保证。PFS 采用托管架构将系统部署到用户 VPC 的虚机或物理机上,让它在整个物理网络上和计算节点离得非常近,这里的物理网络可能是 RDMA 或高速 TCP。PFS 通过这些软硬件上的多重保证,来确保整个系统的性能是比较高的。
  2. RapidFS的定位是一个缓存加速系统,原理是将用户计算节点上的冗余资源,组织成一个小的 P2P 缓存来加速计算。RapidFS 加速的能力主要来自于两个方面:
    1. 第一个加速效果来自层级命名空间(namespace)。命名空间在存储系统里负责组织文件和目录之间的关系以及保存属性信息,文件系统的元数据也是指这一部分。层级命名空间是 POSIX 使用的命名空间形式,就像一棵倒挂着生长的树,从根目录开始,每一个文件或目录都属于一个父目录。平坦命名空间是对象存储使用的命名空间,文件和目录彼此是平等独立的,不存在父子关系,这个设计可以让命名空间的扩展性能更好。对于用户来说,想要通过 POSIX 的方式(这是很常见的用法)去访问对象存储,会有很大的元数据操作的放大。为了解决这个问题,RapidFS 内置了一个高效的层级命名空间,来做 BOS 命名空间的缓存。
    2. 第二个加速效果来自数据缓存。针对于 BOS 上数据访问比较慢的问题,RapidFS 将比较热的数据缓存到用户提供的冗余内存和磁盘上面,这样等用户去访问的时候,访问路径很短。

有了这两类运行时存储之后,需要解决怎么在这两个系统和存储底座之间做数据流转的问题。实际上我们是通过两种机制来满足的:

  1. 第一种机制是生命周期,在一些场景如 HPC 中,业务的整个访问入口主要是文件系统,也就是 PFS。很多数据在 PFS 里产生之后,逐渐转冷,PFS 可以通过生命周期的功能,把近期内不再使用的数据,自动地转移到对象存储里面,让用户能够把成本降下来。用户去访问的时候,PFS 又把数据自动地给加载回来。这样用户自己其实是不需要去关心数据到底是在对象存储还是在 PFS 里面,他只需要关心哪些目录需要开启生命周期功能。在我们的规划里,RapidFS 后续将推出的 Block 模式也具备类似的能力,访问入口在 RapidFS,热数据缓存在计算节点本地,数据的持久化和冷数据由对象存储负责。PS:数据访问入口在 PFS或RapidFS
  2. 另外一个机制是 Bucket Link。Bucket Link 的数据流走向跟生命周期正好是反向的。很多情况下,用户的数据实际上已经在对象存储里面了,例如自动驾驶这样的业务,它的数据是线下采集的一些路测数据,这些数据通过对象存储服务提供的工具上会传到对象存储里,训练时候的数据源实际上就是对象存储。但如果想要用 PFS 或者 RapidFS 来支撑训练,就需要高效地把数据搬过来。Bucket Link 要解决的就是这个问题,它本质上是将数据搬运的能力内置到了存储系统里面,用户只要通过一个很简单的命令,或者说一个界面的操作,就能够把 PFS 的一个目录或者 RapidFS 的一个命名空间,和对象存储里面的一个路径做一个绑定,这个绑定就是 Bucket Link。绑定后,PFS 和 RapidFS 可以自动地帮用户完成数据的加载,以及数据的预热。这样等到用户训练真正开始运行的时候,PFS 和 RapidFS 里的数据已经准备好了,任务直接可以运行。
  3. 统一调度,业界有一个开源项目叫 Fluid,它可以将整个训练的过程分成了两个阶段,一个阶段是用来做数据加载,另外一个阶段才是用来做真正的训练。这两个阶段拆分之后,在不同的任务之间 pipeline 并发起来。举个简单的例子,整个系统可能只有四张 GPU 卡,A 训练跟 B 训练都需要去用这四张卡来做训练,那在 A 训练跑 GPU 任务的时候,完全可以让 B 训练提前做数据预加载的工作,将数据提前预热到 PFS 或者 RapidFS 里。等到 A 训练任务完成的时候,就直接可以让调度器把 B 训练跑起来了。整体上看到的效果就是 B 的数据加载阶段被隐藏掉了,加载过程跟计算过程分阶段 pipeline 化了。对于那些训练任务很多的用户,GPU 等待时间变少了,利用率得到了很大的提高。

存算分离的缓存系统Alluxio

如何用Alluxio来加速云上深度学习训练?在Alluxio之上可以对接不同的数据应用,包括Spark、Flink这种ETL工具,presto这种query工具,以及TensorFlow、PyTorch等深度学习框架。在Alluxio下面也可以连接不同数据源,比如阿里云、腾讯云、HDFS等。深度学习训练框架PyTorch、TensorFlow、Caffe,它们的原生数据源都是本地文件系统。企业数据量日益扩大,导致我们需要去使用分布式数据源。Alluxio可以把来自不同的远端存储系统,以及分布式文件系统的数据都挂载到Alluxio统一的命名空间之内。通过Alluxio POSIX API,把这些数据变成类似于本地文件的形式,提供给各种训练任务。

  1. 能够对数据进行读写缓存。大家可能很多数据是在云存储上,或者是在远端的HDFS和Ceph集群上,如果每一次的数据应用都要去远端不断重复地拿同样的数据,那么整个拿数据的流程是非常耗时的,而且可能会导致我们整体训练或数据处理效率不高。通过Alluxio,我们可以把一些热数据在靠近数据应用的集群进行缓存,从而提升重复数据获取的性能。
  2. 可以对元数据进行本地缓存。每一次获取元数据都要通过网络去获取是比较慢的。如果通过Alluxio,可以直接从本地集群获取元数据,延时会大大缩短。同时,模型训练的元数据需求是非常高压的,我们在与蚂蚁金服的实验中,可以看到成千上万QPS。如果全部压力都压到存储系统中,存储系统可能会不稳定,或进行一定的限流处理,导致一些读取错误。通过Alluxio可以很好地分担这部分元数据的压力。
  3. A ==> hdfs ==> B 可以变为 A ==> Alluxio ==> hdfs ==> Alluxio ==> B,实际数据还未写入hdfs 即可被读取,即使数据持久化的速度比较慢,也不影响B。
  4. 可以把 把Alluxio作为整个数据的抽象层。整个训练集群,不管它需要的数据源来自何方,来自一些存储系统,由大数据ETL处理好的数据,或者是C++、python处理好的数据,都可以通过Alluxio进行读缓存,供给给训练。所有数据预处理的中间数据,以及训练的中间数据,都可以通过Alluxio进行暂时的写缓存。对于数据预处理和训练的结果,我们也可以通过Alluxio持久化到不同的存储系统之中。不管大家有什么样的数据应用,都可以通过Alluxio来对不同的数据源中的数据进行读写操作。

B站基于Iceberg + Alluxio 助力湖仓一体项目落地实践 未读

其它

面向大数据存算分离场景的数据湖加速方案 未读

9年演进史:字节跳动 10EB 级大数据存储实战 未细读。从 HDFS 发展历程入手,介绍发展路径上的重大挑战及解决方案。