源码阅读导引: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 相关工具的名字里,例如 kubectlkubelet。它不是这里额外引入的一套运行系统。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 中的交接。

图 1:从一次 RayJob 提交看三者分工;部署准备与计算提交是不同步骤
图 1:从一次 RayJob 提交看三者分工;部署准备与计算提交是不同步骤 查看原图

图中先由 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
2
3
4
5
6
7
8
apiVersion: apps/v1
kind: Deployment
metadata:
name: intro-deployment
namespace: default
spec:
replicas: 2
# 此处省略 selector 和 template

apiVersionkind 说明这份内容使用哪种资源接口。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 分开画,表示职责分工,并不要求每个框都占一台独立机器。

图 2:Kubernetes 集群中的管理组件与节点组件;实线表示请求,虚线表示持续观察与状态回报
图 2:Kubernetes 集群中的管理组件与节点组件;实线表示请求,虚线表示持续观察与状态回报 查看原图

4.3 为什么关掉提交终端,管理还会继续

前面的步骤靠各组件观察资源变化接续,图 3 按发生顺序把它们展开。

图 3:从 Deployment 声明到容器运行;各组件通过 API 中的资源变化接续工作
图 3:从 Deployment 声明到容器运行;各组件通过 API 中的资源变化接续工作 查看原图

提交终端只负责发出请求,后续工作交给集群里的进程。若一个受管理的 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
2
3
4
5
6
7
8
spec:
group: ray.io
names:
kind: RayCluster
listKind: RayClusterList
plural: rayclusters
singular: raycluster
scope: Namespaced

这段内容让 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 和提交程序,先看这套集群怎么运行起来。

图 4:KubeRay 管理资源,Kubernetes 启动容器,Ray 在容器内安排计算;下半部分是示例部署位置
图 4:KubeRay 管理资源,Kubernetes 启动容器,Ray 在容器内安排计算;下半部分是示例部署位置 查看原图

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 管理作业提交和收尾。

图 5:Ray 可以直接部署,也可以选择 Kubernetes 场景。图中只列出默认调度器与 Volcano 两种选择;箭头概括职责交接,省略绑定结果经 API 传递等步骤。
图 5:Ray 可以直接部署,也可以选择 Kubernetes 场景。图中只列出默认调度器与 Volcano 两种选择;箭头概括职责交接,省略绑定结果经 API 传递等步骤。 查看原图

是否接入 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 看组件分工与协作

参考资料


源码阅读导引:Ray、Kubernetes、KubeRay 与 Volcano
https://tanxinyu.work/kuberay-volcano-00-reading-guide/
作者
谭新宇
发布于
2026年9月15日
许可协议