源码阅读导引:Ray、Kubernetes、KubeRay 与 Volcano
假设手里有一个 Python 程序,要处理一批数据。在自己的电脑上,装好依赖、执行 python main.py 就能开始跑。后来数据多了,希望用几台机器一起计算。这时会遇到两类问题:程序怎样利用多台机器,以及这些机器上的程序由谁部署和管理。
这套文章研究 KubeRay 和 Volcano,分别沿集群管理和批调度两条线阅读源码。理解它们之前,先要知道 Ray 在运行什么、Kubernetes 管理什么。下面从计算需求出发,逐步引入这些项目,再解释它们为什么可以组合使用。
这里选择 Kubernetes 上的 Ray 部署作为研究场景。Ray 也可以在单机或多台机器上直接运行,不要求先安装 Kubernetes;选用 Kubernetes 和 KubeRay 以后,也可以先使用默认调度器,按工作负载的需要再考虑 Volcano。文章的阅读顺序不代表部署时必须把这些组件全部装上。
本文假设你运行过程序、用过命令行,不要求 Kubernetes、Ray 或 Volcano 使用经验。先解释基础概念,再走一遍从声明到程序运行的过程,最后讨论资源竞争为什么会引出批调度。集群安装和具体运维留给各项目的使用文档。熟悉 Ray、Kubernetes、KubeRay,以及 Pod、Controller 和 CRD 的读者,可以从第一篇开始;第六篇会介绍 Volcano 的资源模型。
基础概念依据官方文档,引用集中在文末。KubeRay 源码沿用固定提交 6bf05eb17a3e,Ray 使用 3f785c0711b9,Volcano 使用 d8984501e4ad。下面的部署图用于解释职责,没有加入集群实测结果。
1. 一个 Python 程序,为什么会需要 Ray
先看计算本身。假设程序要处理许多文件,每个文件都可以独立完成计算,最后再汇总结果。单进程逐个处理时,后一个文件需要等前一个完成。如果希望同时利用多台机器,就得把这些工作交给不同进程执行,并把输入和结果传递过去。
可以自己写进程管理、网络通信和工作分配逻辑,也可以使用分布式计算框架。Ray 是这类框架之一:使用者通过它的编程接口表达可以分开的工作,Ray 运行时负责安排执行,以及计算对象的存储和传递。“运行时”指程序执行期间为这些操作提供支持的一组进程和机制。
Ray Core 提供两个后文经常出现的概念。task 表示一次可以在其他执行进程中异步运行的函数调用。例如,把“处理一个文件”写成 Ray 远程函数,就可以发起多个 task,再收集它们的结果。actor 则把一个带状态的对象放到执行进程中,通过方法调用使用它。例如,某个对象先加载一份模型,后续多次调用可以继续使用这份已加载的模型。
这里需要使用者或所用的上层库表达计算如何拆分。把普通 Python 脚本原样交给 Ray,并不会自动把其中的串行循环改成分布式计算。
这些工作需要一套 Ray 运行环境。Ray 集群由一个 head 节点和与它连接的 worker 节点组成:head 除了普通运行组件,还承载集群管理进程;worker 节点提供执行用户代码的进程,并参与计算调度和对象传递。head 也可以承担用户计算,具体取决于配置。运行入口脚本、发起 task 和 actor 的进程称为 driver;一次 Ray 作业包含由这个入口程序发起的计算。
Ray 可以先在一台机器上使用,也可以部署到多台机器。学习 task 和 actor 时,不需要先准备 Kubernetes。接下来引入 Kubernetes,是因为我们还希望管理这些运行环境的部署和变化。
2. 选择 Kubernetes 部署时,它管理什么
假设已经确定要运行一个 head 和两个 worker。还需要把对应进程启动起来:选择机器,准备 Python 和依赖,配置启动参数,让 worker 找到 head。如果同时维护许多套环境,还要处理资源不足、容器退出、配置更新,以及不同程序争用机器的问题。
如果选择用 Kubernetes 管理这部分工作,就需要先把运行环境组织成容器。Kubernetes 是管理容器化应用的开源系统:使用者把可用机器组成集群,再通过统一接口描述应用需要的运行环境,由集群中的管理组件安排部署并持续观察。它可以管理 Ray,也可以管理网站、数据库等其他应用。
先解释容器。程序除了源码,还依赖解释器、软件包和系统库。容器镜像可以把程序和所需的用户态环境放在一起,供不同机器按同一份内容启动。容器是根据镜像运行起来的实例;实际启动和管理容器的软件叫容器运行时,例如 containerd。数据文件可以另外挂载或下载,不必全部放进镜像。
Kubernetes 中的 Node 是加入集群的机器,可以是物理机,也可以是虚拟机。安排到 Node 上的基本运行单位叫 Pod。Pod 包含一个或多个容器,这些容器一起部署到同一个 Node,并共享网络等资源。最简单的情况,就是一个 Pod 里运行一个应用容器。
采用 KubeRay 部署时,Ray 的 head 和 worker 节点以 Pod 的形式运行。因此,Ray 节点与 Kubernetes Node 不要求一一对应:一台机器上可以放多个 Ray Pod,只要资源和调度规则允许。一个 worker Pod 内也可以有多个执行进程,“两个 worker Pod”不等于“只能同时运行两个 task”。
两层系统作决定的对象不同。Kubernetes 为 Pod 选择机器,并负责容器运行所需的管理工作;Ray 在已经运行起来的环境中安排 task 和 actor。Kubernetes 不会因为 Python 又调用了一次远程函数,就为这个函数创建一个 Pod。
Kubernetes、K8s 和 kube 是什么关系
K8s 就是 Kubernetes 的缩写:保留开头的 K 和末尾的 s,用数字 8 代替中间的八个字母。两种写法指同一个项目。
kube 常出现在 Kubernetes 相关工具的名字里,例如 kubectl、kubelet。它不是这里额外引入的一套运行系统。kubectl 是操作 Kubernetes 的命令行工具,可以提交声明、查询资源和查看状态;kubelet 则运行在集群节点上,后面会具体解释它怎样接手容器启动。
3. Kubernetes 已经能部署容器,KubeRay 补了什么
Kubernetes 提供的是通用部署机制。它能接收“运行这个镜像、需要这些资源”的要求,但内置逻辑并不知道一个 Ray 集群需要怎样组合 head 和 worker,也不知道提交一次 Ray 作业前应该等待哪些条件。
使用者可以自己写这些配置和管理脚本。例如,先创建 head,再创建 worker,检查集群是否准备好,然后提交程序;结束以后,再决定保留哪些资源、删除哪些资源。随着作业数量和异常情况增加,这些步骤需要持续维护。
KubeRay 将 Ray 的这部分管理逻辑接入 Kubernetes。它提供专门的资源类型,让使用者描述 Ray 集群、一次作业或一个持续服务,再由管理程序读取这些声明、创建资源并跟踪状态。它管理的对象和 Ray 调度的 task、actor 并不处于同一层。
| 希望交给 KubeRay 管理的事情 | 使用的资源类型 |
|---|---|
| 准备一套 Ray 运行环境,描述 head 和 worker 配置 | RayCluster |
| 执行一次作业,准备或选择集群,提交入口程序并处理收尾 | RayJob |
| 持续运行 Ray Serve 应用,管理集群、应用健康和访问入口 | RayService |
Ray Serve 是 Ray 上用于构建在线服务的库,可以用来提供模型推理等服务。持续服务与执行完就结束的作业有不同管理要求,所以 RayService 和 RayJob 是两种使用方式。本系列以 RayJob 为主线,第五篇再单独分析 RayService 的切换。下面先看没有接入 Volcano 的基础部署,资源竞争的问题放到第 7 节。
把三者放在一起,一次作业的管理与计算大致经过图 1 中的交接。
图中先由 KubeRay 根据 RayJob 准备资源,Kubernetes 把对应容器启动起来。环境就绪后,KubeRay 安排提交程序把入口命令交给 Ray,Ray 再执行用户计算。后续状态检查和收尾仍需要 KubeRay,具体交接将在第一篇展开。
4. Kubernetes 怎样把一份声明变成运行中的容器
“Kubernetes 启动容器”还省略了几个参与者。为了看清它们各自做什么,先暂时离开 Ray,用一个普通应用演示副本管理,再回到 KubeRay。
4.1 先声明希望运行什么
如果希望两个相同的应用副本持续运行,可以使用 Deployment。这是 Kubernetes 内置的一种资源类型,使用者在其中声明容器模板和副本数,相关控制器负责持续维护。这里把它命名为 intro-deployment,它与后面的 RayJob 案例无关。
在 Kubernetes API 中,Deployment 和 Pod 是资源类型,intro-deployment 是某个具体对象的名称。API 是供程序提交和查询这些对象的接口;YAML 则是表达对象内容的一种文本格式。Pod 模板描述要按什么配置创建副本,selector 则用标签条件识别这些副本。例如,给 Pod 标记 app: intro,再用相同条件选中它们。下面只摘出 Deployment 的外层结构,省略了必需的 selector 和 Pod 模板,不能直接作为部署文件使用:
1 | |
apiVersion 和 kind 说明这份内容使用哪种资源接口。metadata 保存名称等标识;namespace 是命名空间,为对象名称提供范围,例如 default/intro-deployment。它是逻辑上的资源分组,不表示另一套机器。
spec 表达使用者的期望,这里是两个副本。许多资源还有 status,记录系统最近观察到的情况。期望两个副本时,实际可能只有一个,也可能两个都还在启动。把期望保存下来,再由管理组件持续使实际情况接近期望,就是这里所说的声明式管理。
4.2 请求经过谁,容器又由谁启动
使用者通过 kubectl 提交声明时,请求先到 API Server。它是 Kubernetes API 的服务端,接受资源的创建、查询和更新;资源数据由 etcd 保存。etcd 保存的是这类集群信息,不会自动保存 Python 程序的计算结果。API 接受 Deployment 声明之后,还需要其他组件继续工作。
Controller,中文通常叫控制器,是持续观察资源并执行管理逻辑的程序。Deployment Controller 读取副本和模板要求,管理名为 ReplicaSet(图中简称 RS)的资源;ReplicaSet 记录一组 Pod 的副本要求,由相应控制器根据缺口创建 Pod 对象。创建了 Pod 对象,只是让集群知道有这些容器需要运行。
接着由 Scheduler,也就是调度器,为尚未分配节点的 Pod 选择 Node。它需要考虑资源需求和调度约束,再通过 API 确定 Pod 与 Node 的分配关系,这一步称为绑定;如果没有合适的节点,Pod 就需要继续等待。Scheduler 选定机器以后,容器仍要由那台机器上的组件启动。
这里的资源需求来自配置。例如,容器的 resources.requests 可以声明需要多少 CPU、内存,调度器用这些请求判断节点能否放下它;resources.limits 则约束容器使用相应资源的上限。请求值与程序此刻的实际用量可能不同。后文分析资源分配时,主要讨论这份请求账目。
每台运行 Pod 的节点上都有 kubelet。它持续观察分配给本机的 Pod,按照其中的配置调用容器运行时,并向 API Server 回报 Pod 状态。容器运行时负责实际运行容器。这样,“哪个 Pod 归本机运行”和“本机的容器实际怎样了”就能在节点与管理端之间对应起来。
Pod 的状态还需要分开看。Pending 表示启动尚未完成,可能在等节点,也可能已经有节点、正在拉取镜像;Running 表示已进入容器运行阶段,不能单独证明应用可以接收工作。Ready 是另一项就绪条件,结合容器就绪状态等因素判断;如果配置了 readiness probe,就绪探针会检查应用是否准备好。后面 KubeRay 对集群是否就绪的判断,还会再组合多个 Pod 的情况。
API Server、调度器和控制器等承担集群管理工作的组件合称控制面。图 2 把它们与运行应用的 Node 分开画,表示职责分工,并不要求每个框都占一台独立机器。
4.3 为什么关掉提交终端,管理还会继续
前面的步骤靠各组件观察资源变化接续,图 3 按发生顺序把它们展开。
提交终端只负责发出请求,后续工作交给集群里的进程。若一个受管理的 Pod 被删除,ReplicaSet Controller 观察到副本数量不足,会尝试创建替代对象;新 Pod 再经过调度和启动。控制器反复比较期望与实际情况,决定下一步操作,这个过程称为协调,英文是 reconcile。
只创建一个孤立的 Pod,就没有 Deployment 和 ReplicaSet 这一层副本维护逻辑。机器故障后能否补建,还取决于故障是否被识别、相应控制器能否工作,以及集群是否有资源。即使替代 Pod 已经启动,也不能由此推断原程序的内存和计算进度已经恢复。
程序还需要相互访问。Pod 会被替换,地址也可能改变,因此常用 Service 提供相对稳定的访问入口。Service 可以通过标签选择后端 Pod;标签就是对象上的键值标记,用来识别一组资源。后面出现的 head Service 就承担类似职责,让提交程序等组件能够找到 Ray head。实际网络转发依赖集群的网络实现,本文先保留到这个层次。
5. Kubernetes 怎样认识 RayCluster
回到 Ray。Deployment 能表达按照模板维护副本,Ray 集群却还有 head、worker 组、启动参数和连接关系。KubeRay 需要让 Kubernetes 保存这些额外信息,也需要有人按它们执行管理动作。
增加资源类型的机制之一叫 CRD,全称 CustomResourceDefinition,即自定义资源定义。它向 Kubernetes 声明一种资源叫什么、有哪些字段、如何校验。KubeRay 仓库里就有 RayCluster 的 CRD 文件,下面摘出名称定义,省略同级的其他字段:
1 | |
这段内容让 API 知道资源属于 ray.io 这个组,类型名是 RayCluster,并按 namespace 区分命名范围。安装完整 CRD 后,再创建一个名叫 intro-cluster 的 RayCluster,这个具体对象就是自定义资源,常简称 CR。CRD 定义类型,CR 是这种类型的实例。intro-cluster 这里只用于解释概念;第一篇的专用集群会由 RayJob 创建。
CRD 让 API 能保存集群声明,具体管理动作由 KubeRay Operator 执行。它读取 RayCluster,根据 head 和 worker 配置创建、维护 Pod 及 Service,并回写集群状态。只有资源定义、没有相应控制器时,API 不会自行推导出这些步骤。
把应用的管理逻辑写进控制器,通过自定义资源驱动它工作,就是这里使用的 Operator 模式。KubeRay Operator 通常也作为 Pod 部署在集群中,它通过 API 读写资源,不需要成为 API Server 进程的一部分。
后文会反复出现 Operator、Controller 和 Reconcile:Operator 是部署起来的管理程序,Controller 是其中负责某一类资源的控制逻辑,Reconcile 则是控制器处理一个待协调对象时调用的函数。KubeRay 的一个 Operator 进程可以容纳多个 Controller,三类核心资源并不意味着需要三个独立进程。
6. 回到 RayJob:资源准备好,程序才开始提交
现在可以把图 1 中“准备 Ray 运行环境”展开到 Pod 和进程。图 4 假设声明需要一个 head Pod 和两个 worker Pod,省略上层 RayJob 和提交程序,先看这套集群怎么运行起来。
KubeRay 读取 RayCluster 并创建所需资源;Scheduler 给 Pod 选择 Node;节点上的 kubelet 和容器运行时启动 Ray 容器。图中把一个 head Pod 和两个 worker Pod 放在两台机器上,实际位置由资源需求和调度规则决定。Operator 也可以运行在这些 Node 上,为突出职责,图中单独列出它。
容器里的 Ray 进程组成集群之后,还需要把入口程序交给它运行。本系列的 RayJob 案例会等待集群就绪,再创建一个 Kubernetes Job 来运行提交程序。Kubernetes Job 是管理运行至完成的 Pod 的内置资源;这里的 Pod 运行 Ray CLI,也就是 Ray 的命令行工具,通过 Ray Jobs API 提交入口命令。
因此,一条流程里会同时出现 RayJob、Kubernetes Job 和 Ray 作业。RayJob 描述这次作业的管理要求;提交用的 Kubernetes Job 管理提交程序的 Pod;Ray 接收入口命令后运行用户程序,安排它发起的 task 和 actor。资源创建成功、提交程序启动、用户计算完成,是三个不同的进展。
计算开始以后,Ray 节点之间的计算数据不经过 Operator。KubeRay 继续检查作业状态,并按声明决定怎样收尾。Pod 是否存在、容器是否就绪、程序是否成功,各有对应的观察依据,第一篇会把这些状态放在同一条提交过程中说明。
程序及依赖怎样到达容器,也需要提前确定。后续案例统一假设它们已经包含在运行镜像中。声明一个 RayJob 本身不会把客户端电脑上的任意目录上传到集群;本地打包、上传和持久保存,需要对应的提交或制品管理流程。
7. 多个作业争用资源时,为什么会考虑 Volcano
前面的步骤解决了怎样创建和维护 Ray 集群。若同时提交几套作业,机器资源只能满足其中一部分,接下来还要决定谁先运行,以及怎样在作业之间分配资源。
例如,两个作业都至少需要三个成员才能有效计算,机器却只能容纳四个等大的成员。如果各自启动两个,双方都可能占着资源等剩下的成员。默认的 Pod 调度路径不会自动从业务代码推导出这种组内依赖,需要另外表达“这一组至少满足什么条件,启动才有意义”。这只适用于有相应要求的程序;有些 Ray 程序能够利用现有 worker 逐步计算,不必等待全部成员到齐。
Volcano 是建立在 Kubernetes 上的批处理与调度系统。这里的批处理主要指一批提交后需要安排资源执行的计算工作负载。它提供成组调度、队列和资源共享策略:成组调度通常称为 Gang,检查一组成员是否满足最低运行要求;队列把采用同一资源策略的工作负载组织起来,用于决定它们怎样共享资源。
接入时,KubeRay 继续负责 Ray 集群和作业生命周期,把组的调度要求交给 Volcano。Volcano 为相关 Pod 选择节点,kubelet 仍在节点上启动容器,Ray 继续在运行环境中安排 task 和 actor。安装 Volcano 不会让计算结果自动持久化,也不会代替 KubeRay 管理作业提交和收尾。
是否接入 Volcano,要看业务有没有最低成员要求、队列间共享或优先级等调度需求,并评估现有调度方式是否足够。本系列通过它研究这些机制,第一至第五篇的基础案例仍使用原有部署,第六至第九篇才启用 Volcano。其他部署和调度方案也可以满足不同需求,这里不把它们逐一展开。
到这里,四个项目可以按各自处理的对象区分:
| 项目 | 在本系列场景中负责什么 | 采用条件 |
|---|---|---|
| Ray | 执行用户程序,安排 task 和 actor,管理计算对象 | 本系列研究的计算框架,可以独立部署 |
| Kubernetes | 保存部署声明,安排 Pod 到节点,管理容器运行 | 本系列选择的部署环境 |
| KubeRay | 根据 RayCluster、RayJob 等声明维护 Ray 资源和生命周期 | 选择用 Operator 管理 Kubernetes 中的 Ray |
| Volcano | 根据组需求和队列策略,为相应 Pod 分配资源与选择节点 | 按调度需求接入,本系列后四篇的研究对象 |
8. 从哪里进入后续源码阅读
第一至第五篇使用同一个主案例:在 default 命名空间提交 RayJob demo-job,由它新建专用 RayCluster,包含一个 head Pod 和 compute 组的两个 worker Pod,通过 Kubernetes Job 运行提交程序。第一篇会列出这些对象的名称与关系。先追资源由谁创建,再看等待和重试由谁安排,最后把故障放进这条流程里。
| 文章 | 带着什么问题读 | 读到哪里停下来核对 |
|---|---|---|
| 一:组件分工与协作 | 只创建一个 RayJob,其他资源是谁创建的? | 对照资源关系图,区分对象、Pod 和进程 |
| 二:事件与 Reconcile | Pod 状态变了,控制器怎样收到消息并继续处理? | 追到监听注册、事件队列和 Reconcile 的返回值 |
| 三:并发模型 | 100 个 RayCluster 如何共享协调协程? | 分清队列里的对象标识与正在运行的 Ray 计算 |
| 四:状态与恢复 | 资源创建成功、状态还没写回,Operator 就重启了怎么办? | 看哪些身份和状态已经保存,以及重试前查询什么 |
| 五:高可用 | Operator、head 或底层控制面故障,分别由谁恢复? | 检查选主、计算状态和服务切换各自的条件 |
第六至第九篇为主案例增加 Volcano,先认识调度系统,再追到两端接入。涉及 Gang、PodGroup 和资源份额时,正文会从用途和具体场景解释。
| 文章 | 带着什么问题读 | 读到哪里停下来核对 |
|---|---|---|
| 六:Volcano 架构 | 作业需求由哪些对象表达,由哪些进程管理? | 区分工作负载管理与 Pod 调度 |
| 七:调度流程与 Gang | 一组 Pod 只放得下一部分时,会提交哪些操作? | 跟踪试分配、撤销、绑定和失败后的观察 |
| 八:队列与资源竞争 | 资源不足时,哪些作业获得运行机会? | 分开分析份额、优先级、节点条件与资源回收 |
| 九:KubeRay 与 Volcano 接入 | RayJob 怎样表达组需求,完成后怎样调整? | 核对字段传播、submitter 依赖和清理分支 |
第一次打开 KubeRay 仓库,可以先看下面三个位置。文末参考资料给出了对应固定提交的链接,避免主分支变化导致代码对不上。
| 入口 | 在这里找什么 |
|---|---|
ray-operator/apis/ray/v1/ |
RayCluster、RayJob 等对象在 Go 中的字段定义,尤其是 spec 与 status |
ray-operator/main.go |
管理进程怎样创建 Manager、注册各个 Controller 并启动 |
raycluster_controller.go |
从一次 Reconcile 进入实际的资源维护逻辑 |
Manager 是 controller-runtime 提供的控制器运行框架对象。它把客户端、缓存、控制器启动等公共设施组织在一起,第二篇会沿启动过程解释它。读第一篇时,只需知道这些控制器共同运行在 Operator 进程里。
下一篇从提交 demo-job 开始,具体追踪 RayJob、RayCluster、Pod 和提交程序之间的交接:从 RayJob 看组件分工与协作。
参考资料
- KubeRay 源码基线(6bf05eb17a3e)
- Kubernetes 概述
- Kubernetes 镜像
- Kubernetes Pod
- Kubernetes 组件说明
- Kubernetes Namespace
- Kubernetes 对象说明
- Kubernetes Deployment
- Kubernetes Controller
- Kubernetes Service
- KubeRay:ray-operator/config/crd/bases/ray.io_rayclusters.yaml
- Kubernetes 自定义资源
- Kubernetes Operator 模式
- Ray:doc/source/ray-core/key-concepts.md
- Ray:doc/source/cluster/key-concepts.md
- KubeRay:ray-operator/apis/ray/v1
- KubeRay:ray-operator/main.go
- KubeRay:ray-operator/controllers/ray/raycluster_controller.go
- Ray:doc/source/serve/index.md
- Ray:集群部署方式
- Ray:KubeRay 与 Volcano 接入背景
- Volcano:调度流程与配置
- Kubernetes:资源请求与限制
- Kubernetes:Pod 生命周期与就绪条件
- Kubernetes:标签与选择器