货拉拉大数据离线调度平台性能优化实践
引言
大数据离线开发平台作为大数据体系最基础的能力之一,从货拉拉大数据部门成立之初便从0开始搭建,支持任意数据库数据交换、丰富的任务分析方式,并提供完善的一站式交互查询服务。
随着货拉拉业务在全球范围内、八条业务线上展开,货拉拉大数据团队朝着“深度优化基础能力,高效整合中台体系能力,深耕驱动业务智能应用场景”的方向努力,越来越多的任务与业务场景的接入,也同时给平台能力带来了更高的挑战。本文将就离线平台最近半年内在满足功能、业务需求下,针对离线调度性能做的优化进行简单介绍。
调度背景&逻辑
货拉拉大数据技术体系作为典型Lambda架构,从建设之初便参考了业界大多数大数据体系,我们选择了研发门槛低、维护成本较小的离线链路(即Batch Layer)作为开发重心,一方面由于当时实时数据框架体系尚未成熟,另一方面则是考虑方便快速接入原有业务收益更明显,如下图所示:
随着货拉拉业务在不同业务线的展开、技术数智化设施完善、数据仓库基础建设逐渐成型,离线数据的重心也开始从支撑集团报表转变到如何科学赋能业务。在这个抽象的话题中,离线开发平台的关键点是如何在保证稳定和安全的前提下,高效支撑数据应用、线上服务、数据智能等多种场景,比如数据智能场景中的特征计算部分,既需要离线数仓与特征系统功能打通,又需要离线计算与特征计算流程保持数据与任务语义一致性,使计算出的结果能加速落到特征查询系统等其他服务存储中使用,如下图所示:
在如上场景中,一方面对离线平台的功能如任务管理、编排,监控告警,API等提出了更高要求,另一方面任务及实例会肉眼可见大幅度增加。
由于当前离线调度采用了单层实体模型,即基于任务级别配置对当前运行周期生成的可运行实例进行下发式调度。大致实现为:周期性从存储中扫描出符合调度下发条件的任务实例,在内存中对依赖及相关边界条件进行计算后再执行下发,分布式的Runner节点进行具体任务执行操作。这种调度方式最大优势是每个实例都会严格计算一遍依赖及下发条件,调度逻辑准确性SLA能很轻松达到100%,但是同样如果同一周期内实例数达到一定量级,依赖计算和下发环节的延迟有线性恶化可能性,同时给数据库的压力也将增加。据生产环境测试估计,以当前调度方式持续运行,一年内,可能导致实例的平均调度时间增长到分钟级别,显然调度能力的优化问题亟待解决。
优化方案
针对类似复杂系统的性能问题,战略上通常首先要定义出问题本质,即量化出性能瓶颈,再评估能够优化达到的目标,最后拆解到每个模块/环节能做哪些优化动作。由于前两部分比较冗长复杂,我们将重点聚焦在战术上,即每个模块可以落地的措施上:简单来说优化动作分为短期及中期两阶段,包含减少调度依赖计算量、优化关键环节算法及业务上的分级保障三个部分。
短期方案
前文强调过,日渐增长且不受控制的实例数量对于调度系统的压力在于每当进行大量进行依赖计算,对应的元数据库的负担都很大,所以我们能想到的第一个措施就是让每次调度计算的基数能降下来。
调度系统中,在整体角度通常很多任务是没有价值或会影响其它任务的:
- 比如没有上下游依赖,但是每个周期仍然在跑,这种任务既占据调度资源又占用集群资源
- 或者实例任务跑出来但没有数据,虽然不占据集群资源但仍然会使用到调度计算
对于这类任务,我们统称为僵尸任务,为了方便治理也进行了比较详尽的分类及统计。
僵尸任务及实例清理
如下表所示,对于不同的僵尸任务特征我们划分了四种类型,分别是数据空、失败、就绪、无依赖;目前离线平台中存在10W级别的这四种类型的僵尸实例无人管理,并且每天以几百的数量在增长,因此对于存量僵尸任务,可以直接进行冻结或强制成功操作。
针对此类实例,通过强制成功等手段,经统计平均每个任务能优化3秒左右的调度计算时间。
实例保留策略
随着时间的推进,实例也会递增增长,但是目前没有完善的实例清理策略,导致实例一直堆积在数据库中,对数据查询、计算,都产生了很大的压力。因此,完善的数据保留策略,能保证计算资源池的实例数量不会爆炸野蛮式增长。
如下表所示,在保证其最大数据生命周期的前提下(数据-任务生命周期一致性),我们对不同周期的任务也制定了不同的保留策略以保证增量实例能一直控制在可接受的范围内。
| 任务周期 | 保留策略 |
|---|---|
| 月 | 1年 |
| 周 | 3个月 |
| 天 | 1个月 |
| 小时 | 1周 |
链路分级
核心链路任务的调度优先级,要始终高于非核心链路任务。当同时满足下发条件时,核心链路任务可以不关注调度时间,优先下发;同时也增强了核心链路相应的运维、监控等能力,做到优先下发、优先维护、优先管理。
中期方案
另一种比较常见的性能优化思路是空间换时间,通常是通过损失一部分数据结构的改造或存储,使代码运行耗时得到大幅度改观。离线平台中这部分优化可以从如下两个角度切入:
内存结构
由于原始的离线平台元数据大部分存储于Mysql中,且依赖计算流程中会大量用到相关任务的元数据,为了提升调度速度,可以按不同的任务周期将任务详情、任务依赖、任务实例列表映射到内存,这样计算任务实例下发时,可直接使用缓存。当发生重跑、终止、强制成功、状态上报,更新数据库的同时更新缓存。主要流程如下图所示:
算法调整
单层实体模型调度下发最主要的过程是依赖的计算,即自依赖或父依赖。最普遍的情况下(全周期依赖)需要检查被依赖任务的所有实例,如果从元数据库中顺序取出会浪费大量时间,因此我们针对这两种使用最广泛的依赖类型,进行算法上的调整,使单次依赖计算时间得到量级上的提升:
| 现状 | 改进 | |
|---|---|---|
| 自依赖 | 现有自依赖会依赖历史所有的实例,依赖实例数量: n | 新增“前一周期依赖”,依赖实例数量: 1,时间复杂度: O(1) |
| 父依赖 | 现有父依赖检查每次都会查询数据库,时间复杂度: O(n) | 内存中实例按数据时间排序,二分法查找,时间复杂度: O(log n) |
高可用
随着缓存在核心调度流程中的深入使用,核心模块的高可用动作也涉及部分改造,主要体现在:
- 上报状态与更新缓存时通过ZK分布式更新保证分区容错
- FailOver时同时要在数据库事务后加上缓存更新及重载动作以保障DB和缓存一致性
如下图所示:
展望
通过评估与测试,当前的优化手段在日计算百万级别实例时仍然会暴露相关性能风险,结合业务实际情况 ,从长远来看,调度能力的优化还是要基于整体调度下发机制的改进及相关能力跟进来补充,主要方式有:计算、下发方式重构,定向调度能力补齐等。
调度模式改进
| 现状 | 改进 | |
|---|---|---|
| 计算方式 | 被动计算,现有的计算方式为Runner每次请求时触发计算,取优先级最高的实例下发执行 | 主动计算,新增依赖计算器,当状态改变时,驱动更改实例下发状态,满足依赖条件后,放入待下发队列 |
| 下发方式 | Runner主动拉取 | Base主动推送 |
定向调度能力
现有的Runner调度为随机运行,在实现定向调度后,可通过指定Runner运行,达到灰度发布的目的,同时避免Runner版本发布影响当前正处于运行状态的任务(导致任务失败或状态不更新等)。
笔者介绍:凌霄|货拉拉大数据平台负责人,主导画像、数据API、离线&实时研发平台等方向