1. 项目背景
字节跳动的业务类型具备多元化的特点,主要包括在线业务体系和离线业务体系。
- 在线业务体系:通常服务于终端用户,包含 Web 服务,算法服务,有状态服务,视频编解码、FaaS 服务等,这些服务通常对 RPC 调用延迟比较敏感。
- 离线业务体系:包含临时查询、定时报表、模型训练、数据分析等作业,这些服务的特点是它们可以承受一定程度的排队或等待,在合理时间得到合理结果即可。
为了保证在线业务的稳定性,研发团队会将大量计算资源供给在线业务体系。这会导致离线作业处于较为严重的排队状态,而在线业务体系自身会呈现比较明显的潮汐效应。
下图展示了字节内部的在线业务和离线业务的天级 CPU 利用率情况。从图中可以看到,离线业务基本可以在天级维度将 CPU 利用率维持在较高水平;而在线业务晚高峰的整体 CPU 利用率可能只达到峰值的 20% ~ 30%,造成离线业务排队和资源浪费的问题。
为了更好地解决资源匹配问题,业界的常用做法是通过在离线资源并池实现利用率的提升,字节跳动内部也采用了类似的方式。根据统计,字节内部资源占用最多的在线业务主要是 Web 服务和算法类服务;排队最严重的离线业务主要是报表查询和模型训练相关的作业。
因此,字节内部研发团队的优化重点是针对这些不同的业务类型,定制不同的并池方案,主要有以下 2 种资源效率提升场景。
1.1 场景一:在线 Web 服务和离线批式作业
研发团队首先选择进行并池的服务是在线 Web 服务和离线批式作业。为什么选择这两种服务,主要是考虑到它们的业务模型简单,且资源模型互补。
- 在线 Web 服务:由于字节的微服务架构大多基于 Golang 进行编写,在线 Web 服务在资源使用模式上更加偏向于 CPU,较少占用内存、磁盘、网络等资源,因此在线 Web 服务天然适合与离线报表查询的批式作业进行混合部署。
- 离线批式作业:批式运行时间短,存在快进快出的特性,同时十分消耗内存和吞吐,在资源模式上可以与在线 Web 服务形成匹配;同时,离线作业在运行过程中并不重延时,当在线服务出现 Burst 行为时,我们可以在单机维度对离线作业进行资源挤压,甚至杀死异常实例。
因此研发团队采取了在离线混合部署方案,通过单机多维度的资源隔离以及中心 + 节点两级管控的策略,很好地支持了两种服务进行并池尝试。
1.2 场景二:在线算法服务和离线训练作业
为什么选择这两种服务?主要是考虑到它们的业务模型复杂,且资源模型同质。
- 在线算法服务:包括推荐、广告、搜索核心服务等;算法服务在运营过程中需要加载大量的在线模型,在资源使用上除了占用 CPU,也会占用较大的内存;同时算法服务不仅对调用延迟较为敏感,对业务的展现效果也有一定要求;为了满足服务的极致性能要求,我们通常需要对服务进行一些 NUMA 绑定,或者使用 GPU、RDMA 等异构设备支持达到交付效果。
- 离线训练作业:包括推荐广告 CTR/CVR、NLP训练等;该类服务在训练过程中需要注重吞吐和效果,如果对其进行资源挤压,将无法保证训练的效果是否稳定复现;同时训练作业运行时间长,且需要多个不同的训练角色协调互动才能够完整运行一个业务;为了满足训练作业的高质量资源要求,我们也会提供 NUMA 绑定和异构设备支持。
因此,面对这类资源并池场景,字节研发团队采取了弹性并池方案,即在在线业务低峰的时段将在线资源进行缩容,腾出空闲的资源供给离线业务使用,从而实现资源的分享复用,提高资源利用效率。
1.3 弹性并池挑战
为了保证弹性并池方案的顺利落地,此处有三点值得考虑:如何弹、如何用、如何稳。
- 如何弹:在线业务容器化改造后,天然支持水平扩展,但是离线服务会有一些比较复杂的编排框架,因此我们需要对离线的业务体系提供一些深度的结合与定制,增强弹性能力。
- 如何用:在线业务和离线业务作为两套不同的业务体系,甚至可能部署在两个不同的集群,因此如何实现跨资源的协同感知也是一个重要的问题。
- 如何稳:弹性资源最大的特点是它整体的资源供应量不确定,当在线服务出现抖动时,我们需要优先保证在线服务的稳定性,极端情况下会做容器兜底杀死的逻辑,而这会与保证离线业务的稳定性背道而驰,因此如何在不稳定的资源供应基础上保证离线业务的稳定性也十分重要。
2. 在离线弹性体系
针对上述弹性并池挑战,下文将分别从在线弹性设计、离线式分布训练、在离线资源协同感知和稳定性保证四个方面提出解决方案。
2.1 在线弹性设计
在线服务天然支持水平扩展,关键挑战在于构建快、稳的弹性系统。为了应对该挑战,先来看一看在线弹性分层架构,如下图所示:
从图中可以看到,Agent 负责采集业务各种数据,包括业务指标如 QPS 、P99 延迟等,以及系统维度指标如 Load、CPU 利用率等。这些数据最终会由两个接收方进行消费,一方面它会通过中心式采集的组件进入到实时数据的存储系统,另一方面它会通过一个消息队列进入离线算法模型中。
中心式的 Controller 负责消费这两种数据,并在这些数据的基础上决定当前的扩缩容行为。需要补充一点,字节内部研发团队没有使用原生的 Deployment 描述在线的无状态服务,而是在上面构建了一层 HPAGroup 用于控制多个 Deployment 支持小流量或者 AB 发布。因此扩缩容行为是由 Controller 调整 HPAGroup 的 replica 数,最终进入到 K8s 调度体系中产生 Pod,完成最终的调度。
2.1.1 在线弹性实时性保证
如何实现整个弹性系统”快”的特性要求,研发团队在实时性保证方面进行了三方面的优化,以达到“非突发流量预先扩容,突发流量分钟级扩容”的效果。
一是模型触发:研发团队建构了一个模型触发机制,可以基于业务的历史数据构建资源画像,从而取得业务流量。在没有发生异常突发的阶段,可以提前预测执行扩容行为。
二是实时触发:研发团队自研了可扩展内存数据存储系统,同时根据字节内部的服务组织方式,在内存中建立了多级维度索引,加快查询效率。同时我们通过实时数据预取,以及聚合逻辑下发的方式,加快整个数据获取的速度。
三是组件性能:在整个扩容链路中消耗时间较大的主要有三个方面:K8s 云原生调度器的性能、镜像拉取的性能、推广、搜索核心服务。针对这三种场景,我们首先通过分片调度 + 乐观并发 Bind 的方式来加速我们调度器的吞吐和性能,其次通过镜像 Lazy Loading 进行按需加载,最后自研 P2P 实现镜像和模型快速分发。
2.1.2 在线弹性稳定性保证
如何实现整个弹性系统”稳”的特性要求,研发团队进行了以下 6 个方面的优化。
配额:缩容部分资源仍然占住配额,服务可以随时可回收;
数据:系统指标和业务指标协同,完善数据异常 fallback 机制;
组件:分布式追踪 Pod 生命周期,完善报警,快速定位组件异常;
联邦:集群具备自治能力,联邦提供自动故障切换;
组合:根据调用关系提供链路组扩缩能力,避免出现服务瓶颈;
兜底:在缩容业务实例时,并没有真正删除容器,而是建立一个 Shadow Deployment 通过上层流量摘除以及启动进程替换实现容器的保留。当出现一些异常情况时,我们可以快速地重新拉取流量,从而实现一键式容灾,如下图所示:
2.2 离线分布式训练
离线分布式训练模型根据通信模式的不同,主要分为两种模式:PS-Worker 框架和 Ring AllReduce 框架。
2.2.1 PS-Worker 弹性定制
下图展示了 PS-Worker 离线分布式训练框架:
在 PS-Worker 训练框架中,所有业务实例大致分为两种角色:PS 和 Worker。其中,PS 负责存储整个分布式训练的参数,其本身需要保证相对的稳定,不具备弹性能力;Worker 负责实时地从 PS 里面拉取当前模型参数,并从 HDFS 中读取模训练的数据输入,将训练完成的梯度的信息更新到对应的 PS 中。
由于在该场景下,PS 本身不具备任何弹性能力,且 Worker 弹性加速比其实不高,因此为了应对该场景下的弹性资源使用问题,我们通常会将整个训练作业作为一个维度来进行弹性扩缩。
2.2.2 Ring AllReduce 弹性定制
下图展示了 Ring AllReduce 离线分布式训练框架:
在该训练框架中不存在中心式节点,所有的节点都是 Worker,即所有的节点都可以参与梯度计算过程。整个训练过程会分为若干个 Step,在每个 Step 中,每个 Worker 都会获取一份独立的数据,并且每个 Worker 都会加载当前的模型参数,然后独立地去计算自己的参数梯度。
除此之外,Worker 之间是有序的,每个 Worker 在计算完当前 Step 的梯度之后,会找到自己的下一跳, 然后把自己计算的梯度传递给它。一轮 Step 训练最多可以经过 2N-1 次的参数传递和收敛,从而完成一轮所有梯度在全 Worker 的传播。此时 Worker 就有了完整的梯度信息,随后可以开始下一次 Step 操作。
Ring AllReduce 训练框架中,Woker 天然支持故障容忍和弹性,且弹性加速比很大,弹性加速的效果和 Worker 的数量呈现出正向比例关系,问题在于 Worker 之间存在非常明显的木桶效应,因此我们需要尽可能地保持不同 Worker 的资源是同质的。在该场景中,我们一般以单 Pod 的维度作为一个弹性的最小粒度来进行弹性扩缩。
2.3 在离线资源协同感知
为了实现在离线资源资源之间的协同感知,我们主要进行了两个方向的工作:单集群统一调度和跨集群资源整合。
2.3.1单集群统一资源调度
在离线并池的前提是构建统一的单集群调度和管控体系,研发团队主要进行了以下三个方面的工作:
自定义 CRD 扩展资源表达:K8s 原生拥有一个 Node 对象进行资源存储, 但是该对象的资源表达能力非常弱,只能存储一些简单的数据信息。但是由于字节场景需要很多异构设备支持,并且需要 NUMA 维度的拓扑表达。因此研发团队自定义了一个 CRD 扩展资源表达能力。
微拓扑感知调度:字节研发团队自研了一套在离线统一的调度系统,支持 Gang/Same 等丰富的调度语义,另外基于 Schedule Framework 去做了很多定制化的 Predicate 和 Priority 的策略改造,并且尽可能地去堆叠 NUMA 来减少资源的浪费等。
微拓扑感知管控:字节研发团队使用了社区原生的 Toplogy Manager 来实现拓扑的分配和管理。并且将社区原生的 CPU Manager 或者 Memory Manager 下沉到 Plugin 中实现,同时抽象了一层和 Device Manager 同级的 Resource Manager 。
2.3.2 跨集群资源整合
为了解决跨集群资源整合的问题,字节研发团队引入了社区的 Virtual Kubelet 方案,在离线的集群中插入了若干个虚拟节点,由虚拟节点汇总某一个在线集群里面的弹性资源信息,从而实现跨集群的资源整合。
引入虚拟节点也带来了一个新的问题,即 Gang/Same 语义实现起来会比较复杂。为了解决该问题,我们执行了一个比较简单的约束——对于一个相同的作业,它所有的 Pod 如果需要去使用弹性资源,当前只能通过一个虚拟节点同步到同一个后端集群,由此可以把整个微拓扑或者调度的难度重新 Upload 到单集群的视角进行解决。目前的方式是在后端实现 Same 和拓扑感知的语义,在前端保证 Gang 的语义。
2.4 稳定性保证
至此本文已经解决了在线业务和离线业务“怎么弹”、“如何用”的问题,但我们还面临最后一个问题,即如何在看似不稳定的资源里面为离线业务提供稳定性的保证。资源不稳定性来源于多方面,主要有以下三种:
首先,整体资源的供应是不稳定的,因为整个资源的供应量是完全受制于在线业务的扩缩容情况,这会导致整体的资源的总量、资源供应的时间,甚至每一天资源所对应的具体的机器环境是不一样的。因此我们没有办法让离线业务针对弹性资源做一些提前的资源规划,同时当在线业务发生任何抖动时,我们会随时产生资源回收,这对整个训练作业的体验并不好。
其次,单个作业内存在 Min/Max 语义,即 Worker 的数量其实是不确定的,离线业务整体的资源描述也并非确定值。同时我们还需要解决一个问题,即在提高单个作业的训练速度和满足更多训练作业之寻求平衡。
最后,由于 Gang/Same 语义的存在,资源调度的过程中可能会出现资源死锁情况,即每一个作业都拿到了一部分资源,但这部分资源不足以支撑它运行,由此资源死锁导致资源浪费。
字节内部如何解决上述问题?
- 在资源供应方面:我们在执行缩容操作的过程中,引入了 Deletion Cost 机制定义实例缩容的优先级。比如我们可以尽可能地缩容整机或者整 Socket,甚至尽可能地保证这些缩容出来的资源处于同一个 Pod 或者使用了同质的 GPU ,从而减少资源碎片的问题。
- 在资源分配与调度方面:一方面,字节研发团队采用的策略是优先满足单个作业的加速比需求。因为作业在调度和非调度的过程中,可能会执行很多次 Checkpoint Dump 和 Reload 操作,这个操作过程需要从 HDFS 上实现完整模型的上传和下载,非常耗时。如果我们运行更多的作业,虽然在一定程度上可以优化用户的体验,但是会触发多次无效的 Checkpoint 操作占据大量时间,从而降低资源的利用效率。另一方面,为了解决资源死锁问题,我们在 Gang 语义的基础上加了一个随机超时机制,即在调度器中,如果我们发现一个 Gang 语义持续了一段时间仍然不能满足,我们会先执行一段随机的休眠,然后回滚调度状态,让其他作业能够重新再来一次 Gang 过程,避免资源死锁。
- 在资源回收方面:为了解决资源回收的过程中无脑地杀死离线业务的问题,字节研发团队构建了弹性资源的优先级,基于优先级实现资源回收。目前的弹性资源大概分为三级,如下图所示。以 PS-Worker 架构为例,PS 作业可能会处于一个 High 的优先级;能够满足基本运行的 Min 的 Worker 处于中优的优先级;为了进行弹性加测的 Worker 处于 Low 的优先级。这样做的好处是当我们在线进行资源回收时,我们可以定制一些调度器的抢占策略,使得在线服务总是倾向于去抢占低优先级的作业资源。
2.5 弹性并池体系总结
基于以上弹性方案实现在离线并池的整体架构图,如下图所示:
首先,在线集群里面存在着多个 K8s 集群,然后在上一层抽象一个统一的集群联邦,通过 Federation 和 Member Cluster 两级的集群建设,构建了一套在线内部实时且稳定的弹性系统,从而解决在线业务能够被弹起来的问题。然后在单个离线集群内部,我们通过和离线的框架进行深度的融合,从而解决了离线能够被弹起来的问题。
其次,为了解决资源的协同感知,我们通过引入虚拟节点,从而实现了跨集群的资源整合。同时在单个集群中,我们通过一体化调度和一体化单机维度的资源管控能力,从而很好地解决拓扑感知的调度和管控能力。
最后,为了解决离线资源使用体验问题,我们定制化了缩容策略、资源分配策略、优先级建设,从而使得离线业务虽然使用的不是稳定的资源,但也有一定的稳定性保证。
3. 案例分析
基于上述弹性并池体系,下文将分享三种弹性资源并池场景案例。
3.1 Ring AllReduce 使用弹性资源
场景一是 NLP 和在线推理服务进行资源并池。通常来说, NLP 场景更适合使用 Ring AllReduce 的训练方案。我们可以在一个 GPU 的显存里完整地加载所有的模型参数,通过 Ring AllReduce 更加合理地使用整体带宽,从而达到较高的加速比。
框架说明:在具体实现中,字节研发团队基于社区的 Horovod 和 Et-operator 实现了 Ring AllReduce 框架弹性。从上图可以看到,框架引入了一个中心式 Launcher 负责 Worker 之间通讯环建立、Worker 健康状态检查、异常处理等逻辑。
该场景的弹性思路:将 Launcher 和满足基本训练需要的 Worker 运行在稳定资源上,同时将用于加速的弹性 Worker 运行在弹性资源上。在线推理模型通常来说比较大,一个推理模型的实例可能会占据一个整机,因此这种做法的好处是:缩容一个在线实例等于缩容一台机器,从而供给完整的机器给 Ring AllReduce 的 Worker 运行,规避单机硬件资源的隔离问题。
目前整套框架的弹性加速比可以达到 1:8 水平,达到了对弹性资源充分利用的效果。
3.2 PS-Worker 使用弹性资源
场景二是 PS-Worker 和推广搜核心服务共用 CPU 和 GPU 资源的情形。通常来说,CTV/CVR 的训练模型非常稀疏,而且特征维度非常庞大,所以单机基本没有办法装上所有的特征参数,因此这类训练模式基本上只能使用 PS-Worker 架构。字节内部对 CTV/CVR 这种训练模型的需求十分大,为了更好地支持这种训练任务,字节内部自研了一套 PS-Worker 框架进行异步训练。
与传统 PS-Worker 不同的是,自研框架中的 Worker 被拆分为 Sparse 部分和 Dense 部分。其中 Dense 部分主要负责稠密模型的训练,它能在一个完整的 GPU 卡上加载所有模型参数,从而实现更好的加速效果;而 Sparse 和 PS 通常运行在廉价的 CPU 上。
同时, PS、Worker、在线三种服务会可能同时运行在一台机器上,共享部分单机资源,需要我们提供一些隔离机制减少互相干扰。例如,我们采用双网卡方案,在单机上进行分流,在交换机侧通过流量优先级打标的方式保证在线稳定性;在 NUMA 分配策略上通过微拓扑感知的能力,针对不同的角色定制 NUMA 分配逻辑。
另外,PS-Worker 的弹性粒度是作业整体维度,不可避免会造成比较大的资源碎片,为了解决该问题,我们引入了视频编解码或者 Spark 的一些 batch 类作业来填充资源碎片。
目前,这套框架在字节内部得到了广泛使用,每天可以出让大概 300 万核心乘以 7 小时的资源。
3.3 在线服务使用弹性资源
最后,在线服务也会使用弹性资源,主要分为以下两种场景:
一是节日或者促销类的活动场景:该场景的特点是资源需求规模非常庞大,但可以提前规划和容量预估。因此对于这种模式,字节目前使用的方式是 CronHPA + 分时 Quota 预先分配配额。
二是直播场景:该场景可能需要加载一些新的模型,从而需求更多资源以获取更多的展现效果上的正向收益。对于这种临时需要突破自己容量的情况,我们会允许它突破自己的 Quota 上限,使用低优的离线集群的资源来满足自己的临时性的需求。为了使得整个机制更加合理,我们会对低优的离线集群资源的水位进行实时监控,从而确保离线集群的资源不会被压榨得过于厉害,同时去保证离线服务总是会有一定的 Buffer 来承担在线服务异常流量增长情况。
4. 未来展望
关于未来展望,主要有以下三个方向:
规模化
-
在线服务弹性规模和时间扩展;
-
离线作业云原生和弹性化改造;
-
在离线一体化联邦建设,收敛在离线整个作业的提交入口。
产品化
-
弹性容器产品能力完善:目前字节内部弹性资源和稳定资源的使用方式相对割裂,产品能力上有一些不足,因此后续研发团队会完善相关的能力;
-
可视化资源视图和容量统计。
精细化
-
精细化调度,减少资源碎片;
-
集群维度资源切分到全面整合,形成统一的资源池。
更多资讯
- 项目地址:KubeWharf (github.com)
- KubeZoo: https://github.com/kubewharf/kubezoo
- KubeBrian: https://github.com/kubewharf/kubebrain
- KubeGateway: https://github.com/kubewharf/kubegateway