vivo 在离线混部探索与实践

一 、离线在离线混部技术背景
1.1 为什么混部
数据中心运行的混部服务可以分为在线服务和离线任务两大类 ,它们具有不同的探索资源使用特征 。
在线服务是实践指那些长时间运行、对时延非常敏感的离线服务 ,如电商 、混部游戏等,探索在线服务的实践资源利用率存在明显的波峰波谷现象 ,平均利用率较低。离线离线任务是混部指那些运行周期短 ,源码下载有容错性,探索对实时性要求低的实践服务 ,如数据转换、离线模型训练等 ,混部离线任务在执行过程中资源利用率很高 。探索
在混部之前 ,在线和离线都是分开独立部署 ,机器不共享 ,无法形成有效的资源互补 ,这导致数据中心整体资源利用率不高,却要不断购买新机器 ,造成了资源浪费。
1.2 混部技术定义
通过混部技术,服务器租用我们可以将在线和离线部署到同一台物理机上 ,形成资源互补 ,提升物理机的资源利用率,降低成本 。混部技术最早由谷歌在2015年提出,经过多年的发展,混部技术已经趋于成熟,目前业内利用混部技术可以将数据中心的CPU利用率提升至40%左右 。
vivo在2020年开始调研混部技术,2023年混部平台投入生产,目前我们已经将部分混部集群的CPU利用率提升至25%(最新已达30%)左右。相较业界标杆这还有一定的模板下载差距 ,但随着混部规模的扩大 ,我们将挑战更高的目标 。
二 、在离线混部平台实践2.1 混部平台产品能力
混部平台必须具备两个产品能力 :
第一 、强大的调度 、隔离能力第二、完善的监控、运维能力强大的调度能力解决了 ,我们如何将离线任务高效、合理的源码库调度到在线服务所在的物理机上。而强大的隔离能力保障了在线服务的质量不受离线任务干扰 。完善的监控和运维能力则可以让我们洞悉整个混部平台的运行情况,及时发现潜在风险,帮助运维人员更高效的完成系统和业务的运维工作 ,保障集群的高稳定性。
2.2 混部差异化资源视图
混部首先要解决的一个问题是免费模板离线使用哪一部分资源 。
在vivo混部系统中在线和离线看到的资源视图是不同的:
在线可用资源为 整机资源离线可用资源为 整机资源减去 在线实际使用的资源同时为了避免整机负载太高影响系统的稳定性 ,我们设置一个安全水位线 ,用于调节离线可用资源大小 。
2.3 混部QoS等级
为了保障混部系统的slo,我们将服务分为三个等级 :高、中,低 。
不同等级的服务对物理资源如:CPU、香港云服务器内存 使用时有不同的优先级 。高优先级服务支持绑定CPU模式,适用对延时非常敏感的在线服务 。一般的在线服务可设置为中优先级 。离线任务设置为低优先级,通过这样的等级划分,我们很好的实现在线对离线的资源压制和隔离 ,保障了在线服务质量 。
2.4 混部核心组件架构
我们所有的混部组件都是以插件方式独立运行,对原生K8s无侵入。我们实现了一个混部调度器,在线和离线统一使用这个调度器 ,避免了多调度器资源账本冲突的问题 。
每台物理机上都会部署一个混部agent,它可以实时采集容器资源使用数据,并根据安全水位线对离线任务进行压制、驱逐等操作。
内核层面我们使用了龙蜥OS ,它具备强大的资源隔离能力 ,可以帮助我们更好的隔离在线 、离线资源使用 ,保障在线服务质量。
2.5 混部组件功能
我们把混部组件分为管控组件和单机组件两大类 。
管控组件主要负责调度和控制,根据vivo业务使用场景,我们对调度器做了一些增强,提供了numa感知、负载感知,热点打散,批量调度等能力。
混部控制器主要提供了一些配置管理能力:如资源画像统计、node slo配置、node扩展资源变更等 。
2.6 混部可视化监控
我们为混部建立一套完整的可视化监控体系 。
针对在线服务我们提供了 :容器资源使用指标,受离线干扰指标 、业务混部收益指标等监控能力 。
针对离线任务,我们提供了离线可用资源、任务异常状态等监控能力。
在平台层面上我们提供了节点 、内核 ,核心组件的监控,通过这些监控可及时发现平台潜在的风险 。
2.7 混部平台运维
为了简化运维操作,提升运维效率,我们对混部集群搭建和节点扩缩容操作进行了白屏化改造,开发了资源池管理功能 ,简化了物理机接入流程,运维效率大幅提升。
在运维平台上运维人员可以快速调整混部隔离 、水位线等参数,如果发现在线服务受到干扰 ,运维人员可以一键关闭混部,驱逐离线任务,保障在线服务质量。
2.8 问题与挑战2.8.1 apiServer拆分

通过混部产品能力的建设 ,我们很好的实现了容器混部能力 ,但是在实践中我们还是遇到一些新的挑战 :相对于普通K8s集群 ,混部集群中运行着更多的容器,而且离线任务由于生命周期短,容器创建销毁更为频繁 ,这对K8s apiServer 产生了很大的压力 。
所以我们拆分了apiServer ,离线任务使用独立的apiServer ,保障了集群apiServer 负载一直处于一个安全水平。
2.8.2 监控架构优化

同样混部之后由于采集了更多的监控指标,导致Prometheus内存消耗过多,无法满足平台指标 采集需求。针对这个问题,我们优化了监控架构 ,将在线和离线监控组件分开部署,离线改用性能更好的vmagent ,通过这个优化,监控组件内存消耗减少到原来的十分之一。
2.9 利用率提升
混部初期虽然集群CPU利用率有所提升 ,但是还是没有达到我们的预期,主要原因有 :
一、部分低配置机器资源本身较少。二、Java 类应用堆会固定占用大量内存 ,导致可提供给离线使用内存资源不足。针对这些问题,我们开发了定时调整安全水位线功能 ,在业务低峰期上调安全水位线 ,释放更多的资源给离线使用 。通过一系列的优化手段,我们将其中一个混部集群的CPU利用率由13%提升到了25%左右 ,几乎翻倍 ,混部效果得到了有效的验证。
三、Spark on K8s 弹性调度实
3.1 方案选型
在大方向的技术选型上,我们选择了 Spark on K8s ,在业内,也有一些公司采用了 YARN on K8s的方案 。我们也对这两种方案进行过对比。
从业务适用性来说,YARN on K8s 是通用的 ,可以兼容Hive、Spark、Flink这些引擎 ,它不需要频繁创建Nodemanager pod,对K8s的压力比较小。这些都是它的优点 ,但另一方面,Nodemanager ESS服务是对磁盘有容量和读写性能要求的,混部机器磁盘一般难以满足 。所以我们要支持不同引擎的remote shuffle service。
如果计算引擎有不同的版本,那么RSS也要支持不同版本 ,比如Spark2,Spark3 。如果你有不同的引擎 ,不同的版本,很可能一种RSS还满足不了需求 。另外Nodemanager需要根据K8s混部节点的剩余资源 ,动态调整可用的vcore和内存,所以还需要一个额外的组件来做这个事情,这需要较高的改造成本。在资源利用上 ,NM的资源粒度相对大,自身也会占用一些资源,存在一定的浪费 。在资源紧张的情况下 ,Nodemanager作为整体被驱逐,会影响多个任务 。这些是YARN on K8s的劣势。
作为对比 ,Spark on K8s 劣势有哪些?首先这个特性在Spark 3.1以上版本才正式可用。Spark on K8s由于会频繁的创建、查询、销毁大量的executor pod,对K8s的调度能力以及master节点会造成比较大的压力 。另一方面 ,它的优势在于只需要能支持spark3.X的RSS ,这有较多的开源产品可选择。而且改造成本比较低,不需要额外的组件。资源粒度小 ,更有利于充分利用集群资源 。在资源紧张时 ,会逐个pod进行驱逐,任务的稳定性会更高 。
两方案各有优劣势 ,为什么我们选择了Spark on K8s?一方面因为Spark3.X是vivo当前及未来2~3年的主流离线引擎,另一方面vivo内部对K8s研发比较深入 ,能有力支持我们 。基于以上原因,我们最终决定使用spark on K8s
3.2 三步走战略
确定了方案选型 ,那在vivo我们是如何推进spark on K8s大规模的应用落地呢 ?回顾总结我们走过的路,可以大致归纳为三步走的战略。
第一,是任务跑通跑顺的初期阶段第二 ,是任务跑稳、跑稳的中期阶段最后,是任务跑得智能的成熟阶段接下来的内容,我们将对每个阶段展开细说。
3.2.1 任务跑通跑顺

在任务跑通、跑顺的第一阶段,我们要解决的是怎么将任务提交K8s集群,同时要求易用性 、便利性方面能够达到与on YARN 一致的用户体验。将我们最后采用的方案架构简化一下,就如同这张图所示 。
首先 ,为了降低任务提交的复杂性、避免用户改造任务的成本。我们在任务调度管理平台做到了对原有Spark任务的兼容,通过vivo内部的容器开放API-这个代理层,我们不需要维护额外的K8s client环境,就可以轻松实现任务提交 ,提交后也能近实时获取任务的状态和日志信息 。
另外一个关键点是,我们选用了Spark Operator作为Spark任务容器化的方案 。Spark Operator是谷歌基于K8s Operator模式开发的一款的工具,用于通过声明式的方式向K8s集群提交Spark作业 。
Spark Operator的方式还有其他优点 :
Operator方式对K8s更友好 ,支持更灵活 、更全面的配置项使用上更简单易用内置Metrics,有利于我们做集中管理要达到阶段一的目标,让任务跑通 、跑顺 。我们主要克服了哪些关键问题和挑战?

第一个是日志查看,因为Spark Operator方式并没有提供已结束作业的日志查看方式,包括driver和executor日志。在Driver侧,我们通过定期请求容器开放API ,能准实时地获取Driver Pod状态与日志 。在Executor侧,我们参考了on yarn的方式 ,Executor Pod结束后,日志上传HDFS ,与YARN日志聚合类似 。
另一方面 ,我们也在Spark HistoryServer做了二次开发工作,增加了on K8s方式的日志查看接口。用户查看已完成的Executor日志时 ,不再请求JobHistory Server,而是请求Spark HistoryServer接口。在体验上做到了基本与yarn一致。
在混部K8s集群 ,我们也做了三方面能力的加强 。
一是,确保分配能力能支持离线任务频繁建删pod的需求 ,在优化后我们离线Pod分配能力达到数百pod/秒。二是 ,在K8s侧提升了spark内部的Driver优先级,确保了在驱逐时Driver稳定性高于Executor。最后一个是,发现并修复了spark-operator的一个bug,这个bu是Operator在多副本部署时 ,slave副本webhook处理有一点概率出现pod 找不到的问题。3.2.2 任务跑稳跑准

在第二阶段 ,我们要保障的是任务跑稳 ,数据跑准,因此,我们有两个关键的举措 :
大规模双跑 ,目的是确保Spark任务迁移到K8s集群后是兼容的 ,任务成功率有保障;任务执行时长是稳定的 ,不会明显变慢;数据是准确的,跟on YARN保持一致性。为此 ,我们需要对任务进行on YARN和on K8s两种模式下的双跑测试 ,我们分批次总共进行了7轮双跑,覆盖了2万+的线上正式任务。最终也取得了我们想要的结果 :我们双跑最终达成的任务成功率超过了99.5%,绝大部分的任务在两种模式下的时长波动在25%以内 ,数据一致性是100%。混部集群的压力联调,目的是确保混部集群的承载容量能够支撑大规模的离线任务调度,通过模拟未来1年的任务量来给混部集群做压力测试 ,充分发现和检测K8s集群可能存在的性能问题 。最终 ,通过我们多轮压测和问题解决,我们在单个K8s集群能够支撑150+同时运行的Spark任务 ,1万+同时在运行的Pod数量。
在第二阶段