本文结合源码,讲解了调度平台的一个调度请求,在执行器中如何被处理?
一 接收run请求
当调度平台向某个执行器发送run指令时,会被EmbedServer的内部类EmbedHttpServerHandler,使用线程池异步处理。
那么本次调度就成功触发了。
run请求的参数如下
{
"jobId": 1,
"executorHandler": "demoJobHandler",
"executorParams": "",
"executorBlockStrategy": "SERIAL_EXECUTION",
"executorTimeout": 0,
"logId": 2,
"logDateTime": 1728027839906,
"glueType": "BEAN",
"glueSource": "",
"glueUpdatetime": 1541254891000,
"broadcastIndex": 0,
"broadcastTotal": 1
}
二 向JobThread提交任务
接着会调用ExecutorBizImpl#run。
首先根据入参中jobId,从jobThreadRepository查找JobThread:每个任务体在执行器中,由固定的某个线程来运行。
// key是jobId
private static ConcurrentMap<Integer, JobThread> jobThreadRepository = new ConcurrentHashMap<Integer, JobThread>();
2.1 JobThread为null时
- 某任务第一次执行时,才会向
jobThreadRepository延迟注册一个JobThread。 - 根据入参中的executorHandler,从
ConcurrentMap<String, IJobHandler> jobHandlerRepository中查找任务体jobHandler(执行器启动中就初始化了)。 - 用jobId、jobHandler创建JobThread实例并注册到jobThreadRepository中。
- 调用
JobThread.pushTriggerQueue,将本次执行请求入队,向调度平台响应调度成功。
2.2 JobThread不为null时
JobThread不为null时,会增加对入参中executorBlockStrategy的处理。
当JobThread为null时,本次执行时会新建JobThread实例,也就不可能有积压的任务;也就不用做阻塞处理。
因为本次任务只是提交到JobThread的队列中,如果队列中有积压的任务,或者JobThread正在执行某个任务,本次请求并不会被立即执行,即被阻塞。
任务阻塞时,处理策略有三个:
使用Discard Later时,本次调度将失败。
使用Cover Early时,本次调度成功;队列中等待的任务将不会被执行,但是正在执行中的任务体,如果不能响应
interrupt,还是会被完整的执行掉。
三 JobThread类
3.1 创建实例
JobThread继承了Thread类,增加了以下属性:
执行器给每个任务,都会创建一个
JobThread实例。
即同一执行器上的不同任务间,是线程隔离的。不会因为任务A阻塞或异常,而影响任务B的执行。
3.2 线程任务
JobThread重写了run方法,主要流程如下:
注意这几点:
-
triggerQueue的尺寸是Integer.MAX_VALUE,相当于无界; -
为了防止一个任务的logId被并发使用,pushTriggerQueue中使用triggerLogIdSet做了校验:重复请求不入队,直接响应失败。
-
从队列获取任务时,使用3秒超时的
poll方法,因为take()在队列为空时将一直阻塞,导致不能及时检查toStop标志; -
idleTimes用于记录从队列中未获取到任务的次数,当idleTimes > 30即JobThread空闲超过30*3=90秒时,会将JobThread终结,以释放资源。 -
只要从队列中获得任务,就将idleTimes重置为0;
-
对于设置了超时时间的任务,会将任务体包装为FutureTask,创建一个新线程来执行,通过带超时的FutureTask.get,来检查任务是否超时。
-
没有超时时间的任务,在JobThread中运行;
注意,当一个任务的执行周期大于90秒时,每次执行,都得重新创建JobThread。
因为任务周期很长时,如1天执行一次;那一天之内,JobThread一直在空转,资源浪费严重。不如及时终止它,下次需要时再创建。
四 结果回调
4.1 结果入队
当JobThread处理完一个任务后,会将结果封装为HandleCallbackParam实例,调用TriggerCallbackThread.pushCallBack,添加到TriggerCallbackThread的callBackQueue中。
4.2 结果回调
TriggerCallbackThread是在XxlJobExecutor.start()中被创建并启动。
有两个守护线程:首次回调线程、失败重试线程
// 队列
private LinkedBlockingQueue<HandleCallbackParam> callBackQueue = new LinkedBlockingQueue<HandleCallbackParam>();
// 消费队列的线程
private Thread triggerCallbackThread;
// 回调失败重试线程
private Thread triggerRetryCallbackThread;
private volatile boolean toStop = false;
- callBackQueue.take()返回时,再调用drainTo获取队列中所有消息;
- 调用AdminBizClient.callback,向任意一个调度平台批量推送;
- 回调失败时,将消息记录到文件中,重试线程每30秒读取一次该文件,再次推送。
五 总结
- 任务的调度处理、执行、结果回调,是3个依次执行的独立过程,由不同的线程来运行;
- 执行器中,不同任务的执行,是线程隔离,避免互相影响;
- JobThread实例创建后会被缓存,当空闲超过90秒,就会被回收,在任务再次执行时再创建新实例;
- 当一个任务的执行周期,小于一次执行的平均耗时时,可能导致任务在执行器中积压;默认的阻塞策略是单机串行;
- 当阻塞策略使用Cover Early,正在执行中的旧任务,和本次新提交的任务,可能会存在并发;
- 任务体应当正确响应
interrupt; - 执行结果通过异步批量回调。