引言
项目环境是集团研发同学联调测试必不可少的平台型工具之一,其环境申请与释放动态灵活,环境间流量相互隔离,在开发和上线前的个人自测以及全链路联调场景下有着不可替代的重要作用。一个稳定易用的项目环境能极大地提高一线研发同学的测试体验,通过对环境简化抽象、屏蔽基础设施和微服务复杂性,为业务提供稳定可靠、简单易用的测试环境。
项目环境作为与变更或某个项目的生命周期保持一致的灵活环境,相较于固定环境(如主干,预发,正式)有着不同的特质,其环境申请和释放与变更生命周期绑定,部署和重启更是其中的高频操作。随着项目环境覆盖范围的推开,在每天开发高峰期时期,每秒同时运行的工单量(环境创建、部署、重启、删除等)最高能到数千余次。这为项目环境本身的稳定性提出了极高的要求。
在项目环境建设初期,每一次创建、删除、部署等工单主要通过单机执行,通过内部流程引擎发送消息以及定时任务推动状态流转。在实践过程中随着工单量的上涨逐渐出现了任务重复运行,任务无迹象消失,任务猝死等问题。而研发流程的改造和推进也给项目环境提出了更高要求,但是以上问题的出现限制了项目环境承载更大范围使用的可能性。
本文通过梳理项目环境生命周期内创建、部署、重启、删除等任务的特点后,去除了流程引擎对消息的依赖,使用分布式分片任务,分布式锁实现任务的分布式运行。通过使用工厂模式,责任链模式,以及领域驱动设计的思路对流程引擎进行重构。最终实现在环境数量翻数百倍以上的情况下,日常以及预发环境平均创建成功率达到 99% 以上,单个环境创建时间由数百秒以上降低至 100秒 以下。工单量每秒并行执行翻近百倍的情况下,系统异常率低于 1% ,单次任务执行时间均值降低 8x%。
技术实践
文章架构
现状
1. 流程梳理
环境创建部署等流程如下所示,研发同学创建变更后项目环境会自动开始创建流程,非云原生应用在环境创建完成后便开始异步申请机器资源,等待资源扩容完成后,会自动开始部署流程。云原生应用会在部署过程中生产资源。项目环境资源生产完成后建立隔离标与机器的映射关系,即打标流程。三种工单(创建、部署、重启)的核心流程如下图所示:
2. 任务特点
可以看到上述三类任务有着两个显著特点:
任务间有顺序
单个任务为异步进行
异步任务需要在条件满足后才会触发下一个任务的执行,这是一个典型的流水线模型,所以我们内部将推动任务流转的模型称为流程引擎
3. 任务处理现状
异步任务由用户触发创建,定时任务会定时调用异步任务判断同步结果,任务完成后会通过消息推动任务间流转。
3.1 任务猝死
当异步任务在运行过程中抛出非预期的异常会导致任务直接猝死,由于没有发送任务完成的消息,任务间流转所依赖的消息丢失就导致流程引擎无法推动下一个任务运行。
3.2 任务处理单机瓶颈
环境创建、部署、重启都属于不可重入任务,如果被定时任务被分布式调用,会导致任务被重复执行导致资源浪费,工单报错等无法预期的任务。而单机调度就导致项目环境大部分的机器被闲置,出现一核有难、八核围观的状况。
3.3 任务重复执行
任务量的增多还会导致任务被重复执行。为了更快的推动任务执行,定时任务的运行间隔必须设置的足够短。但是随着任务量的急剧增多,定时任务单机处理会导致当前批次处理的任务在下一次定时任务被调度时仍未执行,从而导致任务被重复执行。
优化之路
1. 任务猝死优化
在流程推进过程中任意一个非预期异常都会导致任务戛然而止,在用户看来就是环境创建无机器,不部署,流水线不展示,隔离不生效等。首先对当前流程引擎的流程架构进行了初步梳理,如下图所示(可以不看,仅做展示)
1.1 架构升级-领域模型(DDD)
欲重构先架构,通过整理梳理发现,整个流程引擎可以分为四大部分,
触发流程创建的 GroupEnv(项目环境) 实体
流程操作的 AppRunningEnv(应用环境) 实体
保存流程信息的 Operation 实体
推动流程运行的 TaskEngine(流程引擎) 实体
根据抽取出来的四个实体,重新总结绘制了相互间的依赖关系图,将分散在各处的部署,删除,重启等依赖关系收敛到各自的 Domain 中,并抽取出四个能力:创建,提交运行,操作以及更新状态。
1.2 流程引擎重构
1.2.1 执行器统一接口
可以发现,子任务的执行过程可以分为两个部分
(1)执行操作
(2)同步结果
其中执行操作需要拿到子任务的初始执行结果。同步结果操作需要同步外部系统的状态到项目环境并更新子任务结果。所以可以将这两个能力抽象到一个接口中,即Taskexecutor。对于不同的子任务调用不同的executor。
1.2.2 使用工厂方法分拆执行器
针对不同操作使用不同的执行器这个思路与工厂模式非常契合。所以可以将每一个子任务执行器都抽取为一个单独的类,通过操作类型以及阶段从工厂类(ExecutorFactory)拿到对应的执行器执行对应的方法。
1.2.3 异常处理兜底
在流程引擎execSubTask以及handleSubTaskSyncResult两个方法中做异常统一处理,规范执行器异常处理,在异常信息中携带第三方或己方处理结果并格式化后填充到异常信息中。
1.2.4 操作子任务状态统一透出
在子任务执行器中构建统一的异常信息模板,并通过异常的形式透出给上层调用者。上层调用者不需要关注执行器中发生了什么,只需要将异常信息落库。
1.2.5 单元测试补齐
以流程引擎为入口,以HTTPClient与mapper为最大mock对象,实现全链路测试。
1.3 优化结果
重构后,极大地简化了任务执行流程,去除了对metaQ的强依赖,流程引擎依赖定时任务触发,实现异常兜底逻辑,单测全覆盖。后附CR[1-3],共删除冗余代码2700行。流程引擎核心流程收敛至Domain。
2. 任务执行时间优化
此前定时任务通过SchedulerX随机选择一台机器作为worker,之后从db中查询全部正在运行中的任务后逐一触发运行。单worker执行导致其他机器一直处于闲置状态。如何充分利用计算资源成为解决任务运行速度的重点。
分布式分片任务解决单机计算瓶颈
Schedulerx2.0提供了map模型,通过一个map方法就能将海量数据分布式到多台机器上分布式执行,通过利用MapReduce能力,随机选取一台机器作为master节点,将所有正在运行中的任务分批推送到包含该节点的worker中,逐一触发运行。总结为以下三个步骤:
取任务全集
任务分片
多机并行执行
经过优化,全量任务单次触发的执行时间降低为原来的1/n ,其中n为机器数。流程引擎具备了横向扩展的能力。
3. 重复执行优化
虽然增加了分布式分片能力,但是worker节点仍然是单线程执行,单机串行执行仍然存在执行瓶颈。所以对于单个worker节点通过线程池增加了并行执行的能力,通过增加线程同步器保证单个节点在所有任务执行结束后才返回。
3.1 秒级调度解决重复执行
使用秒级别调度,等待所有worker的任务都执行完成后才开始下一次调度。Schedulerx2.0的秒级任务具有高可靠的特性,如果某台机器挂了,可以在30秒内在另一台机器上重新拉起。任务执行时间进一步优化至1/(n*x),x为线程池线程数。
4. 多机忙等问题解决
使用分布式分片任务+单机多线程+秒级任务,项目环境的任务基础架构已经基本完工,但是在实际运行过程中仍然发现多机执行时间仍然很长。表现在用户触发任务执行后状态需要等待几秒后才会向前流转。
由于线程同步器以及秒级任务的双重加持,整个分布式分片任务的单次执行时间取决于本批任务中的执行时间最长的任务,这就导致很多worker在等待运行最长任务的worker执行后才会被再次调度。
4.1 分布式锁解决多机忙等
使用线程同步器可以保证单个worker所有任务执行完成后才会向主节点上报任务集执行完成的信息,在加快任务执行速度的同时解决了任务重复执行的问题,但是会造成执行完任务的worker等待的情况。
通过分析任务模型可以发现,AppRunningEnv(应用环境)是流程引擎执行的最小不可分原子,只需要保证单个环境不被重复操作,就可以去掉worker节点的线程同步器。解决方案就是分布式锁。
通过数据库唯一键索引可以根据AppRunningEnv的环境Id建立唯一主键,通过在任务被调度时争夺分布式锁,如果未拿到锁,则直接跳过不执行。获取到锁后执行任务,在执行完成或者出现异常后释放锁。
为防止占有锁的worker节点挂掉,导致任务无法被调度,在任务分片时,主节点会释放所有过期的锁,增加兜底能力。
结果
环境创建成功率
环境创建成功率从之前的随机成功,到现在的环境创建成功率稳定在99%以上(排除云原生应用后)
环境创建时间
在环境数量翻百倍的情况下,创建时间均值由300秒以上降低至100s以内。
任务执行异常率
工单量每秒并行执行n+情况下,系统异常率低于1%
单次任务执行时间
消除调用量尖峰,在工单量翻70倍的前提下, 最大执行时间下降为原来的十六分之一。