一. 演进背景
字节跳动(以下简称字节)内部离线业务具有庞大的规模,线上每天有数十万节点运行,每天的任务数达到百万量级,每天使用的资源量达到千万核量级。在如此庞大的计算规模下,为了能够高效地处理任务,提高资源流转效率,调度系统发挥了非常重要的作用。
如上图所示,我们可以清楚地看到,字节内部调度架构分为两大块 —— 离线调度系统和在线调度系统,离线调度系统主要负责离线资源管理和离线任务调度,在线调度系统主要负责在线资源管理和在线任务调度。
- 离线调度系统基于 YARN 实现,主要包括 Resource Manager(RM) 和 Node Manager(NM) 两个组件,负责资源调度和容器运行时管理。字节内部在 YARN 的基础上进行了很多功能丰富和优化工作,针对不同场景实现了不同的调度器,例如:Batch Scheduler,Gang Scheduler 等。
- 在线调度系统基于 Kubernetes 生态,进行了很多优化,支持字节内部多样化的在线服务。
为了提高字节内部整体的资源利用率,我们也进行了混部技术的探索 。 主要思路是在在线的节点上同时部署 Kubelet 和 NM 服务,当在线节点比较空闲时可以及时将空闲资源出让给离线业务使用,以此使得整个数据中心的资源利用率能够得到比较大的提升。
但随着公司内业务规模的持续发展 ,这一套系统也暴露出了一些短板 :
- 首先,在离线属于两套系统,一些重大活动场景需要通过运维方式进行在离线资源转换,运维负担繁重,转换周期长;
- 其次,现在的混部架构只是在部分节点上同时部署了 NM 和 Kubelet 两个 Agent,资源利用率仍有很大的提高空间;
- 最后,在离线是两套割裂的系统,Quota 平台、机器运维等都不能复用,大数据作业无法享受到云原生的各种好处,例如:资源池化、更好的单机隔离特性等。
综上所述, 字节内部有三个核心诉求:
- 重大活动场景(春节 / 双 11 等),在离线资源需要能够高效、灵活地相互转换;
- 整个数据中心的利用率需要得到更全面、充分的提升,进一步降本增效;
- 在离线资源共池,Quota 管控、调度、运行、机器运维统一。
为了实现上述诉求,我们进行了一些思考和探索。其中一种解决方案是:能不能让离线作业直接迁移到 Kubernetes? 即:大数据生态下的各个计算引擎(包括:Spark、Flink 等)进行深度改造去适配 Kubernetes。在探索过程中发现这种方式有比较大的缺陷,主要有以下三点:
- 对计算引擎侵入较深,计算引擎侧需要做大量改造才能支持原先在 YARN 的各种特性;
- 生产环境的作业(百万级)非常多,如何从 YARN 平滑迁移到 Kubernetes 也是个比较大的问题;
- 特别地,部分比较古老的计算引擎,比如 MapReduce,目前处于 Maintain 状态,已经无法进行大的改造来迁移。
基于以上思考,我们提出了一种全新的解决方案——Yodel。Yodel 的全称是 YARN on Gödel(Gödel 是公司内部增强版 Kubernetes,它对 API Server、Gödel 调度器以及底层运行时都进行了增强),是字节跳动提出的 Hadoop YARN 云原生化演进实践方案。通过 Yodel 我们将公司内的大数据业务(Spark、Flink 等)、训练业务(Primus)平滑迁移到了 Kubernetes ,实现了在离线资源池统一,提升了整体资源利用率。
二. 解决方案
下面将从 Yodel 整体架构、Remote Godel Scheduler(RGS) 服务、Remote Kubelete Service(RKS) 服务、持久化服务、平滑迁移及重要优化六个方面来详细介绍我们的解决方案。
2.1 Yodel 整体架构
Yodel 基于 YARN 实现,新增 ZK / ETCD / KV State Store、Remote Godel Scheduler 、Remote Kubelet Service 服务。ZK / ETCD / KV State Store 主要用于持久化存储、Remote Godel Scheduler 维护资源请求并与 API Server 交互,将调度能力统一到 Godel Scheduler;Remote Kubelet Service 实现了 YARN NM 所有接口,对用户和作业透明的前提下,把 NM 的 Container 管理能力平滑下沉到 Kubelet。
Yodel 整体架构图
从上面的架构图可以清楚看到 Yodel 的整体架构, 图中蓝色组件是进行了适配改造的组件,蓝色中标红的组件是新增组件,黄色组件是 Gödel 生态下的组件,关于新增组件:
- ZK / ETCD / KV State Store:支持将集群元数据信息持久化到 ZK、 ETCD 和 KV 等持久化存储,可以通过 API Server 方便地进行相关数据查询和更新;
- Remote Godel Scheduler:维护集群所有任务的资源请求,通过该服务将任务的资源请求转化为 Pod 写入 API Server,同时与 API Server 交互获取已调度的 Pod,最终将调度能力下沉到底层的 Godel Scheduler;
- Remote Kubelet Service:实现了原来 YARN 中 NM 的所有接口,例如:启动容器、停止容器、获取容器状态的接口。通过这个服务容器启动从 NM 切换到 Kubelet,最终将容器运行时的管理下沉到底层的 Kubelet。
下面介绍在 Yodel 架构下一个离线任务的提交和运行流程 :
- 用户从开发机或任务托管平台向集群提交一个任务;
- 当任务经过校验后,Yodel RM 会新建一个 App 对象并持久化至 API Server;
- Yodel RM 创建 AM Pod 并写入 API Server,等待底层调度器调度;
- Yodel RM 收到已经调度完成的 AM Pod 并进行相关转化操作;
- Yodel RM 将相关启动信息丰富至 AM Pod 中并 Patch 至 API Server 由 Kubelet 拉起相关进程;
- AM 启动成功后,随心跳主动向 Yodel RM 申请资源;
- Yodel RM 收到任务的资源请求后,通过 RGS 服务将资源请求转化为 Pod 对象或 PodGroup 对象并写入到 API Server;
- 底层调度器 Watch 到相关对象后,按照一定策略进行调度,同时 Yodel RM 也会及时地 Watch 到已经调度的 Pod;
- Yodel RM 会将已经调度的 Pod 转化为 Container,随心跳返回给对应的 AM;
- AM 收到已经调度的 Container 后,会再跟 Yodel RM 进行交互,来启动对应的容器;
- Yodel RM 收到容器启动请求后,通过 RKS 服务将容器启动所需要的信息丰富到 Pod 对象里并 Patch 到 API Server。Kubelet Watch 到待启动的 Pod 后,会进行这个 Pod 的启动。
Yodel 架构 Pod 生命周期
上面讲了 Yodel 架构下任务的启动流程,下面我们来看一下对于一个 Pod 来说,它的生命周期是怎么样的 ,核心流程如上图所示:
- 首先,AM 启动起来后会随心跳申请资源;
- Yodel RM 收到资源请求后,会基于该资源请求的资源量、优先级等创建一个 Pod 对象写入 API Server。创建完成后,该 Pod 对象处于 Pending-Unscheduled 状态,等待底层调度器进行调度;
- 底层调度器 Watch 到新创建的 Pod 后,根据一定策略进行调度,调度完成后会将调度结果写入 API Server。写入完成后,该 Pod 对象的状态会变为 Pending-Scheduled 状态;
- Yodel RM Watch 到已经调度完成的 Pod 后会转化为 Container,该 Pod 对象的状态会变为 Allocated 状态;
- 新分配的 Container 会随心跳返回给 AM,Container 被对应 AM 拿走后,该 Pod 对象的状态会变为 Acquired 状态;
- AM 获取到容器后会与 Yodel RM 交互进行启动操作;
- Yodel RM 收到容器拉起请求后,会把容器启动所需的信息填充到 Pod 对象中并 Patch 到 API Server ;
- Kubelet Watch 到需要启动的 Pod 后,会启动相关进程,容器运行时由 Kubelet 维护。
2.2 Remote Godel Scheduler
下面来介绍调度模块比较重要的一个服务 —— RGS 服务。由上图可以看到,RGS 服务主要分为三大部分:
- 最上层是 Quota Manager 负责进行 Quota 管理: Quota Manager 部分在 YARN 基础上进行了增强。在 YARN 中有队列的概念,但队列只支持一种资源类型。在 Yodel 中对此进行了扩展,一个队列可以同时支持两种类型的资源 —— Guaranteed Resource 和 Best-effort Resource。单队列支持两种资源类型后可以显著简化用户的队列管理成本,对用户使用更友好。
- Guaranteed Resource :稳定资源,使用 Guaranteed Resource 的容器一般情况下不会被抢占也不会被驱逐;
- Best-effort Resource:混部资源, 是在线节点出让的暂时空闲不用的资源,资源会随着节点负载情况动态波动,使用 Best-effort Resource 的容器可能会被抢占或驱逐;
- 中间层是 Allocate Service 负责进行请求转换和状态维护: 主要包括四个子服务,Convert Reqeust To Pod Service 负责将任务的资源请求转化为 Pod 对象并写入 API Server;Convert Pod To Container Service 负责将已经调度的 Pod 转化为 Container 并返回给 AM;Update Pod Status Service 负责及时更新 Pod 状态并持久化至 API Server ;Delete Pod Service 负责在容器或任务结束时,及时删除 API Server 中的相关对象。
- 最下层是 Remote Scheduler 负责进行调度和关键信息持久化。
通过上述各个服务的协调配合, Yodel 能够实现:
- 100% 兼容 Hadoop 协议,用户无需要做任何改动,可以像原来使用 YARN 一样来使用 Yodel;
- 支持 GT 和 BE 两种资源类型,方便上层用户对平台的使用。
2.3 Remote Kubelet Service
接下来介绍 RKS 服务。RKS 部署在 Yodel RM 内部,实现了 YARN NM 的所有接口,把 NM 的 Container 管理能力平滑下沉到 Kubelet。它主要由两部分组成:
- Patch Pod Service : 主要负责收到 AM 拉起请求后,将容器启动所需的信息丰富到 Pod 对象中,这些信息包括:容器的 ENV 、 HDFS 自研列表、启动命令等;
- Pod Status Update Service : 该服务会及时从 API Server Watch Pod 的最新状态,并将状态返回给对应 AM。
此外,为了补齐 NM 上的运行体验,底层以 daemonset 方式部署了一些其他服务。这些 daemonset 补齐了 NM 的能力,使得离线作业只需要升级 hadoop 依赖,不用做太多改动,就能让容器运行在 Kubelet 上。这些服务包括:
- LocalizationService : 用于下载 Pod 所需的 HDFS 资源;
- Log Serving : 用于方便用户查看 Pod 日志;
- Shuffle Service : 主要有 Spark Shuffle Service 及 MR Shuffle Service,这些 Shuffle Service 是从 NM 的进程解耦出来的,单独部署用于提供计算框架的 Shuffle 服务;
- Metrics Collector : 用于收集离线 Pod 运行时的各维度监控信息;
- Webshell : 方便用户通过 Web 端进入到容器的 Shell,方便排查问题。
下面看一个容器是怎么运行在 Kubelet 上的:
- 改造了 NM Client SDK,使 AM 调用 startContainer 时能直连 RKS;
- RKS 收到启动请求后,会把 containerLaunch 上下文等信息写入到 Pod 并 Patch 到 API Server;
- Kubelet Watch 到离线 Pod 后,会通过本机的 LocalizationService 下载 Pod 对应的 HDFS 资源;
- 下载完成后, Kubelet 通过 Containerd 把对应的 HDFS 资源挂载到容器的 Pod 里,之后通过 Containerd 启动 Pod;
- 启动完成后,Kubelet 会把 Pod 的状态更新回 API Server;
- RKS watch 到 Pod 状态变化后,同步更新内存中的 Container 状态,之后等待 AM 心跳时同步 Container 最新状态。
2.4 持久化服务
YARN 架构是通过 ZKRMStateStore 将元数据信息持久化到 ZooKeeper,而 Yodel 架构,我们自己实现了一个 KVStateStore 存储元数据到 API Server,存储的元数据包括 MetaData,Queue,Application,Appattempt 和 Pod。现在线上的一个 API Server 可以支持存储 300 queue,2w 个 application,10w 个 app attempt,以及支持 30W 离线 Pod 同时运行。
- MetaData:集群元数据信息、集群默认配置等;
- Queue (~300 / cluster) :队列 Quota、ACL 信息等;
- Application(~2W / cluster) :Name、User、State 等;
- AppAttempt(~10W / cluster) :Name、User、State 等;
- Pod (~30W / cluster) :State、Annotation、ENV、HDFS 自研列表、启动命令等。
2.5 平滑迁移
字节内每天运行着百万量级的任务,如何平滑地把作业从 YARN 架构迁移到 Yodel 架构是一个很大的挑战,整体上我们是通过 ResLake 来完成的,首先介绍几个关键组件:
- WorkFlow Hosting:作业托管平台,负责进行作业提交;
- ResLake:是 RM Proxy,可以根据一定策略把作业路由到不同集群;
- Quota Platform:用于同步队列的 Quota 信息;
- AutoMigration:负责从 YARN 集群下线节点,搬迁到 Yodel 集群上。
ResLake 在 YARN 集群和 Yodel 集群上有同名的队列,如上图中的 root.queueA 和 root.queueB,这两个集群上的队列有着相同的元数据信息。AutoMigration 服务会不断地从 YARN 搬迁节点到 Yodel 集群,搬迁信息会同步给 Quota Platform,Quota Platform 会进一步将队列 Quota 信息同步给 Reslake。作业托管平台提交作业到 ResLake 时,ResLake 会根据 YARN/Yodel 上队列的 Quota 信息,决定作业是提交到 YARN 集群还是 Yodel 集群。随着机器不断地往 Yodel 搬迁,最终作业也平滑迁移到了 Yodel 集群上。
2.6 重要优化
在 Yodel 架构升级上线过程中,也遇到了很多问题,我们也做了非常多的优化,主要包括性能优化和运行优化。
2.6.1 性能优化
- Recover 阶段异步恢复 Pod 状态降低切主时间(秒级):起初为了确保切主后集群、队列和任务的各维度统计信息准确,采用同步方式恢复 Pod,但上线时发现恢复过程非常耗时。为此通过优化,在确保各维度信息统计准确的前提下异步恢复 Pod 状态,将切主时间缩短到秒级;
- 异步多线程操作 Pod 以提高调度吞吐(~2K / s):通过异步多线程方式将已经调度 Pod 转化为 Container 后,调度吞吐得到显著提升,目前调度吞吐可以达到每秒 2000 个 Pod;
- PodName 散列优化助力底层存储写延迟降低为原来的 1 / 100(百分之一) : 因 API Server 底层采用基于 range 的 KV 存储,若 PodName 有序会频繁产生分区裂变,导致 API Server 的相关处理延迟显著增加。通过将 PodName 进行散列优化,将 Pod 打散存储在不同的分区中,底层存储写延迟下降 100 倍;
- 与 API Server 交互增强,Java Fabric8 Kubernetes Client 优化:
- 支持指数退让重试,增强 API Server 故障容错;
- List 操作默认添加 ResourceVersion 参数,避免击穿到底层存储;
- 将 Informer Resync 设置为 0,避免频繁内存拷贝造成 OOM。
2.6.2 运行优化
- AM 容器运行在单独资源池,独立优先级不可抢占:对于使用 BE 资源的容器有被抢占或驱逐的风险,而 AM 作为任务的 Master 一旦失败就会导致整个任务失败。为了避免此问题,将 AM 容器运行在单独的资源池,确保 AM 可以稳定运行避免任务失败;
- 支持镜像本地化约束,平均拉起速度提升约 1000 倍:一些离线任务的镜像比较大,在容器启动时拉取镜像会花费较多时间,进而导致启动时间变长。为了解决该问题,支持了镜像本地化约束,让容器可以尽量调度到有镜像的节点,该功能上线后容器平均拉起速度提升 1000 倍;
- 支持双栈节点和 v6 only 节点在单集群混跑:通过将双栈节点和 v6 only 节点混跑在同一个集群中显著降低了运维成本,同时也有利于资源利用率提升;
- Shuffle 数据写远程,避免打爆本地磁盘:shuffle 数据通常较大很容易将本地磁盘打满,将 shuffle 数据写远程后,可以避免因本地磁盘打满而导致任务运行异常;
- 大量引入 SSD 和 Nvme 磁盘,加速作业运行。
三. 上线收益
Yodel 架构已经在字节内部上线,上线后带来了如下收益:
- 高效资源切换:实现了 2022 元旦/春节 约 50 万核离线资源分钟级出让,显著提高了在离线资源转化效率,为重大活动场景下的资源切换提供了坚实的技术支撑;
- 利用率提升:NM 和周边单机组件下线,降低 Overhead,带来单机 2% 利用率提升;
- 在离线统一:在离线资源全量共池,Quota 管控、调度、运行、机器运维统一。
四. 未来规划
- RGS & RKS 部署云原生化
- 接入服务发现
- 支持容器化部署
- 可弹性扩展
- 开源 Yodel 回馈社区