一周玩转云原生技术之volcano【与云原生的故事】

举报
龙哥手记 发表于 2022/05/08 14:21:48 2022/05/08
【摘要】 Volcano 它是一款优秀的基于 K8s 批处理系统,能够希望上层的 HPC、中间层大数据的应用以及最下面一层 AI 在统一 K8s 上面运行,这样会更加高效些,目的呢是弥补K8s在深度学习、大数据计算场景下不足的地方而诞生。

本文的主要内容有:

  • Volcano能干啥
  • 基本的名词
  • 如何快速部署
  • Volcano产生的背景
  • 场景下如何细分
  • 功能是有哪些

一 Volcano能干啥

现在 Volcano 已支持很多流行的计算框架,并源于华为云AI容器,能方便AI、大数据、基因、渲染等诸多行业通用计算框架接入,具体就是Spark、TensorFlow、PyTorch、Flink、Argo、MindSpore和PaddlePaddle,不仅包括了作业调度,还包含作业生命周期管理、多集群调度、命令行、数据管理、作业视图及硬件加速等功能,它还支持不同架构的计算调度,譬如x86、Arm、鲲鹏等都是可以兼容。

image.png

二 基本的名词

Queue
队列,Cluster级别的一种资源对象,可声明资源配额,由多namespace共享,提供soft isolation

Job
一种批量计算作业,支持定义作业所属队列,生命周期策略、所包含的任务模板以及持久卷等信息

PodGroup
任务组,与queue绑定,占用队列的资源;与Volcano Job是1:1的关系,主要是供 schedule调度时使用的

Command
支持外部干预运行中的作业,作用对象是Volcano Job

三 如何快速部署

部署Volcano的最简单方法是用Helm这个工具,或可以通过克隆代码来部署Volcano

1、那我们用Volcano Helm Repo这个工具

使用以下命令添加helm repo

helm repo add volcano https://volcano-sh.github.io/charts

然后使用以下命令安装Volcano

helm install volcano/volcano --namespace <namespace> --name <specified-name>

譬如

helm install volcano/volcano --namespace volcano-trial --name volcano-trial

2、克隆代码

1]、先决条件

首先,把repo克隆到本地路径上面来

# mkdir -p $GOPATH/src/volcano.sh/

# cd $GOPATH/src/volcano.sh/

# git clone --recursive https://github.com/volcano-sh/volcano.git

2]、Volcano Image

DockerHub上提供了官方images,但可以使用以下命令在本地创建它们

cd $GOPATH/src/volcano.sh/volcano

make images

## Verify your images

# docker images

敲黑板:需要确保在kubernetes集群中正确加载images,例如,如果使用的是类型集群,请尝试为每个images 用如下命令kind load docker-image <image-name>:<tag>

3]、Helm charts

其次,是安装helm这个工具

helm install installer/helm/chart/volcano --namespace <namespace> --name <specified-name>

譬如:

helm install installer/helm/chart/volcano --namespace volcano-trial --name volcano-trial

若要验证安装,请运行下面命令

#1. Verify the Running Pods

# kubectl get pods --namespace <namespace>

#2. Verify the Services

# kubectl get services --namespace <namespace>

四 Volcano产生的背景

image.png
上图是我们做的一个分析,我们将其分为三层,最下面是资源管理层,中间为领域的框架,包括AI的体系、HPC、Batch, WKflow的管理以及像现在的一些微服务及流量治理等。再往上是行业以及一些行业的应用。

随着一些行业的应用变得复杂,它对所需求的解决方案的要求也越来越高。举个例子在10多年以前,在金融行业提供解决方案时,它的架构是非常简单的,可能需要一个数据库,一个ERP的中间件,就可以解决银行大部分的业务。

而现在,每天要收集大量的数据,需要spark去做数据分析,甚至需要一些数据湖的产品去建立数据仓库,然后去做分析,产生报表。同时它还会用 AI的一些系统,来简化业务流程等。

所以,现在的一些行业应用与10年前比,变得很复杂,它可能会应用到下面这些领域框架里面的一个或多个,其实对于行业应用,它的需求是在多个领域框架作为一个融合,领域框架的诉求是下面的资源管理层能够提供统一的资源管理。

K8s现在越来越多的承载了统一的资源管理的角色,它可以为HPC这些行业领域框架提供服务,也可以作为大数据领域的资源管理层。Volcano主要是基于Kubernetes做的一个批处理系统,希望上层的HPC、中间层大数据的应用以及最下面一层AI能够在统一Kubernetes上面运行的更高效。

六 场景下如何细分

而在调度方面,Volcano 又对场景进行了细分以及归类,并提供了相关的方案及算法;同时也为这些功能提供了调度框架,方便用户对调度器进行扩展。那么对于分布式计算或者说并行计算来说,根据场景和作业属性的不同,也可以对其进行细分;在 《并行计算导论》 中将并行计算简单分成三类:

  • 简单的并行

指的是多个子任务(tasks)之间没有通信也不需要同步,可以完全的并行的执行。比较典型的例子应该就属MapReduce了,它的两个阶段都属于这种类型:mapper任务在执行时并不会彼此通信同步运行状态;另一个常见的例子是蒙特·卡罗方法 ,各个子任务在计算随机数时也无需彼此通信、同步。由于这种并行计算有比较广泛的应用,例如 数据处理、VatR 等,针对不同的场景也产生了不同的调度框架,例如 Hadoop、DataSynapse 和 Symphony。敲黑板了哈,由于子任务之间无需信息和同步,当其中某几个计算节点(workers)被驱逐后,虽然作业的执行时间可能会变长,但整个作业仍可以顺利完成;而当计算节点增加时,作业的执行时间一般都会缩短。因此,这种作业也常常被称作 Elastic Job。

  • 复杂的并行
    指的是多个子任务之间需要同步信息来执行复杂的并行算法,单个子任务无法完成部分计算。最近比较有名的例子应该算是 Tensorflow 的 “ps-work模式” 和 ring all-reduce了,各个子任务之间需要大量的数据交换和信息同步,单独的子任务无法独立完成。正是由于作业的这种属性,对作业调度平台也提出了相应的调度要求,比如 gang-scheduling、作业拓扑等。又由于子任务之间需要彼此通信,所以作业在启动后无法动态扩展子任务的,在没有checkpoint的情况下,任一子任务失败或驱逐,整个作业都需要重启哦,这种作业也常常被称作 Batch Job,传统的HPC场景多属于这种类型的并行作业,针对这种场景的调度平台为 Slurm/PBS/SGE/HTCondor 等。

  • 流水线并行
    指的是作业的多个子任务之间存在依赖关系,但不需要前置任务完全结束后再开始后续的任务。比如 Hadoop 里有相应的研究,在 Map 没有完全结束的时候就部分开始 Reduce 阶段,这样从而提高任务的并行度,提高整体的运行性能。符合这种场景的应用相对来说比较少,一般都做为性能优化;因此没有针对这种场景的作业管理平台。需要区分一下工作流与流水线并行,工作流一般指作业之间的依赖关系,而流水线并行一般指作业内部多个任务之间的依赖。由于工作流中的作业差异比较大,很难提前开始后续步骤。

值得一提的是"二次调度"。由于简单并行的作业一般会有大量的子任务,而且每个子任务所需要的资源相对一致,子任务之间也没有通信和同步;使得资源的复用率相对比较高,所以二次调度在这种场景下能发挥比较大的作用;Hadoop的YARN,Symphony的EGO都属于这种类型。但是在面对复杂并行的作业时,二次调度就显得有也吃力;复杂并行作业一般并没有太多的子任务,子任务之间还经常需要同时启动,子任务之间的通信拓扑也可能不同 (e.g. ps/worker, mpi),而且作业与作业之间对资源的需求差异较大,因此导致了资源的复用率较低。

虽然针对两种不同并行作业类型有不同的作业、资源管理平台,但是根本的目标都是为作业寻找最优的资源;因此,Volcano一直以支持以多种类型的作业为目标进行设计。目前,Volcano可以同时支持 Spark、TensorFlow和MPI等多种类型的作业。

七 功能是啥

1 设计目标是啥

1)首先提供统一接口,支持多种类型批 量计算任务,可以描述复杂类型 的作业

Job API的定位是一个bach-system,支持多种类型的作业,好比说大数据、AI、HPC,包含了多种作业类型。所以我们希望提供一个统一的接口去简化用户的使用,无论哪种类型、哪个场景的作业,都可以通过统一的接口运行起来,这是我们设计的时候一个很重要的考虑点。

2)灵活扩展性,满足定制化需求

其次就是灵活性,虽然说不同类型的作业,通过一个作业能够跑起来,但其实不同的场景,有不同定制化化需求,所以我们在设计这个Job API的时候,能提供定制化的需求,可以支持大部分的场景。

2 Volcano多任务模板

TaskSpec结构体里除包含最基本的信息外,还有一个policies, policy就定义了Task的生命周期。因为一个Job里会有多个 Task,就像下图描述的如何用Vocano运行一个Tensorflow的Job,Task里面就有多个角色,不同的角色可以定义它自己的 port template,这样的话我们就可以通过一个统一的Volcano Job支持不同的作业类型。

3 Volcano作业插件机制

  1. 作业内任务互相访问的需求

  2. 作业内任务索引

  3. ssh免密登录

其实有一些定制化的需求。好比说分布式训练这种作业,它在多个pod跑起来之后,它的pod和pod之间需要有网络互访,做数据同步。 对于像这种类型的需求,我们通过插件化的机制做成job plugin,让用户去通过这种方式去支撑它的场景。

在这里面我们看到我们定义的接口叫PluginInterface,PluginInterface的定义当一个作业在增删改查,或者Job在创建的时候,用户可以去实现这个函数,去做定制化的需求。以下三个作业的插件是我们默认已经支持的,简单配置就可以使用:

svc:

不同类型的任务之间互访

env:

任务索引,例如Tensorflow Worker index

ssh:

ssh秘钥对创建及挂载,主要供MPI作业使用

接下来我们看一下作业插件是怎么用的。它使用起来其实非常简单,刚才说的那三个插件,只要在job spec里面的plugins字段,再加上插件的名字(如有需要还可以添加参数,本课示例未添加)就可以运行起来了。

下图是一个Volcano跑Tensorflow的Job。

除了刚才提到的统一job、定制化的需求,还有一个很重要的方面就是作业生命周期的管理。因为作业的状态以及pod状态是非常多的,上节课我们也提到大概有七八种pod的状态,这些pod在发生不同事件时,对于作业的状态都会有影响。由于不同的场景需求是不一样的,这就要求我们在支持生命周期管理时,提供丰富的用户可配置机制的生命周期管理。

除了作业生命周期管理之外,还有一个就是作业状态的设计。上面我们提到我们在Volcano里增加了很多Pod的状态。现在这里面提到的是作业的状态,作业的状态有8种,Controller 维护 State 子类的实例,State定义接口,封装与Controller 特定状态相关的行为,每个子类实现一个与Controller 特定状态相关的行为。

八 常见的调度场景

1.组调度 (Gang-scheduling)

运行批处理作业(如Tensorflow/MPI)时,必须协调作业的所有任务才能一起启动;否则,将不会启动任何任务。如果有足够的资源并行运行作业的所有任务,则该作业将正确执行; 但是,在大多数情况下,尤其是在prem环境中,情况并非如此。在最坏的情况下,由于死锁,所有作业都挂起。其中每个作业只成功启动了部分任务,并等待其余任务启动。

2.作业级的公平调度 (Job-based Fair-share)

当运行多个弹性作业(如流媒体)时,需要公平地为每个作业分配资源,以满足多个作业竞争附加资源时的SLA/QoS要求。在最坏的情况下,单个作业可能会启动大量的pod资源利用率低, 从而阻止其他作业由于资源不足而运行。为了避免分配过小(例如,为每个作业启动一个Pod),弹性作业可以利用协同调度来定义应该启动的Pod的最小可用数量。 超过指定的最小可用量的任何pod都将公平地与其他作业共享集群资源。

3.队列 (Queue)

队列还广泛用于共享弹性工作负载和批处理工作负载的资源。队列的主要目的是:

  • 在不同的“租户”或资源池之间共享资源
  • 为不同的“租户”或资源池支持不同的调度策略或算法
    这些功能可以通过层次队列进一步扩展,在层次队列中,项目被赋予额外的优先级,这将允许它们比队列中的其他项目“跳转”。在kube批处理中,队列被实现为集群范围的CRD。 这允许将在不同命名空间中创建的作业放置在共享队列中。队列资源根据其队列配置(kube batch#590)按比例划分。当前不支持分层队列,但正在进行开发。

集群应该能够在不减慢任何操作的情况下处理队列中的大量作业。其他的HPC系统可以处理成百上千个作业的队列,并随着时间的推移缓慢地处理它们。如何与库伯内特斯达成这样的行为是一个悬而未决的问题。支持跨越多个集群的队列可能也很有用,在这种情况下,这是一个关于数据应该放在哪里以及etcd是否适合存储队列中的所有作业或pod的问题。

4.面向用户的, 跨队列的公平调度 (Namespace-based fair-share Cross Queue)

在队列中,每个作业在调度循环期间有几乎相等的调度机会,这意味着拥有更多作业的用户有更大的机会安排他们的作业,这对其他用户不公平。 例如,有一个队列包含少量资源,有10个pod属于UserA,1000个pod属于UserB。在这种情况下,UserA的pod被绑定到节点的概率较小。

为了平衡同一队列中用户之间的资源使用,需要更细粒度的策略。然后考虑到Kubernetes中的多用户模型,使用名称空间来区分不同的用户, 每个命名空间都将配置一个权重,作为控制其资源使用优先级的手段。

5.基于时间的公平调度 (Fairness over time)

对于批处理工作负载,通常不要求在某个时间点公平地分配资源,而是要求在长期内公平地分配资源。例如,如果有用户提交大作业,则允许用户(或特定队列)在一定时间内使用整个集群的一半, 这是可以接受的,但在下一轮调度(可能是作业完成后数小时)中,应惩罚此用户(或队列)而不是其他用户(或队列)。在 HTCondor 中可以看到如何实现这种行为的好例子。

6.面向作业的优先级调度 (Job-based priority)

Pod优先级/抢占在1.14版本中被中断,它有助于确保高优先级的pod在低优先级的pod之前绑定。不过,在job/podgroup级别的优先级上仍有一些工作要做,例如高优先级job/podgroup应该尝试以较低优先级抢占整个job/podgroup,而不是从不同job/podgroup抢占几个pod。

7.抢占 (Preemption & Reclaim)

通过公平分享来支持借贷模型,一些作业/队列在空闲时会过度使用资源。但是,如果有任何进一步的资源请求,资源“所有者”将“收回”。 资源可以在队列或作业之间共享:回收用于队列之间的资源平衡,抢占用于作业之间的资源平衡。

8.预留与回填 (Reservation & Backfill)

当一个请求大量资源的“巨大”作业提交给kubernetes时,当有许多小作业在管道中时,该作业可能会饿死,并最终根据当前的调度策略/算法被杀死。为了避免饥饿, 应该有条件地为作业保留资源,例如超时。当资源被保留时,它们可能会处于空闲和未使用状态。为了提高资源利用率,调度程序将有条件地将“较小”作业回填到那些保留资源中。 保留和回填都是根据插件的反馈触发的:volcano调度器提供了几个回调接口,供开发人员或用户决定哪些作业应该被填充或保留。

九 Volcano 调度框架

Volcano调度器通过作业级的调度和多种插件机制来支持多种作业;Volcano的插件机制有效的支撑了针对不同场景算法的落地,从早期的gang-scheduling/co-scheduling,到后来各个级别的公平调度。下图展示了Volcano调度器的总体架构:


并且Cache 已经缓存了集群中Node和Pod信息,并根据PodGroup的信息重新构建 Job (PodGroup) 和 Task (Pod) 的关系。由于在分布式系统中很难保证信息的同步,因此调度器经常以某一时间点的集群快照进行调度;并保证每个调度周期的决定是一致的。在每个调度周期中,Volcano 通过以下几个步骤派发作业:

  • 在每个调度周期都会创建一个Session对象,用来存储当前调度周期的所需的数据,例如,Cache 的一个快照。当前的调度器中仅创建了一个Session,并由一个调度线程执行;后续将会根据需要创建多个Session,并为每个Session分配一个线程进行调度;并由Cache来解决调度冲突。
  • 在每个调度周期中,会按顺序执行 OpenSession, 配置的多个动作(action)和CloseSession。在 OpenSession中用户可以注册自定义的插件,例如gang、 drf,这些插件为action提供了相应算法;多个action根据配置顺序执行,调用注册的插件进行调度;最后,CloseSession负责清理中间数据。
    (1) action是第一级插件,定义了调度周期内需要的各个动作;默认提供 enqueue、allocate、 preempt和backfill四个action。以allocate为例,它定义了调度中资源分配过程:根据 plugin 的 JobOrderFn 对作业进行排序,根据NodeOrderFn对节点进行排序,检测节点上的资源是否满足,满足作业的分配要求(JobReady)后提交分配决定。由于action也是基于插件机制,因此用户可以重新定义自己的分配动作,例如 基于图的调度算法firmament。

(2) plugin是第二级插件,定义了action需要的各个算法;以drf插件为例,为了根据dominant resource进行作业排序,drf插件实现了 JobOrderFn函数。JobOrderFn函数根据 drf 计算每个作业的share值,share值较低代表当前作业分配的资源较少,因此会为其优先分配资源;drf插件还实现了EventHandler回调函数,当作业被分配或抢占资源后,调度器会通知drf插件来更新share值。

  • Cache 不仅提供了集群的快照,同时还提供了调度器与kube-apiserver的交互接口,调度器与kube-apiserver之间的通信也都通过Cache来完成,例如 Bind。
  • 同时,为了支持上面这些场景,Volcano的调度器还增加了多个Pod状态以提高调度的性能:
  • Pending: 当Pod被创建后就处于Pending状态,等待调度器对其进行调度;调度的主要目的也是为这些Pending的Pod寻找最优的资源
  • Allocated: 当Pod被分配空闲资源,但是还没有向kube-apiserver发送调度决策时,Pod处于Allocated状态。 Allocated状态仅存在于调度周期内部,用于记录Pod和资源分配情况。当作业满足启动条件时 (e.g. 满足minMember),会向kube-apiserver提交调度决策。如果本轮调度周期内无法提交调度决策,由状态会回滚为Pending状态。
  • Pipelined: 该状态与Allocated状态相似,区别在于处于该状态的Pod分配到的资源为正在被释放的资源 (Releasing)。该状态主要用于等待被抢占的资源释放。该状态是调度周期中的状态,不会更新到kube-apiserver以减少通信,节省kube-apiserver的qps。
  • Binding: 当作业满足启动条件时,调度器会向kube-apiserver提交调度决策,在kube-apiserver返回最终状态之前,Pod一直处于Binding状态。该状态也保存在调度器的Cache之中,因此跨调度周期有效。
  • Bound: 当作业的调度决策在kube-apiserver确认后,该Pod即为Bound状态。
  • Releasing: Pod等待被删除时即为Releasing状态。
    Running, Failed, Succeeded, Unknown: 与Pod的现有含义一致。
    状态之间根据不同的操作进行转换,如图这样

  • Pod的这些状态为调度器提供了更多优化的可能。比如说,当进行Pod驱逐时,驱逐在Binding和Bound状态的Pod要比较驱逐Running状态的Pod的代价要小 (思考:还有其它状态的Pod可以驱逐吗?);

  • 并且状态都是记录在Volcano调度内部,减少了与kube-apiserver的通信。但目前Volcano调度器仅使用了状态的部分功能,比如现在的preemption/reclaim仅会驱逐Running状态下的Pod;这主要是因为分布式系统中很难做到完全的状态同步,在驱逐Binding和Bound状态的Pod会有很多的状态竞争;

【与云原生的故事】有奖征文火热进行 快来参加:https://bbs.huaweicloud.com/blogs/345260

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。