xxl-job服务注册浅析

320 阅读7分钟

如官方文档《XXL-JOB架构图.pptx》中v2.1.0版本架构图,本文将结合源码,来介绍xxl-job中“注册服务”、“执行器管理”功能,并与eureka中服务注册进行比较。通过阅读本文,你将对注册中心、服务心跳保活有一定了解。 image.png

一、准备工作

1.1 启动调度中心

从GitHub下载源码后(本文使用2.2.0版本),执行SQL创建数据库表,修改或配置xxl-job-admin模块application.properties,然后就能启动调度中心。未修改port和context-path时访问地址为http://localhost:8080/xxl-job-admin ,默认登录账号 "admin/123456"。
源码地址:https://github.com/xuxueli/xxl-job.git

### 修改或使用默认配置
server.port=8080
server.servlet.context-path=/xxl-job-admin

### datasource,修改为自己的
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

启动后,在【执行器管理】中能看到一条记录(是由初始化SQL创建的)。

INSERT INTO `xxl_job_group`(`id`, `app_name`, `title`, `address_type`, `address_list`) VALUES (1, 'xxl-job-executor-sample', '示例执行器', 0, NULL);

image.png image.png

1.2 启动执行器

修改xxl-job-executor-samples/xxl-job-executor-sample-springboot项目中application.properties。

### 应用端口
server.port=8081

### 调度中心地址,可配置多个,如"http://address" or "http://address01,http://address02" 
### 执行器将会使用该地址进行"执行器心跳注册""任务结果回调";为空则关闭自动注册;
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin

### 执行器名称,选填,可以与spring.application.name不同。心跳注册分组依据;为空则关闭自动注册
xxl.job.executor.appname=xxl-job-executor-sample  
### 优先使用address自动注册,address为空时使用ip:port
xxl.job.executor.address=
### 执行器IP,选填,默认为空表示自动获取IP,多网卡时可手动设置指定IP
xxl.job.executor.ip=  
### 执行器端口号,默认端口为9999,单机部署多个执行器时,注意要配置不同的端口;
xxl.job.executor.port=9999  
### 执行器运行日志文件存储磁盘路径,需要对该路径拥有读写权限;为空则使用默认路径;
xxl.job.executor.logpath=D:/xxl-job
### 执行器日志文件保存天数,过期日志自动清理, 限制值大于等于3时生效; 否则关闭自动清理功能;
xxl.job.executor.logretentiondays=30

通过-Dserver.port、-Dxxl.job.executor.port修改启动参数端口,在一台机器上启动多个执行器实例,刷新页面后看到“机器地址”列展示了执行器地址。 image.png 现在,你是否有这些疑惑,接下来我们通过源码寻找答案。

  • 在执行器启动后,如何注册到调度中心?
  • 执行器下线或上线,调度中心如何及时感知?
  • 执行器又是如何被调度的?

二、执行器注册

自动注册由执行器向调度平台发起,注册的地址列表由调度平台维护。

2.1 自动注册

使用springboot集成xxl-job时,在配置类中创建了XxlJobSpringExecutor实例,会触发afterSingletonsInstantiated()方法(类似于InitializingBean.afterPropertiesSet()),其中调用了XxlJobExecutor.start()。 image.png

// 简化后代码,在XxlJobSpringExecutor实例创建后调用
public class XxlJobSpringExecutor extends XxlJobExecutor implements ApplicationContextAware, SmartInitializingSingleton {
    // start,类似于InitializingBean.afterPropertiesSet()
    @Override
    public void afterSingletonsInstantiated() {
        // init JobHandler Repository (for method)
        initJobHandlerMethodRepository(applicationContext);
        // refresh GlueFactory
        GlueFactory.refreshInstance(1);

        // 调用XxlJobExecutor.start()
        try {
            super.start();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
 }

XxlJobExecutor.start()如下。 image.png XxlJobExecutor.initAdminBizList()中,为每个xxl-job-admin(调度中心可以集群部署),创建一个rpc invoker即AdminBizClient。有3个接口实现,registry()在ExecutorRegistryThread中被使用,我们沿着调用链一步步深入。 image.png image.png image.png XxlJobExecutor.initEmbedServer()中,调用EmbedServer.start()启动执行器Server(使用Netty通信),然后调用EmbedServer.startRegistry()向调度中心发起注册。

    public void startRegistry(final String appname, final String address) {
        // start registry,新启线程
        ExecutorRegistryThread.getInstance().start(appname, address);
    }
// 移除校验、try-catch后简化的代码
  public void start(final String appname, final String address){
    registryThread = new Thread(new Runnable() {
      @Override
      public void run() {
        // 服务未停止时,持续注册
        while (!toStop) {
          RegistryParam registryParam = new RegistryParam(RegistryConfig.RegistType.EXECUTOR.name(), appname, address);
          // 在此之前已经创建了AdminBiz,向任一调度中心注册成功即可
          for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) {
            ReturnT<String> registryResult = adminBiz.registry(registryParam);
            if (registryResult!=null && ReturnT.SUCCESS_CODE == registryResult.getCode()) {
              registryResult = ReturnT.SUCCESS;
              break;
            } else {
              logger.info(">>>>>>>>>>> xxl-job registry fail, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult});
            }
          }

          if (!toStop) {
            // 睡眠30秒,即30秒续期一次
            TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT);
          }
        }

        // 服务未停止时,移除注册
        RegistryParam registryParam = new RegistryParam(RegistryConfig.RegistType.EXECUTOR.name(), appname, address);
        // 向任一调度中心请求成功即可
        for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) {
          ReturnT<String> registryResult = adminBiz.registryRemove(registryParam);
          if (registryResult!=null && ReturnT.SUCCESS_CODE == registryResult.getCode()) {
            registryResult = ReturnT.SUCCESS;
            logger.info(">>>>>>>>>>> xxl-job registry-remove success, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult});
            break;
          }
        }
      }
    });
    registryThread.setDaemon(true);
    registryThread.setName("xxl-job, executor ExecutorRegistryThread");
    registryThread.start();
  }

阅读以上代码,有如下结论:

  • 启一个守护线程,在执行器服务未停止时,每隔30秒(while + sleep实现)主动向调度中心注册;
  • 注册时,向调度中心集群任一节点注册成功即可,移除注册时也是;
  • 一旦执行器服务停止,不再注册而执行移除注册,然后退出线程。

2.2 手动注册

在页面修改执行器的注册方式为“手动录入”,填写执行器地址(多个之间英文逗号分隔),点击“保存”发现调用POST /jobgroup/update接口,将直接更新 xxl_job_group表记录,而不会在xxl_job_registry表新增记录。 image.png image.png 当注册方式又修改为“自动注册”,将从xxl_job_registry表查询已注册执行器地址,逗号拼接后更新xxl_job_group表address_list字段。 image.png

三、调度中心处理注册

3.1 响应自动注册请求

在com.xxl.job.admin.controller.JobApiController.api()中,根据uri区别处理执行器发来的请求,转给AdminBizImpl。 image.png

// AdminBizImpl.registry实现,简化后代码
@Override
public ReturnT<String> registry(RegistryParam registryParam) {
	// 更新xxl_job_registry表的update_time列
	int ret = xxlJobRegistryDao.registryUpdate(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date());
        // 首次注册时ret < 1,将新增记录
	if (ret < 1) {
        xxlJobRegistryDao.registrySave(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date());
	}
	return ReturnT.SUCCESS;
}

image.png

3.2 注册地址维护

在调度中心启动时,在XxlJobAdminConfig.afterPropertiesSet()中创建XxlJobScheduler,并调用init()完成环境初始化。 image.png

image.png 在JobRegistryMonitorHelper.start()中,启动守护线程每隔30秒(while + sleep实现)执行以下逻辑:

  1. 只处理“自动注册”的执行器;
  2. 删除xxl_job_registry表update_time在90秒前的记录;
  3. 查询xxl_job_registry表update_time在90秒内的记录;
  4. 更新xxl_job_group表address_list。

四、结论

  1. 执行器每隔30秒,向调度中心发起一次registry,每次只更新xxl_job_registry表update_time字段(首次除外);连续3次registry失败,会被调度中心从xxl_job_registry表删除;
  2. 无论执行器配置为“自动注册”或“手动注册”,执行器在线时都会向调度中心发起registry;
  3. 调度中心每30秒对注册方式为“自动注册”的执行器进行一次维护,删除下线的,更新xxl_job_group表address_list字段;
  4. 注册方式为“手动录入”时,执行器地址列表不会被定时维护;
  5. 执行器注册或下线时,不会立刻更新xxl_job_group表address_list字段;页面看到的“Online机器地址”有延迟;image.png
  6. 执行器注册间隔、注册表维护间隔都是30秒,不可配置;image.png
  7. 可见,调度中心是无状态的,注册信息保存在数据库xxl_job_group表,对调度中心集群部署时各节点共享。

五、对比eureka

5.1 相似点

  • xxl-job与eureka都涉及服务注册,因此架构上分为两端:注册中心(xxl-job中叫调度中心)和客户端(xxl-job中叫执行器),客户端与业务应用集成,服务端需单独部署。
  • 服务注册都是由客户端主动发起,并定时向注册中心发送心跳(也称续期、续约或注册),心跳超时会被认为该客户端服务已下线
  • 注册中心都提供了UI界面,并支持由人主动维护注册表(eureka提供了endpoint,需用HttpClient发送请求,而xxl-job可在页面编辑)

5.2 不同点

  • xxl-job将注册信息保存在数据库中,而eureka的注册表在内存中。因此eureka注册中心是有状态的,要实现集群模式下各节点信息同步(采用CAP理论中的AP模式,实现注册表的最终一致性);而xxl-job调度中心集群各节点间无需通信,所有信息都从数据库查询;
  • eureka客户端要定期从注册中心拉取最新的注册表,因此注册中心将注册表保存在内存中,以保证接口性能。而xxl-job中执行器不用读取调度中心的地址列表
# Eureka Client每隔多久从Eureka Server拉取一次服务列表,默认30秒
eureka.client.registry-fetch-interval-seconds
  • 虽然默认心跳周期都是30秒,默认心跳超时都是90秒(即3次心跳),但xxl-job中不支持配置,而eureka中支持配置;
# 在接收到上一个心跳之后等待下一个心跳的秒数(默认 90 秒),超过此时间则移除实例
eureka.instance.lease-expiration-duration-in-seconds
# 表示 Eureka Client 向 Eureka Server 发送心跳的频率(默认 30 秒)
eureka.instance.lease-renewal-interval-in-seconds
  • 对于注册地址,xxl-job调度中心每隔30秒维护一次,而eureka中默认60秒维护一次,但可配置。
# 表示 Eureka Server 清理无效节点的频率,默认 60000 毫秒(60 秒)
eureka.server.eviction-interval-timer-in-ms