字节跳动 YARN 云原生化演进实践
本文整理自字节跳动基础架构工程师邵凯阳、林友权在 2022 Hadoop Meetup 上的演讲,文章主要从演进背景、解决方案、上线收益和未来规划四个方面介绍了字节跳动 YARN 云原生化演进实践。
1. 演进背景
字节跳动(以下简称字节)内部离线业务具有庞大的规模,线上每天有数十万节点运行,每天的任务数达到百万量级,每天使用的资源量达到千万核量级。在如此庞大的计算规模下,为了能够高效地处理任务,提高资源流转效率,调度系统发挥了非常重要的作用。
如上图所示,我们可以清楚地看到,字节内部调度架构分为两大块 —— 离线调度系统 和 在线调度系统 ,离线调度系统主要负责离线资源管理和离线任务调度,在线调度系统主要负责在线资源管理和在线任务调度。
-
离线调度系统基于 YARN 实现,主要包括 Resource Manager(RM) 和 Node Manager(NM) 两个组件,负责资源调度和容器运行时管理。字节内部在 YARN 的基础上进行了很多功能丰富和优化工作,针对不同场景实现了不同的调度器,例如:Batch Scheduler,Gang Scheduler 等。 -
在线调度系统基于 Kubernetes 生态,进行了很多优化,支持字节内部多样化的在线服务。
为了提高字节内部整体的资源利用率,我们也进行了 混部 技术 的 探索 。 主要思路是在在线的节点上同时部署 Kubelet 和 NM 服务,当在线节点比较空闲时可以及时将空闲资源出让给离线业务使用,以此使得整个数据中心的资源利用率能够得到比较大的提升。
但随着公司内业务规模 的持续发展 ,这一套系统也暴露出了 一些 短板 :
-
首先,在离线属于两套系统,一些重大活动场景需要通过运维方式进行在离线资源转换,运维负担繁重,转换周期长; -
其次,现在的混部架构只是在部分节点上同时部署了 NM 和 Kubelet 两个 Agent,资源利用率仍有很大的提高空间; -
最后,在离线是两套割裂的系统,Quota 平台、机器运维等都不能复用,大数据作业无法享受到云原生的各种好处,例如:资源池化、更好的单机隔离特性等。
综上所述, 字节内部有 三个核心诉求:
- 重大活动场景(春节 / 双 11 等),在离线资源需要能够 高效、灵活 地相互转换;
- 整个数据中心的利用率需要得到更全面、充分的提升,进一步 降本增效 ;
- 在离线 资源共池 ,Quota 管控、调度、运行、机器运维统一。
-
对计算引擎侵入较深,计算引擎侧需要做大量改造才能支持原先在 YARN 的各种特性; -
生产环境的作业(百万级)非常多,如何从 YARN 平滑迁移到 Kubernetes 也是个比较大的问题; -
特别地,部分比较古老的计算引擎,比如 MapReduce,目前处于 Maintain 状态,已经无法进行大的改造来迁移。
2. 解决方案
下面将从 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。
-
ZK / ETCD / KV State Store : 支持将集群元数据信息持久化到 ZK、 ETCD 和 KV 等持久化存储,可以通过 API Server 方便地进行相关数据查询和更新; -
Remote Godel Scheduler : 维护集群所有任务的资源请求,通过该服务将任务的资源请求转化为 Pod 写入 API Server,同时与 API Server 交互获取已调度的 Pod,最终将调度能力下沉到底层的 Godel Scheduler; -
Remote Kubelet Servic e: 实现了原来 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 来说,它的生命周期是怎么样的 ,核心流程如上图所示:
-
首先,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
-
最上层是 Quota Manager 负责进行 Quota 管理: Quota Manager 部分在 YARN 基础上进行了增强。在 YARN 中有队列的概念,但队列只支持一种资源类型。在 Yodel 中对此进行了扩展,一个队列可以同时支持两种类型的资源 —— Guaranteed Resource 和 Best-effort Resource。单队列支持两种资源类型后可以显著简化用户的队列管理成本,对用户使用更友好。
-
Guaranteed Resourc e : 稳定资源,使用 Guaranteed Resource 的容器一般情况下不会被抢占也不会被驱逐; -
Best-effort Resourc e: 混部资源, 是在线节点出让的暂时空闲不用的资源,资源会随着节点负载情况动态波动,使用 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 Platfor m: 用于同步队列的 Quota 信息; -
AutoMigratio n: 负责从 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 磁盘,加速作业运行。
3. 上线收益
Yodel 架构已经在字节内部上线,上线后带来了如下收益:
-
高效资源切换 : 实现了 2022 元旦/春节 约 50 万核离线资源 分钟级 出让,显著提高了在离线资源转化效率,为重大活动场景下的资源切换提供了坚实的技术支撑; -
利用率提升 : NM 和周边单机组件下线,降低 Overhead,带来单机 2% 利用率提升; -
在离线统一 : 在离线资源 全量共池 ,Quota 管控、调度、运行、机器运维统一。
4. 未来规划
-
RGS & RKS 部署云原生化
-
接入服务发现 -
支持容器化部署 -
可弹性扩展 -
开源 Yodel 回馈社区
戳
“阅读原文”,参与技术交流!