01 引言
前面已经介绍了XXL-JOB
的整体架构以及主要的组件。
XXL-JOB作为一款广泛应用的分布式任务调度框架,通过其精巧的架构设计和高效的通信机制,为跨服务调用提供了标准化的解决方案。
在分布式系统中,任务调度作为协调跨服务协作的核心组件,面临着服务发现、通信可靠性、负载均衡等诸多挑战。尤其在微服务架构下,不同业务模块往往独立部署,如何高效、安全地实现跨服务任务触发与执行,成为保障系统弹性和可扩展性的关键。
本文将揭秘XXL-JOB实现跨服务调用的核心原理,揭示其如何借助服务注册、动态调度及RPC通信等技术,构建高可用的分布式任务调度体系。
02 通讯模块剖析
2.1 完整的任务调度通讯流程
- 「调度中心」向「执行器」发送http调度请求
- 「执行器」中接收请求的服务,实际上是一台内嵌Server,默认端口9999,使用的基于Netty的WebSocket;
- 「执行器」执行任务逻辑
- 「执行器」http回调「调度中心」调度结果
- 「调度中心」中接收回调的服务,是针对执行器开放一套API服务
上面的流程摘自官网。
简单来说:
「调度中心」向「执行器」发送http调度请求,「执行器」使用WebSocket接收并处理,最后返回结果。
2.2 通讯数据加密
调度中心向执行器发送的调度请求时使用RequestModel
和ResponseModel
两个对象封装调度请求参数和响应数据, 在进行通讯之前底层会将上述两个对象对象序列化,并进行数据协议以及时间戳检验,从而达到数据加密的功能。
03 调度中心源码追踪
我们以手动发起一项调度任务为例,其访问路劲为:/jobinfo/trigger
3.1 源码链路
从源码链路来看,最终通过工具类发起了POST
请求。
3.2 链路的入口
com.xxl.job.admin.controller.JobInfoController#triggerJob
java
@RequestMapping("/trigger")
@ResponseBody
public ReturnT<String> triggerJob(HttpServletRequest request,
@RequestParam("id") int id,
@RequestParam("executorParam") String executorParam,
@RequestParam("addressList") String addressList) {
// login user
XxlJobUser loginUser = PermissionInterceptor.getLoginUser(request);
// trigger
return xxlJobService.trigger(loginUser, id, executorParam, addressList);
}
3.3 关键源码解析-线程池调用
com.xxl.job.admin.core.thread.JobTriggerPoolHelper#addTrigger
通过线程池调用com.xxl.job.admin.core.trigger.XxlJobTrigger#trigger
3.4 关键源码解析-获取执行器的客户端
com.xxl.job.admin.core.scheduler.XxlJobScheduler#getExecutorBiz
最终获取的客户端:com.xxl.job.core.biz.client.ExecutorBizClient
3.5 关键源码解析-构建Http请求
com.xxl.job.core.biz.client.ExecutorBizClient#run
java
@Override
public ReturnT<String> run(TriggerParam triggerParam) {
return XxlJobRemotingUtil.postBody(addressUrl + "run", accessToken, timeout, triggerParam, String.class);
}
com.xxl.job.core.util.XxlJobRemotingUtil#postBody
最终客户端也就是我们的调度中心发起一个Http请求:
04 执行器源码追踪
执行器在属于xxl-job-core
中的模块,是集成在业务项目中的。大胆猜想,既然调度中心发起了Http请求,执行器中应该有对应的请求路径,才能完成闭环。
然后,经过在源码中一顿搜索,并没有对应的映射路径。只有一个内置的Server,这也就是官方说内置的Server。
com.xxl.job.core.server.EmbedServer
4.1 源码链路
执行器的源码入口,我们暂且放一放,后面再介绍。先从上面搜索下图开始:
链路图如下:
4.2 关键源码解析-构建线程
com.xxl.job.core.biz.impl.ExecutorBizImpl#run
没有线程就直接创建线程:
java
public static JobThread registJobThread(int jobId, IJobHandler handler, String removeOldReason){
// 创建JOB线程,并启动
JobThread newJobThread = new JobThread(jobId, handler);
newJobThread.start();
logger.info(">>>>>>>>>>> xxl-job regist JobThread success, jobId:{}, handler:{}", new Object[]{jobId, handler});
// putIfAbsent | oh my god, map's put method return the old value!!!
JobThread oldJobThread = jobThreadRepository.put(jobId, newJobThread);
if (oldJobThread != null) {
oldJobThread.toStop(removeOldReason);
oldJobThread.interrupt();
}
return newJobThread;
}
4.3 关键源码解析-推送参数到队列
com.xxl.job.core.thread.JobThread
将请求参数推动到自己的阻塞队列。
java
public ReturnT<String> pushTriggerQueue(TriggerParam triggerParam) {
// avoid repeat
if (triggerLogIdSet.contains(triggerParam.getLogId())) {
logger.info(">>>>>>>>>>> repeate trigger job, logId:{}", triggerParam.getLogId());
return new ReturnT<String>(ReturnT.FAIL_CODE, "repeate trigger job, logId:" + triggerParam.getLogId());
}
triggerLogIdSet.add(triggerParam.getLogId());
triggerQueue.add(triggerParam);
return ReturnT.SUCCESS;
}
4.4 关键源码解析-消费队列信息
com.xxl.job.core.thread.JobThread#run
该线程在创建的时候,就已经运行起来了,详见4.3
。
从上面源码中可以看到,线程启动后,运行着一个带有开关的死循环线程。每隔3s从阻塞队列中拉取一次信息。
拉取到参数之后,如果未超时,则启动异步线程调用业务方法handler.execute()
,最后异步等待结果,但是这个结果后续没有再用到,也不重要。
自此执行器的链路就执行完成了。
4.5 链路的入口
com.xxl.job.core.biz.impl.ExecutorBizImpl#run
这个是执行器处理的入口。但是执行器是如何监听或保证此方法能够被执行的呢?
这也是执行器的原始入口。我们采用反推的方式,一步步探索链路的真正的入口。
反推链路:
从反推链路来看,其实真正的链路入口是:com.xxl.job.core.executor.XxlJobExecutor#start
。只要项目调用了此
方法即可初始化链路入口。
还记得我们定义执行器的配置的么?
再定义Bean
时,指定了initMethod
的初始化方法。项目启动后,Spring
容器 会自动初始化。
但是这个initMethod
有没有必要呢?官方文档中并没有此方法,我们也来看下为什么可以?这个就要看XxlJobSpringExecutor
的继承关系了。
XxlJobSpringExecutor
实现了Spring
的扩展接口org.springframework.beans.factory.InitializingBean
,被修饰的Bean被实例化后会自动调用afterPropertiesSet()
方法,该方法就会调用start()
方法。
start()方法的魔力:
为什么调用start()
方法之后就会,就能够自动接收客户端的请求呢?
因为com.xxl.job.core.server.EmbedServer
使用了基于netty
的长连接WebSocket
。
接收请求的核心代码块:
java
@Override
protected void channelRead0(final ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
// request parse
//final byte[] requestBytes = ByteBufUtil.getBytes(msg.content()); // byteBuf.toString(io.netty.util.CharsetUtil.UTF_8);
String requestData = msg.content().toString(CharsetUtil.UTF_8);
String uri = msg.uri();
HttpMethod httpMethod = msg.method();
boolean keepAlive = HttpUtil.isKeepAlive(msg);
String accessTokenReq = msg.headers().get(XxlJobRemotingUtil.XXL_JOB_ACCESS_TOKEN);
// invoke
bizThreadPool.execute(new Runnable() {
@Override
public void run() {
// do invoke:处理请求的核心
Object responseObj = process(httpMethod, uri, requestData, accessTokenReq);
// to json
String responseJson = GsonTool.toJson(responseObj);
// write response
writeResponse(ctx, keepAlive, responseJson);
}
});
}
小结 :执行器在业务项目启动时,会建立WebSocket
连接,通过channelRead0
等待请求来临,请求过来之后,就会调用process()
方法完成后续处理。
05 思考
XXL-JOB
没有借助任何第三方复杂的RPC框架,采用了自研的轻量级的RPC调用。既然调度中心发起Http请求,执行器为什么不通过普通的映射的关系(Controller)来接收呢?
普通的请求映射,是一次完整的Http协议的请求,每一次请求都需要三次握手四次挥手来完成一次请求。调度中心有大量的调度任务,采用Http协议的话,就会频繁的占用服务器资源,严重的话可能会拖垮业务项目。这也许就是框架本身性能强劲的一个重要原因。
END
喜欢就点赞收藏,也可以关注我的微信公众号:【编程朝花夕拾】