如官方文档《XXL-JOB架构图.pptx》中v2.1.0版本架构图,本文将结合源码,来介绍xxl-job中“注册服务”、“执行器管理”功能,并与eureka中服务注册进行比较。通过阅读本文,你将对注册中心、服务心跳保活有一定了解。
一、准备工作
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);
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修改启动参数端口,在一台机器上启动多个执行器实例,刷新页面后看到“机器地址”列展示了执行器地址。
现在,你是否有这些疑惑,接下来我们通过源码寻找答案。
- 在执行器启动后,如何注册到调度中心?
- 执行器下线或上线,调度中心如何及时感知?
- 执行器又是如何被调度的?
二、执行器注册
自动注册由执行器向调度平台发起,注册的地址列表由调度平台维护。
2.1 自动注册
使用springboot集成xxl-job时,在配置类中创建了XxlJobSpringExecutor实例,会触发afterSingletonsInstantiated()方法(类似于InitializingBean.afterPropertiesSet()),其中调用了XxlJobExecutor.start()。
// 简化后代码,在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()如下。
XxlJobExecutor.initAdminBizList()中,为每个xxl-job-admin(调度中心可以集群部署),创建一个rpc invoker即AdminBizClient。有3个接口实现,registry()在ExecutorRegistryThread中被使用,我们沿着调用链一步步深入。
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表新增记录。
当注册方式又修改为“自动注册”,将从xxl_job_registry表查询已注册执行器地址,逗号拼接后更新xxl_job_group表address_list字段。
三、调度中心处理注册
3.1 响应自动注册请求
在com.xxl.job.admin.controller.JobApiController.api()中,根据uri区别处理执行器发来的请求,转给AdminBizImpl。
// 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;
}
3.2 注册地址维护
在调度中心启动时,在XxlJobAdminConfig.afterPropertiesSet()中创建XxlJobScheduler,并调用init()完成环境初始化。
在JobRegistryMonitorHelper.start()中,启动守护线程每隔30秒(while + sleep实现)执行以下逻辑:
- 只处理“自动注册”的执行器;
- 删除xxl_job_registry表update_time在90秒前的记录;
- 查询xxl_job_registry表update_time在90秒内的记录;
- 更新xxl_job_group表address_list。
四、结论
- 执行器每隔30秒,向调度中心发起一次registry,每次只更新xxl_job_registry表update_time字段(首次除外);连续3次registry失败,会被调度中心从xxl_job_registry表删除;
- 无论执行器配置为“自动注册”或“手动注册”,执行器在线时都会向调度中心发起registry;
- 调度中心每30秒对注册方式为“自动注册”的执行器进行一次维护,删除下线的,更新xxl_job_group表address_list字段;
- 注册方式为“手动录入”时,执行器地址列表不会被定时维护;
- 执行器注册或下线时,不会立刻更新xxl_job_group表address_list字段;页面看到的“Online机器地址”有延迟;
- 执行器注册间隔、注册表维护间隔都是30秒,不可配置;
- 可见,调度中心是无状态的,注册信息保存在数据库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