背景
工作中遇到一个项目,需要并行或串行的调度若干个AI或算法进程,一共四五类流程,类似流程引擎。区别在于每个流程节点需要启动单独的进程处理,需预估进程的资源消耗并限流、保证负载均衡与两个独立流程间基本的顺序性。
可类比此流程(只是对实际业务的一个类比,非实际业务)
类似此流程的还有若干个,流程相对固定,基本不会改变。
技术方案
基本架构
以上述流程图描述的流程为例,AI语音识别字幕、翻译,两个流程节点显然是要依赖AI能力,而归一化、转码、合成字母和视频文件则明显是CPU密集型的流程节点。流程节点对机器处理能力的需求各不相同。
实际业务中还可能会有内存密集型、单核运算型、多核运算型、只能跑在Windows下的、只能跑在Linux下的等等,多种的流程节点类型,想在一台机器上将这些特性全部囊括是不可能的,因此,每个流程节点都需要在单独的机器上执行,称为Worker。
而管理这一个流程在若干个Worker上运行,并恰当的使用Worker资源,就需要一个任务调度中心来完成。每个Worker上也需要部署一个Agent,以完成调度中心委派来的指令,启动对应的流程节点处理程序。
任务调度中心使用GO语言最为适合,若使用Java、Python等语言,难免会为了跟踪流程节点的执行状态而开海量的线程,使用go协程可以很好的降低这种负担。而Agent使用Python即可,简单方便。
详细设计
对于流程节点调度,可自行实现一个简易工作流框架,以完成调度。由于流程相对固定,所以可以一定程度上直接硬编码,不用像Flowable之类的流程引擎一样将流程可配置化,搞一堆xml。
基本的实现框架使用责任链模式
,不过不能照搬责任链模式,责任链模式会在当前机器上会流式的处理完所有的流程,中途没有停顿,无法加入流控与负载均衡等,并且两个独立流程间各个节点间的执行时序是随机的。
如:A,B流程是同一类流程,A流程有A1,A2两个流程节点,B流程有B1,B2两个流程节点,A流程先被执行,B流程后被执行。那么在责任链模式里,如果对A,B分别开一个线程处理流程,则A1,B1之间的执行顺序是随机的。此时假设全局的1号流程节点,同时只能处理一个,则希望A1的执行优先级高于B1,A2的优先级高于B2,责任链模式在此处无法胜任。最好在调度中心加入流控,并通过其他手段确保顺序性。
因此,具体实现时要对责任链模式做一些改动:为每个流程节点建立一个顺序队列,一个流程节点执行完成后,并非直接执行下一个流程节点,而是丢到下一个流程节点的顺序队列中。顺序队列消费时调用的方法,即流程节点在责任链里的Handler。
当然,还需要一个服务注册中心+监控中心,获取工作节点状态,也可以在调度中心里结合Worker上部署的Agent,自行实现一个工作节点注册中心+监控中心。最好使用后者,因为复杂任务调度过程中很有可能会有除CPU使用率,内存使用率等常规状态以外的状态,如工作节点正在处理的总视频长度、视频精度、字幕条数等。
前文提到的每个流程节点的顺序队列的消费时机,则需要新开一些线程,有多少工作节点类型,开多少线程,轮询此类型工作节点可处理的所有流程节点队列,若此类工作节点的最小占用的节点状态,满足流程节点队列首位的流程节点的资源需求,则开线程执行此流程节点,并追踪流程节点执行状态,直到投递到下一个流程节点队列或失败或结束。
另外,很可能存在流程前后两个流程节点对工作节点的类型需求一致,此时当然需要先执行流程后的流程节点,因此流程下游的流程节点执行优先级要高于流程上游的。
因此轮询流程节点时,要从下游的流程节点开始轮询,分配给一个工作节点或无法分配给任何工作节点时,则立刻continue,等待下一次轮询。
以上述流程图举例,此时可以转化为此时序图。
当然,实现时还要考虑若干详细逻辑,如
- 工作节点连接断开,或若干时间未发送心跳包,则将其视为假死,再过若干时间若还未收到心跳包,则将其视为死亡,需要发送短信通知运维,并将工作节点上的任务回收并重新放置在队首。因此,当然也需要记录工作节点当前正在处理的Job有哪些,方便回收任务,也可作为负载判断时的依据。
- 对于可能由于服务器状态波动等原因失败的Job,也需要考虑进行重试,但也不能无限重试。
- 如何判断负载量,最简单的,根据工作节点当前的Job数量判断,但稍显不足,因此需要统计能拿到的所有指标,并列一个任务各项指标-资源消耗的散点图,综合分析,得到一个负载预估算法,各个流程节点的负载算法也一定是不一样的。
- 对于不同类型的用户,任务处理的优先级也很有可能是不一样的,因此得考虑是否为重要用户单独设一个任务调度集群,或为每个任务加入优先级设定,优先级高的可在排入队列时随机或通过某种算法往前排一些。最好使用后者或两者一起使用,因为优先级算法在同一类流程的不同节点间也需要设置,流程下游的节点执行优先级要高于流程上游,否则在新任务不断进入系统的情况下,会将所有任务卡死在上游。
- 后面的流程节点可能会使用前面的流程节点的产出,因此每个流程类型的任务都需要定义一个Context,以存储流程节点的产出。
部署方案
分布式部署方案
分布式部署方案主要考虑任务调度中心的部署,Worker的部署只需要启动Agent即可。
任务调度中心的部署在业务量不大时,可简单通过竞争式的分布式锁做个主备即可,锁的释放时间可以适当设高一点,调度中心的主备切换意味着要重新同步一遍当前活动的所有Task状态,代价很高。提高锁的释放时间可以尽可能的减少误识别主节点死亡的可能性。并且任务调度中心整个都是离线异步处理的,对可用性要求不高,业务上也能接受一两分钟掉线。
而业务量大起来时,可参考Redis的集群部署方案,做个哈希环或哈希链+主备,哈希环上的每个节点可以都做一个备,以尽量避免Rehash,给集群内其它调度中心太多的负载。整个系统在设计时,每个Task一定有一个固定的ID,根据此ID做哈希环的散列轻而易举。
任务调度中心集群化后,Worker与任务调度中心之间的关系建议还是保持一对一的关系,即一个Worker一定只被一个调度中心管理,避免集群内的调度中心对同一Worker的负载统计不一致,导致给Worker过度分配任务而宕机。
云上部署方案
由于Worker上运行的基本全是资源密集型的进程,并且在此架构下,对Worker的可用性基本没有要求,因此可以考虑将调度中心放在云端,而Worker全部放在自建低级别机房里,或每种特性的Worker只在云端留一两份以确保所有类型的任务都能够处理。比起完全上云,成本可大幅缩减。
将Worker本地化还有一个额外的好处:配置可以自由搭配。
如:假设有CPU单核性能需求很高的流程节点,那么完全可以自购一批E52667。如果找云服务商,那不好意思没这种实例卖。再比如假设有多核计算密集型,但内存占用不大的流程节点,那完全可以核心:内存=1:1的配置机器,但如果找云服务商,那不好意思核心:内存最小1:2。
如果业务端与任务调度中心的交互可以全异步化,那么调度中心也可以放在自建机房里。更加节省成本。