XXL-JOB:揭秘跨服务调用

01 引言

前面已经介绍了XXL-JOB 的整体架构以及主要的组件。

XXL-JOB作为一款广泛应用的分布式任务调度框架,通过其精巧的架构设计和高效的通信机制,为跨服务调用提供了标准化的解决方案。

在分布式系统中,任务调度作为协调跨服务协作的核心组件,面临着服务发现、通信可靠性、负载均衡等诸多挑战。尤其在微服务架构下,不同业务模块往往独立部署,如何高效、安全地实现跨服务任务触发与执行,成为保障系统弹性和可扩展性的关键。

本文将揭秘XXL-JOB实现跨服务调用的核心原理,揭示其如何借助服务注册、动态调度及RPC通信等技术,构建高可用的分布式任务调度体系。

02 通讯模块剖析

2.1 完整的任务调度通讯流程

  1. 「调度中心」向「执行器」发送http调度请求
  2. 「执行器」中接收请求的服务,实际上是一台内嵌Server,默认端口9999,使用的基于Netty的WebSocket;
  3. 「执行器」执行任务逻辑
  4. 「执行器」http回调「调度中心」调度结果
  5. 「调度中心」中接收回调的服务,是针对执行器开放一套API服务

上面的流程摘自官网。

简单来说

「调度中心」向「执行器」发送http调度请求,「执行器」使用WebSocket接收并处理,最后返回结果。

2.2 通讯数据加密

调度中心向执行器发送的调度请求时使用RequestModelResponseModel两个对象封装调度请求参数和响应数据, 在进行通讯之前底层会将上述两个对象对象序列化,并进行数据协议以及时间戳检验,从而达到数据加密的功能。

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请求:

http://ip:port/run

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


喜欢就点赞收藏,也可以关注我的微信公众号:【编程朝花夕拾】

相关推荐
JavaPub-rodert1 小时前
golang 的 goroutine 和 channel
开发语言·后端·golang
TFHoney2 小时前
Java面试第十一山!《SpringCloud框架》
java·spring cloud·面试
ivygeek2 小时前
MCP:基于 Spring AI Mcp 实现 webmvc/webflux sse Mcp Server
spring boot·后端·mcp
Hi-Jimmy2 小时前
【VolView】纯前端实现CT三维重建-CBCT
前端·架构·volview·cbct
日暮南城故里3 小时前
Java学习------初识JVM体系结构
java·jvm·学习
GoGeekBaird3 小时前
69天探索操作系统-第54天:嵌入式操作系统内核设计 - 最小内核实现
后端·操作系统
鱼樱前端3 小时前
Java Jdbc相关知识点汇总
java·后端
canonical_entropy4 小时前
NopReport示例-动态Sheet和动态列
java·后端·excel
kkk哥4 小时前
基于springboot的母婴商城系统(018)
java·spring boot·后端
王者鳜錸4 小时前
四、小白学JAVA-石头剪刀布游戏
java·开发语言·游戏