在工作中接触到了几个版本的调度系统。
初版调度系统
调度分为三个模块:web模块,核心模块,插件模块。
web模块
web模块的前端是 使用了开源的Raphael.js https://github.com/topics/rap...去控制任务之间的依赖,通过拖拽的方式构建各种粒度的任务,通过箭头来表达任务之间的依赖。前端的任务详情页如图所示。每一个小的矩形都是一个最小的原子任务,原子任务可以是脚本、sql,文件路径,以及任何可以表达任务的信息,这些信息由后端的插件获取到去处理。图中这些矩形最后形成一个整体job,job之间又可以通过类似的方式表达依赖关系,最终成为一个巨大的有先后依赖关系的job集合。同时任务在执行的时候图中的矩形和箭头的颜色会随之该变,去标识任务执行到哪个阶段了。在中间某个任务执行失败的时候可以点击矩形从失败的位置开始往后去执行。
web模块还有任务的列表页,展示当前所有的任务和执行时间(crontab表达式)。
这是整个调度中需要存储的部分,每个原子单元有单独的表存储,有id,name,执行器,参数等字段。同时还会有job和task的映射关系表,在前端展示和任务执行的时候去找到对应的节点。
核心模块
这部分主要定义了前端的各级图形对应的bean类,任务编辑、保存、删除等操作的后端,抽象了任务初始化的各种共有方法,定义了调度系统的异常类,以及系统的scheduler部分(quartz)。
插件模块
调度系统为了支持多种开发的方式,所以单独开辟了一个插件模块。例如,想要执行一个shell脚本,那么对应的插件就要负责拿到shell脚本的路径,以及脚本的外部参数,通过线程池用Runtime.getRuntime().exec(cmd)去执行脚本。如果想要执行jdbc的sql语句,插件就实现前端传进来的数据库的连接和执行。执行hivesql的时候,插件会通过zk方式去拿到可用的hiveserver地址,连接hiveserver去执行对应的sql语句。同时还支持一些外部插件的二次封装,例如如sqoop的支持,需要将参数传进来,在通过代码启动宿主机上的sqoop去执行数据同步。
这部分会定义常用变量的表达式,例如离线数仓一般都会跑t-1的数据,那么就会定义一个变量去代替这个日期值,在后端拿到sql的时候将变量替换为具体的日期。
任务的失败成功报警也在这里实现,目前支持的报警方式有电话,短信,微信,自研IM报警等。
初版调度的问题
在任务量小的时候还好,任务量巨大的时候任务之间的依赖不好处理,往往会导致一个job中非常多的task节点,在某个task失败的时候影响下游很多任务。
误操多,前端是通过流程图的方式表达的依赖,操作起来很快,但是误操性很强。
任务的日志在系统看不到,想要看任务的失败原因只能通过登录宿主机或者实时日志收集到Es通过kibana的方式去查看。
插件的方式简单,但是容易泛滥,肆无忌惮的开发插件导致很多应该在业务中处理的逻辑定制化到了调度中,例如为了简洁开发了操作某个数据库的插件,那么在数据库机器过保需要换uri的时候就得改代码重发调度。
系统没有做拆分,所有的模块都在同一宿主机上部署,前后端没做分离。
所有任务的日志都在一个文件中,排查问题的时候要grep任务的关键字去看。
第二版调度
技术选型
- springboot +vue 框架
- cron表达式 用来配置任务定时运行周期,以及表达依赖关系
- postgresql 存储底层数据
- quartz 集群 定时触发正常调起任务的操作
- kafka 节点间异步通信
- 自研服务定时掉起轮询任务
系统架构图
服务端分为 Master和Slave两种角色;
Master负责任务创建保存修改,master启动定时任务去扫数据库,将即将开始要执行的任务加入队列启动一个任务实例,再有轮训的定时任务查询slave机器的资源负载等,满足条件的时候将任务调起,发送消息到salve;同时master提供web页面后端接口,任务的失败通知,超时任务处理等。
quartz负责任务定时管理太多任务,quartz库表和master库表分离;
Slave负责部署在客户机上,接收master的调起任务消息,负责在目标机器上调起job,交给部署在salve机器上的executor jar包去执行任务。同时开放本机资源信息接口和心跳接口供matser调用。
slave的机器按照不同的业务去分组,多个机器组对应不同业务线的调度任务物理隔离,将任务分发到各自的机器上执行,保证不会因为机器问题(跨机房)导致所以任务失败;
slave创建子进程,调起executor;
executor是实际执行任务的jar包,执行具体任务逻辑,同时将任务的最新状态通过消息通知master去更新master底表的任务状态。每个任务单独的打日志,存储在slave磁盘上,暴露接口给master,在master web上可以查看任务的执行log;
executor使用的是Runtime.getRuntime().exec(cmd)去执行任务,所有的业务逻辑必须封装到脚本中。
统一的任务启动脚本去调用具体的任务脚本,在启动脚本中source各种环境变量、固定参数,以及伪重试机制。
调度策略
- 从DB Queue中按照优先级等规则获取指定数量的task,根据task获取对应的符合条件的slave机器(内存不超过阈值,load不超过阈值)。
- 任务数量空闲时段不超过阈值, 繁忙时段不超过阈值。
- 对slave按照load,内存,任务梳理从小到大排序,对task从大到下排序,根据获取到的task平均资源进行排序
- 组合符合条件的slave与task, 一对一, 丢弃多余的task
- 任务通过消息分发
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。