1.1 启动时, 加载applicationcontext-xxl-job-admin.xml, 初始化spring容器
Spring容器在初始化xxlJobDynamicScheduler时, 会调用inti-method指定的init()方法.
1.2 xxlJobDynamicScheduler类init()分析
public void init() throws Exception {
// admin调度中心专门开启一个自动注册线程registryThread, 获取类型为自动注册的执行器信息,完成executor执行器的自动注册与发现
// 1 用来监控自动注册上来的机器,达到自动注册的目的
// 2 admin调度中心的自动注册就是, 将executor端每30秒心跳发送过来的address写入registry表,
// 然后就是开启一个自动registry线程, 每隔30s, 将registry表数据写入到group表.
// admin registry monitor run
JobRegistryMonitorHelper.getInstance().start();
// 启动失败日志监控线程
// 2 监控任务的执行状态, 如若失败,则发送邮件预警
// admin monitor run
JobFailMonitorHelper.getInstance().start();
NetComServerFactory.putService(AdminBiz.class, XxlJobDynamicScheduler.adminBiz);
NetComServerFactory.setAccessToken(accessToken);
// valid
Assert.notNull(scheduler, "quartz scheduler is null");
logger.info(">>>>>>>>> init xxl-job admin success.");
}
上述代码片段中, 主要是开启了两个线程:
JobRegistryMonitorHelper.getInstance().start(); // registryThread线程: 用于发现注册上来的executor执行器
JobFailMonitorHelper.getInstance().start(); // monitorThread线程: 查询数据库log表, 根据trigger_code字段值(200成功, 500失败), 用于监控executor执行器本次job执行是否成功.
NetComServerFactory.putService(AdminBiz.class, XxlJobDynamicScheduler.adminBiz); // 将Spring容器初始化的adminBizImpl对象放入本地缓存
NetComServerFactory.setAccessToken(accessToken);// 设置访问AccessToken
1.3 具体分析admin调度中心是如何实现对executor执行器暴露服务的自动发现 -- registry线程
public void start() {
registryThread = new Thread(new Runnable() {
@Override
public void run() {
while (!toStop) {
try {
// 查找自动注册的,address_type字段类型为0的即为自动注册的
// auto registry group
List groupList = XxlJobDynamicScheduler.xxlJobGroupDao.findByAddressType(0);
// groupList = [{"addressType":0,"appName":"xiongxianze-test1","id":2,"order":2,"title":"新分组,
// 注册方式:自动"}]>
logger.info("==>groupList : {}", JSON.toJSONString(groupList));
if (CollectionUtils.isNotEmpty(groupList)) {
// 删除 90秒之内没有更新信息的注册机器, 90秒没有心跳信息返回,代表机器已经出现问题,故移除
// remove dead address (admin/executor)
XxlJobDynamicScheduler.xxlJobRegistryDao.removeDead(RegistryConfig.DEAD_TIMEOUT);
// fresh online address (admin/executor)
HashMap> appAddressMap = new HashMap>();
// 正常心跳的executor端
List list = XxlJobDynamicScheduler.xxlJobRegistryDao.findAll(RegistryConfig.DEAD_TIMEOUT);
// list =
// [{"id":7,"registryGroup":"EXECUTOR","registryKey":"local-execute","registryValue":"10.0.254.5:9999","updateTime":1539711248000}]>
logger.info("==>fresh online address list:{}", JSON.toJSONString(list));
if (list != null) {
for (XxlJobRegistry item : list) {
// 判断该机器注册信息RegistryGroup ,RegistType 是否是EXECUTOR
if (RegistryConfig.RegistType.EXECUTOR.name().equals(item.getRegistryGroup())) {
// appName = local-execute
String appName = item.getRegistryKey();
// 获取注册的执行器 KEY (也就是执行器)
List registryList = appAddressMap.get(appName);
// registryList1 = null
logger.info("==>registryList1 :{}", JSON.toJSONString(registryList));
if (registryList == null) {
registryList = new ArrayList();
}
if (!registryList.contains(item.getRegistryValue())) {
registryList.add(item.getRegistryValue());
}
// registryList2 = ["10.0.254.5:9999"]
logger.info("==>registryList2 :{}", JSON.toJSONString(registryList));
// local-execute : ["10.0.254.5:9999"]
appAddressMap.put(appName, registryList);
}
}
}
// fresh group address
for (XxlJobGroup group : groupList) {
// xiongxianze-test1
// 从这里就可以看出来, xxl.job.executor.appname properties文件中该项配置,
// 必须与admin端管理执行器时AppName一致
List registryList = appAddressMap.get(group.getAppName());
String addressListStr = null;
// 判断是否已经有了address, 有了的话, 使用"," 拼接addressList
if (CollectionUtils.isNotEmpty(registryList)) {
Collections.sort(registryList);
addressListStr = StringUtils.join(registryList, ",");
}
// 所以addressListStr = null
group.setAddressList(addressListStr);
// group = {"addressType":0,"appName":"xiongxianze-test1","id":2,"order":2,"title":"新分组,
// 注册方式:自动"}>
logger.info("==> group : {}", JSON.toJSONString(group));
XxlJobDynamicScheduler.xxlJobGroupDao.update(group);
}
}
} catch (Exception e) {
logger.error("job registry instance error:{}", e);
}
try {
TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT);
} catch (InterruptedException e) {
logger.error("job registry instance error:{}", e);
}
}
}
});
registryThread.setDaemon(true);
registryThread.start();
}
上述代码片段主要的流程就是:
①XxlJobDynamicScheduler.xxlJobGroupDao.findByAddressType(0);//从数据库中查询address_type=0(即自动注册)的全部分组信息
②XxlJobDynamicScheduler.xxlJobRegistryDao.removeDead(RegistryConfig.DEAD_TIMEOUT);//删除数据库中该xxl_job_qrtz_trigger_registry表中已经过期, 没有进行心跳注册的executor执行器
③XxlJobDynamicScheduler.xxlJobRegistryDao.findAll(RegistryConfig.DEAD_TIMEOUT);//查找全部正常心跳注册上来的executor执行器
④if (RegistryConfig.RegistType.EXECUTOR.name().equals(item.getRegistryGroup())) //判断是否是executor执行器
appAddressMap.put(appName, registryList);//这里的appname指的是executor执行器在xxl-job-executor.properties文件中配置的xxl.job.executor.appname=xxl-job-executor-sample, 而registryList即使每个executor执行器的ip和executor启动jetty server端口号组成, 端口号由配置文件中的xxl.job.executor.port=9999决定.
⑤ 接着的这段代码就是, 循环判断admin调度中心group分组中是否有与executor执行器appname相同的名称.
// fresh group address
for (XxlJobGroup group : groupList) {
// xiongxianze-test1
// 从这里就可以看出来, xxl.job.executor.appname properties文件中该项配置,
// 必须与admin端管理执行器时AppName一致, 因为这里是根据从数据库的group表的appname作为key,
// 去取executor执行器端的放入缓存中appname
List
String addressListStr = null;
// 判断是否已经有了address, 有了的话, 使用"," 拼接addressList
if (CollectionUtils.isNotEmpty(registryList)) {
Collections.sort(registryList);
addressListStr = StringUtils.join(registryList, ",");
}
// 所以addressListStr = null
group.setAddressList(addressListStr);
// group = {"addressType":0,"appName":"xiongxianze-test1","id":2,"order":2,"title":"新分组,
// 注册方式:自动"}>
logger.info("==> group : {}", JSON.toJSONString(group));
XxlJobDynamicScheduler.xxlJobGroupDao.update(group);
}
这里的关键代码就是, XxlJobDynamicScheduler.xxlJobGroupDao.update(group); 将executor执行器端注册上来相同的xxl.job.executor.appname值, 放入了同一个组, 这样admin调度中心, 就能从这个group组中选择不同的调度策略调度本次执行的job. 同时也说明了这几个executor执行器, 对应的是调度同一个job, 这样admin调度中心就能选择相应的调度策略调度job, 比如:first第一个executor执行本次job, last最后一个executor执行器执行本次调度, random随机选择一个executor执行进行本次调度, 或者是LFU最不经常, LRU最近最久未使用的执行器执行本次调度.
到这里admin调度中心就实现了executor执行器注册上来的服务暴露地址的自动发现.
1.4 具体分析admin调度中心是如何实现监控executor执行器执行结果的 -- monitorThread线程
@Override
public void run() {
// monitor
while (!toStop) {
try {
// 从缓存队列中获取数据, jogLogId
Integer jobLogId = JobFailMonitorHelper.instance.queue.take();
if (jobLogId != null && jobLogId > 0) {
// 根据joblogid从xxl_job_qrtz_trgger_log表中获取job执行的log
XxlJobLog log = XxlJobDynamicScheduler.xxlJobLogDao.load(jobLogId);
if (log != null) {
// log.getHandleCode()默认值为0, 而且返回值200, job正在运行中
if (ReturnT.SUCCESS_CODE == log.getTriggerCode() && log.getHandleCode() == 0) {
// job running, wait + again monitor
TimeUnit.SECONDS.sleep(10);
JobFailMonitorHelper.monitor(jobLogId);
logger.info(">>>>>>>>>>> job monitor, job running, JobLogId:{}", jobLogId);
}
// job成功执行
if (ReturnT.SUCCESS_CODE == log.getTriggerCode()
&& ReturnT.SUCCESS_CODE == log.getHandleCode()) {
// job success, pass
logger.info(">>>>>>>>>>> job monitor, job success, JobLogId:{}", jobLogId);
}
// job执行失败
if (ReturnT.FAIL_CODE == log.getTriggerCode()
|| ReturnT.FAIL_CODE == log.getHandleCode()) {
// job fail,
// 执行失败, 发送告警邮件
sendMonitorEmail(log);
logger.info(">>>>>>>>>>> job monitor, job fail, JobLogId:{}", jobLogId);
}
}
}
} catch (Exception e) {
logger.error("job monitor error:{}", e);
}
}
// monitor all clear
List jobLogIdList = new ArrayList();
int drainToNum = getInstance().queue.drainTo(jobLogIdList);
if (jobLogIdList != null && jobLogIdList.size() > 0) {
for (Integer jobLogId : jobLogIdList) {
XxlJobLog log = XxlJobDynamicScheduler.xxlJobLogDao.load(jobLogId);
if (ReturnT.FAIL_CODE == log.getTriggerCode() || ReturnT.FAIL_CODE == log.getHandleCode()) {
// job fail,
sendMonitorEmail(log);
logger.info(">>>>>>>>>>> job monitor last, job fail, JobLogId:{}", jobLogId);
}
}
}
}
①JobFailMonitorHelper.instance.queue.take(); // 首先monitor线程会从queue队列中取出已经logJobId, 而这个jobLogId是什么时候放入queue队列中的呢? 是XxlJobTrigger类在执行trigger()进行一次调度时, 写入的, 如下:
当执行本次调度时, 其实是走了一遍rpc远程调用, 得到执行结果200或者500状态, 而monitor监控线程就是根据该状态判断本次调度是否成功了.
②monitor线程再根据jobLogId查询数据库, 判断本次调度job执行结果, trigger_code=200成功, 则在控制台打印本次调度执行成功.
否则就发送告警邮件.
以上分析的只是admin调度中心初始化时, 所做的一些初始化工作, 一是, 开启一个自动发现executor执行器服务的线程, 并写入xxl-job-qrtz-trigger-group表, 二是, 再开启一个monitor监控线程, 用于监控executor执行器执行本次job的执行结果.
下一篇将解析admin调度中心具体的调度过程, 调度策略以及rpc通信的实现.