Azkaban系列:
前言
Azkaban是不错的调度系统,主要分为WebServer和ExecutorServer两部分,本文主要就WebServer的架构和设计做一些介绍
注:由于项目细节较多,本文主要介绍其中关键技术点,其他细节方面,读者可以自行阅读项目源码
相关技术
Azkaban整个项目使用的项目都比较古早,具体列表如下:
分类 | 技术 |
---|---|
前端 | jQuery |
MVC | Serlet + Velocity |
服务器 | Jetty |
ORM | QueryRunner |
容器管理 | Guice |
数据库连接池 | dbcp2 |
数据库 | mysql/h2 |
以上技术,大部分是比较老的技术,当然,技术没有好坏,我们的关注点还是主要放到架构上。
Azkaban整体架构
整体架构如下
WebServer架构
看完整体架构,我们来分析一下WebServer的架构设计
首先,从我们的视角出来,来看下WebServer应该具备什么样的能力
WebServer应该具备的能力
- Project管理能力
- 定时触发任务能力
- 即时触发任务能力
- 任务重试能力
WebServer能力全景图
本人总结WebServer能力全景图如下:
WebServer如何实现关键能力
Project管理能力
- project设计
有一张project表专门用于记录项目信息,在azkaban中,管理都是以项目为基础,然后扩展到定时任务、执行任务等。
具体这里不赘述了,azkaban的元数据设计并不复杂,感兴趣的同学可自行参考源码,链接地址参考「附录」部分。
定时触发任务能力
-
定时触发的原理
相信大家都使用过linux的crontab来做定时任务,我来会回顾一下crontab的工作原理
首先,先将定时任务command写入crontab,linux系统会有一个crond守护进程定时轮询,判断任务列表中是否有任务需要执行,有即执行,没有则等待下一个执行周期
azkaban的实现方式正是采用类似的原理
-
Azkaban的实现方式
Azkaban的定时触发正是跟linux的crontab同样的原理,这里我们重点讲下守护进程的实现
-
守护线程
Azkaban的守护进程实现在类「TriggerScannerThread」中,简单看下这个类
jsprivate class TriggerScannerThread extends Thread { // 轮询周期,默认是60s private final long scannerInterval; // 任务队列 private final BlockingQueue<Trigger> triggers; // 是否停止 private boolean shutdown = false; public TriggerScannerThread(final long scannerInterval) { // 初始化 } public void shutdown() { // 停止服务 } public void addTrigger(final Trigger t) { // 新增任务 } public void deleteTrigger(final Trigger t) { // 删除任务 } @Override public void run() { // 定期轮询,检测哪些任务需要执行 } private void checkAllTriggers() throws TriggerManagerException { // 检测所有任务 } private void onTriggerTrigger(final Trigger t) throws TriggerManagerException { // 触发任务执行 } private void onTriggerPause(final Trigger t) throws TriggerManagerException { // 停止任务 } private class TriggerComparator implements Comparator<Trigger> { // 任务优先级比较 } }
主要的成员变量就是scannerInterval、triggers
scannerInterval控制每次轮询的周期,默认是1分钟
triggers是任务的队列,用于存储任务
主要方法就是run(),这里细讲下run方法
jspublic void run() { while (!this.shutdown) { synchronized (TriggerManager.this.syncObj) { try { //获取当前时间 TriggerManager.this.lastRunnerThreadCheckTime = System.currentTimeMillis(); TriggerManager.this.scannerStage = "Ready to start a new scan cycle at " + TriggerManager.this.lastRunnerThreadCheckTime; try { // 检测所有任务是否达到执行条件 checkAllTriggers(); } catch (final Exception e) { e.printStackTrace(); logger.error(e.getMessage()); } catch (final Throwable t) { t.printStackTrace(); logger.error(t.getMessage()); } TriggerManager.this.scannerStage = "Done flipping all triggers."; TriggerManager.this.runnerThreadIdleTime = this.scannerInterval - (System.currentTimeMillis() - TriggerManager.this.lastRunnerThreadCheckTime); // 过载保护:如果任务的执行时间超过1分钟,打印错误信息 if (TriggerManager.this.runnerThreadIdleTime < 0) { logger.error("Trigger manager thread " + this.getName() + " is too busy!"); } else { // 当前线程等待「scannerInterval - 触发任务花费时间」再执行 TriggerManager.this.syncObj.wait(TriggerManager.this.runnerThreadIdleTime); } } catch (final InterruptedException e) { logger.info("Interrupted. Probably to shut down."); } } } }
这里采用wait()来让当前线程等待,等待时间就是「scannerInterval - 触发任务花费时间」,以确保定时任务可以每间隔相同的「scannerInterval」周期来执行
注意,这里有一个过载保护,当触发所有任务花费时间大于「scannerInterval」时,这里会打印错误;当然,这里我认为应该报警,当任务量大时,容易导致大面积任务延迟。
-
-
Schedule和Trigger
-
Schedule
Schedule主要任务定时的配置信息,包括cron表达式等,Schedule可与 Trigger进行转换
-
Trigger
Trigger可以理解是一个触发器,主要包括:执行操作「TriggerAction」、触发条件「Condition」、任务状态「TriggerStatus」以及其他配置信息
在守护线程「TriggerScannerThread」中,就是基于「Trigger」来触发任务的
jsprivate void checkAllTriggers() throws TriggerManagerException { // 遍历所有任务 for (final Trigger t : this.triggers) { try { TriggerManager.this.scannerStage = "Checking for trigger " + t.getTriggerId(); // 只执行任务状态为READY的任务 if (t.getStatus().equals(TriggerStatus.READY)) { if (t.getExpireCondition().getExpression().contains("EndTimeChecker") && t .expireConditionMet()) { // 停止任务 onTriggerPause(t); } else if (t.triggerConditionMet()) { // 如果达到触发条件,触发任务 onTriggerTrigger(t); } } if (t.getStatus().equals(TriggerStatus.EXPIRED) && t.getSource().equals("azkaban")) { // 移除任务 removeTrigger(t); } else { // 更新任务的下次执行时间 t.updateNextCheckTime(); } } catch (final Throwable th) { //skip this trigger, moving on to the next one logger.error("Failed to process trigger with id : " + t, th); } } }
这里需要注意两个关键的方法triggerConditionMet()、onTriggerTrigger(),这两个是关键方法
triggerConditionMet():实际调用的是Condition.isMet()来判断当前是否达到任务触发条件,那这个触发条件如何实现判断呢?大致的流程如下:
-
步骤一:使用quartz来解析crontab表达式(没错,azkaban中解析cron表达式正是使用的quartz),这样就能获取下次执行的时间,并记录下来;
-
步骤二:将当前时间跟「步骤一」的执行时间对比,如果当前时间>执行时间,则返回true,否则返回false;
-
步骤三:重新计算下次执行时间,并记录下来;然后重复「步骤二」
onTriggerTrigger():执行当前任务,具体调用的TriggerAction.doAction()方法,所以对于不同类型的定时任务,具体执行的Action由定时任务的类型决定,也可以自行扩展。
-
-
Cron表达式
Azkaban的corn表达式解析使用的quartz的包来解析,主要用于获取下次执行时间
quartz支持的cron表达式可参考官网:www.quartz-scheduler.net/documentati...
即时触发任务能力
即时触发任务比较简单,直接将任务提交给Executor执行即可,这部分等讲解Executor架构时会详细讲到。
任务重试能力
任务重试也比较简单,每个任务增加一个最大重试次数的属性,每次失败,判断当前是否达到重试最大次数即可判断是重试还是将任务置成失败。
WebServer不足以及解决方案
-
WebServer单点问题
-
问题描述
基于当前WebServer设计架构,WebServer是单点的,这样WebServer一旦出现宕机等问题,整个系统就无法工作了。
-
解决方案
解决方案也不是没有,有两个思路:
1、HA方案:通过zookeeper,建立一套HA方案,当主WebServer宕机时,将备WebServer作为主机,继续提供服务;
2、分治思路:将WebServer扩展成水平扩展的服务实例,每个WebServer分摊其中一部分任务,每个WebServer作为一个小集群,能够做到水平扩展;
-
-
任务分配机制待完善
- 问题描述
当前Azkaban WebServer对任务的分配机制还比较简单,在某些情况下,容易出现长时间任务堆积的任务,对系统的健康度十分不友好
- 解决方案
1、任务槽位:每个Executor应该对应的槽位,超过槽位将不再分配任务;
2、任务画像:对任务构建一套任务画像,基于任务画像给任务分配权重,避免过多的任务集中在某些机器或者集群;
总结
总结几点
-
架构方面 在架构方面,Azkaban的可扩展性已经能够满足日常的任务调度系统,而且架构设计、代码设计均是不错的。
-
扩展性方面 在Executor扩展性方面,Azkaban是可以的,但对于WebServer的扩展性,还是不足的;在应用到大规模任务调度场景中,将会略显吃力。
先总结到这里,有意可交流。
如有疏漏,欢迎不吝赐教(可评论留言)。
附录
azkaban官网:azkaban.github.io/
azkaban github地址:github.com/azkaban/azk...
azkaban快速入手:azkaban.readthedocs.io/en/latest