🙋🏻♀️ 编者按:本文作者是蚂蚁集团客户端开发工程师企立,介绍了支付宝精细化调度的技术演进以及未来规划,欢迎查阅~
背景
从 Unix 诞生到现在 Android,iOS 系统生态完善,对设备资源如何合理分配一直是开发者,用户等各个角色所努力的方向。对于 Android 这样的 linux 内核来说,设备对外调度的资源单位是进程,但是进程内部还有线程,目的就是为了更充分的利用 CPU 资源。
随之带来的资源争夺,系统开发者也设计了不少的调度器 ,只为更合理的安排执行“用户”提交上来的线程任务,从基本的 FIFO 和 RT 调度策略,再到 CFS 和各项调度器的混合使用,每一次变化其实都是在针对不同场景,不同系统,不用时代,做不同的策略转换。
支付宝作为生态中的重要一员,并且拥有强大的用户群体,也在这个过程中演进,也时刻参与着这个竞争,支付宝内部的业务复杂繁多,导致“内部竞争”也异常激烈,针对这样外有与系统其他资源的争夺,内有业务内部的资源抢占的情况, 如何能合理的制定出适合支付宝自身的调度方案,满足不同时期的业务需求一直是端上一个重大的挑战。
一路走来-性能体系化的建设
支付宝发展到现在,已为用户提供了大量的服务,这对性能的挑战异常艰巨。性能的优化,主要经历了三个阶段。
雏形阶段:最初的版本中,业务处于高速发展期,大部分业务直接使用原生系统提供的线程池进行开发
问题与挑战
由于每个业务维护自己的线程池,他们很少从端上整体资源的角度去考虑,并且随着支付宝的发展业务也越来越多,凸显出如下问题:
- 超负运行:其中很多业务会无节制的起大量线程去执行,使 CPU 的调度压力增大,主线程及高优线程被分配的概率减少,资源没有合理分配,导致支付宝运行超标,并发严重,引起用户卡顿。
- 缺少统一调度管控:此时框架也无法进行对业务执行线程或任务的干预,导致线程复用率低,任务也不能进行调度。
- 架构分散:每个业务都使用自己的线程池,架构分散厉害,对技术的沉淀也相对较少。
1.0 线程调度
雏形阶段最大的问题,是支付宝超负荷运行,框架也没有进行合理调度,所以 1.0 开始框架提供了统一的线程池服务,业务根据不同的任务类型,可以拿到一个合适的线程类型去执行任务。这样框架可以从整体上把控线程数量。并且不同类型线程池策略是不一样的。这样系统资源的分配就更加合理了,并且也有了框架干预的能力。
关键技术
构建核心线程池调度
- 支持不同优先级类型:在原生提供的线程池基础上新添加不同优先级类型,将线程池归类,按照优先级不同,吐给业务不用类型的调度,针对支付宝特征分为:
- 为前台 UI 所依赖,优先级最高
- 一类紧急任务,专为首页渲染相关任务使用
- 二类紧急任务,优先级一般,不能容忍排队
- 普通不太紧急,可容忍排队的后台任务
- 文件 IO 类操作,持久化任务,耗时可以预计,要么不久成功,要么发生异常
- 网络相关的后台任务,耗时波动较大,典型使用场景为发起 RPC 请求
- 相同 KEY 的 Task 会保证有序串行执行(但不一定全在同一线程),不同的 KEY 对应的 Task 之间会并发
- 线程数量控制:针对不同类型的线程池,制定合理的线程数量,并且做到可动态调节,控制
遗留的问题
1.0 时代架构解决了统一对业务线程进行管理、分级的问题,但是我们所服务的业务也在不断增加扩展,要支持的场景也越来越多,这样也暴露出新的问题。
- 缺少重点场景聚焦:如 每个业务都想在支付宝冷起之后预加载一些逻辑,这就导致启动后一段时间持续高并发,CPU 利用率持续处于高位,这导致点击进入某个一级业务(比如扫一扫、付款码)等操作,用户体感明显降低,卡顿现象出现
- 调度单位局限:此时的调度单位只能到线程维度,不能更细粒度的调度执行,这样会导致虽然线程复用了, 但是业务还是会无节制的提交任务,导致潜在的丢任务风险,并且任务与任务之间的联系也会被忽略
2.0 任务调度
为解决上面的问题,就需要对执行的任务有干预能力,理解当前的用户场景,于是我们构建了基于任务调度的框架,架构图如下:
任务调度框架在线程调度的基础上,添加任务调度层,业务层,监控诊断工具
任务调度的优势:
- 分工更明确细致,结构清晰
- 可接入专属任务隔离接口,对重点场景(例如进入扫一扫)做更细粒度的资源分配调度
- 监控能力提升,诊断工具逐步完善
对于基于场景的调度,例如进入扫一扫后的性能,效果显著:
关键技术
任务染色:隔离公共任务与专属任务
- 专属染色线程池:框架专门提供一种染色线程池,并提高其优先级,该线程池不对外开放,业务在执行专属任务时,回调用染色接口,该任务回自动转派到该线程池中执行
- 公共任务隔离:其他公共任务因没有使用染色接口,还会继续在公共线程池中执行,这样就做到了专属任务与公共任务的隔离
- 染色窗口:有了染色接口并且解决了任务隔离,还有一个重要的问题是任务依赖,如业务依赖一个基础模块提供的服务,在基础服务中需要开启子线程执行一个任务,那么在业务层面,无法干预其他模块的任务,那么解决办法就是业务方打开一个染色窗口,在此窗口内的任务,会被自动染色。
Captain 任务链调度
在解决完重点场景的调度问题后,还需要解决启动后任务无序、高并发的情况,进一步优化整体的执行效率,那么如何提供能力解决这个问题呢?这里我们参考了 google 提供的 WorkManager 的设计,我们命名为 Captain 调度。
打散调度原理
构建一个 worker 任务族,用来提供存放 worker 任务,编排任务执行过程及控制并发数量的能力, 将业务提交的任务(runnable)封装为一个 worker,并将其放入上述的任务族中,使用对外提供的一个 WorkManager 设置各项执行参数和调度行为。通过 worker 专属线程池,执行被封装为 worker 的任务。添加 Captain 调度接口,用来业务接入和调度时机配置。
调度触发时机可定制,目前支持
- 主线程繁忙程度触发调度
- 当前帧率变化触发调度
- 当前 CPU 使用率变化触发
支持能力
- 创建工作链,建立任务依赖执行关系能力
- 可支持最大并行数量限制
- 打散能力,根据任务调度优先级,触发时机等条件构建出对应任务族,任务族根据上述参数变化执行任务
描述:A 先执行,接着并行执行 BC 和 D(此时并发数量限制为2个线程并发),其中 B 和 C 是串行,最后等 C 和 D 都执行完,再执行 E
接入方式
3.0 调度任务升级
针对 2.0 时代功能机制上基本满足需求,但随着业务发展暴露如下问题:
- 染色接入成本高,除自身业务模块接入外,还需要对依赖的模块也进行接入
- 调度范围有限,只支持框架线程池任务调度
关键技术
AOP 收口
针对调度 2.0 产生的新问题,我们又进行了升级,首先是要收口所有非框架线程池的任务,这里用到了 AOP 的技术,对 java 层的所有启动线程/任务的方式进行了切面收口,收口的发起方式有:
升级染色传递方式
调度 2.0 在遇到 native 线程、三方的 SDK 是无法做到染色传递的,并且对于任务(Runnable)启动任务也是无法染色传递,上述情况都需要手动染色加白,业务方适配起来成本高,容易“踩坑”,调度 3.0 为了解决这个问题,采用了任务树的构建原理,进行依赖染色传递。
任务树构建原理
使用 DexAop 的能力,对任务的构造函数添加切面,在每个构造任务的时机去反向抓取任务的执行链路,可以寻找到拉起当前任务的上一个任务,根据上一个任务的染色标记决定当前任务的相关属性,以此类推生成调用链关系,导出运行时任务的执行"树"。如下图:
任务调用链关系生成方式(规则):
- 调度管控期间由主线程派发出来的线程及任务被认为是前台线程或任务
- 任务调度管控期间由 native 线程派发的
- 若当前线程或任务属于前台,则它的 next 任务及线程同样置为前台属性
- 预先被加载的线程:按照首次唤起该线程任务作为上一个节点进行传递,若唤醒任务或线程为前台任务则被唤起的线程同样被传递
- 云控加白策略
监控:建立以任务树为维度的执行监控,对支付宝的整体任务执行情况,不同场景内任务执行时间,执行占比,执行次数进行分析
调度能力提升
让线程进入 TIMED_WAITING 状态
- 任务被提交后判断该任务是否为前台任务
- 若为前台任务直接运行
- 若为非前台任务则计算当前管控任务时间作为 sleep TimeOut 时间,并对当前线程执行 sleep 操作
- 若已到达调度兜底管控时间或者业务流程已经结束则进行推出管控逻辑:对已被 sleep 的各线程触发中断信号,唤醒,并继续执行
相比 2.0 的提升
任务调度升级 | V2.0 | V3.0 |
接入成本 | 高 | 低 |
是否有漏染 | 是 | 无 |
调度范围 | 框架线程池线程及任务 | 所有Java线程任务 |
调度场景隔离 | 无 | 有 |
遗留问题
- 能力单一问题:当前的调度能力架构,只能调度任务级别,对支付宝这样庞大的 App 来说调度维度和调度能力,相对单一,所以我们是不是可以扩大调度范围,从业务维度思考,建立更多维度的调度能力。
- 缺乏对业务的理解,任务没有业务归属:其实现在支付宝已经有明显的层级关系:不同业务域会对应有不同业务线,业务线会有不同的业务,业务内又会分为不同的模块 ,模块里可能才是我们调度的任务。如果可以把任务归属到某一业务,这样对调度范围,属性传递都可以进行更好的控制
- 框架提供的启动时机,广播、切面等这样的异步接口过度使用,缺少规范
- 关键节点没有强收口,没有调度管控,导致高并发
- 依赖关系不明确,未按需加载:当前任务,业务之间的依赖关系相对较分散,这样的情况对于管理无疑是不可控的
展望未来
精细化调度建设
3.0 任务调度仍然有很多问题:框架缺乏对业务的理解,任务没有业务归属,调度的范围也只有任务这一单一维度,各种启动时机、广播等过度使用,缺少规范,没有站在用户的角度去思考和决策端上的任务编排。后续新的调度框架会以“按需加载”为原则,建立更符合当前情况的精细化调度,解决上述问题。
精细化调度要具备的能力
- 统一管控:将对端上的事件,周期等做统一管控
- 模块化插拔:支持以"模块"为维度的调度,可支持云端配置,以用户视角去调度功能模块
- 分级能力:基于设备分级体验能力,包括:性能评分分级,网络状态分级。对设备做更细致的分析,达到功能模块,产品在适当的机型运行,例如低端机上可以对营销类业务,动画进行降级处理
- 场景识别,串联:解决端上错综复杂的依赖关系,如当前冷起进入扫码场景时,将前台场景属性 tag 添加到多媒体模块的任务里,保证在接下来的流程中传递并高优执行
- 中心化决策:建立统一决策中心,包括产品、运营决策调度;业务输入的调度;根据用户行为预测的智能决策