分布式定时任务系列14:XXL-job的注册模型

传送门

分布式定时任务系列1:XXL-job安装

分布式定时任务系列2:XXL-job使用

分布式定时任务系列3:任务执行引擎设计

分布式定时任务系列4:任务执行引擎设计续

分布式定时任务系列5:XXL-job中blockingQueue的应用

分布式定时任务系列6:XXL-job触发日志过大引发的CPU告警

分布式定时任务系列7:XXL-job源码分析之任务触发

分布式定时任务系列8:XXL-job源码分析之远程调用

分布式定时任务系列9:XXL-job路由策略

分布式定时任务系列10:XXL-job源码分析之路由策略

番外篇:从XXL-job路由策略的"服务容错"说起

分布式定时任务系列12:XXL-job的任务触发为什么是死循环?

分布式定时任务系列13:死循环是任务触发的银弹?

Java并发编程实战1:java中的阻塞队列

从XXL-job的架构设计说起

《分布式定时任务系列8:XXL-job源码分析之远程调用》章节中通过源码分析了XXL-job的任务调度流程,即XXL-job的定时任务是如何从调度中心触发让客户端执行!

并据此引用了官网的架构图,如下所示:

整体系统架构可以分为两部分,调度中心、执行器:

  • 调度模块(调度中心)
    负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统性能不再受限于任务模块;
    支持可视化、简单且动态的管理调度信息,包括任务新建,更新,删除,GLUE开发和任务报警等,所有上述操作都会实时生效,同时支持监控调度结果以及执行日志,支持执行器Failover。
  • 执行模块(执行器)
    负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效;
    接收"调度中心"的执行请求、终止请求和日志请求等。

调度中心执行器 之间的交互就是通过其中的**自研RPC(xxl-rpc)**模块。一次完整的任务调度通讯流程:

  1. - 1、"调度中心"向"执行器"发送http调度请求: "执行器"中接收请求的服务,实际上是一台内嵌Server,默认端口9999;
  2. - 2、"执行器"执行任务逻辑;
  3. - 3、"执行器"http回调"调度中心"调度结果: "调度中心"中接收回调的服务,是针对执行器开放一套API服务;

接着又在《分布式定时任务系列10:XXL-job源码分析之路由策略》章节中通过源码分析了XXL-job任务调度的路由策略:

路由策略:当执行器集群部署时,提供丰富的路由策略,包括:

  1. 路由策略:当执行器集群部署时,提供丰富的路由策略,包括;
  2. FIRST(第一个):固定选择第一个机器;
  3. LAST(最后一个):固定选择最后一个机器;
  4. ROUND(轮询):;
  5. RANDOM(随机):随机选择在线的机器;
  6. CONSISTENT_HASH(一致性HASH):每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上。
  7. LEAST_FREQUENTLY_USED(最不经常使用):使用频率最低的机器优先被选举;
  8. LEAST_RECENTLY_USED(最近最久未使用):最久未使用的机器优先被选举;
  9. FAILOVER(故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度;
  10. BUSYOVER(忙碌转移):按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度;
  11. SHARDING_BROADCAST(分片广播):广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务;
  12. - 子任务:每个任务都拥有一个唯一的任务ID(任务ID可以从任务列表获取),当本任务执行结束并且执行成功时,将会触发子任务ID所对应的任务的一次主动调度。

以及由此延伸出来的番外篇《从XXL-job路由策略的"服务容错"说起》,探讨了XXL-job任务调度面临故障的容错设计。不过既然说到路由、容错,那执行器部署自然就是集群部署了,这在官网上也明确提到了:

不同的部署架构

执行器集群部署、调度中心单机部署

在这个架构下面,执行器是集群、调度中心假设为单机,数据源为Mysql:

执行器集群部署、调度中心集群部署

《分布式定时任务系列8:XXL-job源码分析之远程调用》

设计思想

将调度行为抽象形成"调度中心"公共平台,而平台自身并不承担业务逻辑,"调度中心"负责发起调度请求。

将任务抽象成分散的JobHandler,交由"执行器"统一管理,"执行器"负责接收调度请求并执行对应的JobHandler中的业务逻辑。

因此,"调度"和"任务"两部分可以相互解耦,提高系统整体稳定性和扩展性;

**"调度"和"任务"两部分可以相互解耦,提高系统整体稳定性和扩展性,**而执行器的的集群部署则可以提高业务系统的可用性。那么调度中心的集群部署则是提高调度中心的可用性!所以在这里就再来回顾下注册模型。

注册模型

为了观察注册模型的运行过程,先在本地运行2个调度中心实例,然后在将一个任务注册上去。

ER图

xxl_job_group:执行器信息表,维护任务执行器信息;

xxl_job_registry:执行器注册表,维护在线的执行器和调度中心机器地址信息;

sql 复制代码
-- xxl_job_group:执行器信息表,维护任务执行器信息;
CREATE TABLE `xxl_job_group` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `app_name` varchar(64) NOT NULL COMMENT '执行器AppName',
  `title` varchar(12) NOT NULL COMMENT '执行器名称',
  `address_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '执行器地址类型:0=自动注册、1=手动录入',
  `address_list` text COMMENT '执行器地址列表,多地址逗号分隔',
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- xxl_job_registry:执行器注册表,维护在线的执行器和调度中心机器地址信息;
CREATE TABLE `xxl_job_registry` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `registry_group` varchar(50) NOT NULL,
  `registry_key` varchar(255) NOT NULL,
  `registry_value` varchar(255) NOT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `i_g_k_v` (`registry_group`,`registry_key`,`registry_value`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
调度中心集群启动

现在直接在本地通过idea里面启动两个xxl-job-admin实例:端口分别为8080、8081

实例1:端口8080
实例1:端口8081

启动之后,通过两个地址分别访问实例:

http://localhost:8080/xxl-job-admin/http://localhost:8081/xxl-job-admin/

看到的效果都是一样的:

执行器启动

执行器的启动直接在本地通过idea里面启动xxl-job-executor-sample-springboot实例:注册时的配置就跟之前单机的xxl-job-admin有区别:

这个规则在官网上也有说明:

注册完成之后,再来分别查看xxl-job-admin的控制台,会现在注册上去的执行器列表:

如何看待调度中心的高可用

前面说了调度中心的集群部署是为了高可用,意思就是在集群中的某一个或某一些节点不可用时,整个调度中心还能正常工作。来看看官网的解释:

调度中心支持集群部署,提升调度系统容灾可用性

调度中心集群部署时,几点要求和建议:

  • DB配置保持一致;
  • 集群机器时钟保持一致(单机集群忽视);
  • 建议:推荐通过nginx为调度中心集群做负载均衡,分配域名。调度中心访问、执行器回调配置、调用API服务等操作均通过该域名进行。
Server间数据一致性

由于调度中心的数据存储使用的Mysql,并且根据要求DB配置保持一致,所以所有实例连接的是同一个Mysql库。那么就意味着调度中心的数据一致性就是通过DB来保证的,在这种情况下Server间的是天然一致性:

  • 执行器列表会存储在Mysql中,这样调度中心集群每个节点都会获取到相同的执行器(Client)列表
  • 由于XXL-job在触发任务时使用了DB锁(悲观锁)来避免重复执行,所以理论上同一时刻只会有一个调度节点来触发任务
  • 并且任务触发机制没有本地缓存,这样就保证调度中心获取到相同的执行器(Client)列表,就实现了Server节点的数据一致性

关于一致性的可以看到官网的说明:

具体源码的分析可参考:分布式定时任务系列7:XXL-job源码分析之任务触发

从上面的推论可以得出这样一个结论:只要调度中心集群的节点有一个正常运行(前提是Mysql数据库也正常),那么整个任务就可以正常调度了,这要达到了高可用的目的!

Server与Client的一致性

这里还要说明一点的是,虽然调度中心通过Mysql保证了执行器列表一致,但是细节上有点区别:

  • 在任务触发时,获取的执行器列表是读取的表xxl_job_group
  • 但是执行器注册时 是更新到xxl_job_registry这张表的

那这张表是什么时候同步的呢,或者说新的执行器注册之后,调度中心怎么感知的呢?这个就在调度中心的监听任务。回顾下com.xxl.job.admin.core.scheduler.XxlJobScheduler这个类:

java 复制代码
public void init() throws Exception {
        // init i18n
        initI18n();

        // admin trigger pool start
        JobTriggerPoolHelper.toStart();

        // admin registry monitor run
        JobRegistryHelper.getInstance().start();

        // admin fail-monitor run
        JobFailMonitorHelper.getInstance().start();

        // admin lose-monitor run ( depend on JobTriggerPoolHelper )
        JobCompleteHelper.getInstance().start();

        // admin log report start
        JobLogReportHelper.getInstance().start();

        // start-schedule  ( depend on JobTriggerPoolHelper )
        JobScheduleHelper.getInstance().start();

        logger.info(">>>>>>>>> init xxl-job admin success.");
    }

跟进com.xxl.job.admin.core.thread.JobRegistryHelper这个类:

java 复制代码
public void start(){

		// for registry or remove
		registryOrRemoveThreadPool = new ThreadPoolExecutor(
				2,
				10,
				30L,
				TimeUnit.SECONDS,
				new LinkedBlockingQueue<Runnable>(2000),
				new ThreadFactory() {
					@Override
					public Thread newThread(Runnable r) {
						return new Thread(r, "xxl-job, admin JobRegistryMonitorHelper-registryOrRemoveThreadPool-" + r.hashCode());
					}
				},
				new RejectedExecutionHandler() {
					@Override
					public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
						r.run();
						logger.warn(">>>>>>>>>>> xxl-job, registry or remove too fast, match threadpool rejected handler(run now).");
					}
				});

		// for monitor
        // 监听线程
		registryMonitorThread = new Thread(new Runnable() {
			@Override
			public void run() {
				while (!toStop) {
					try {
						// auto registry group
						List<XxlJobGroup> groupList = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().findByAddressType(0);
						if (groupList!=null && !groupList.isEmpty()) {

							// remove dead address (admin/executor)
                            // 从xxl_job_registry中移除超过"下线"时间的执行器
							List<Integer> ids = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findDead(RegistryConfig.DEAD_TIMEOUT, new Date());
							if (ids!=null && ids.size()>0) {
								XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().removeDead(ids);
							}

							// fresh online address (admin/executor)
                            // 从xxl_job_registry中更新"下线"时间内的执行器
							HashMap<String, List<String>> appAddressMap = new HashMap<String, List<String>>();
							List<XxlJobRegistry> list = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findAll(RegistryConfig.DEAD_TIMEOUT, new Date());
							if (list != null) {
								for (XxlJobRegistry item: list) {
									if (RegistryConfig.RegistType.EXECUTOR.name().equals(item.getRegistryGroup())) {
										String appname = item.getRegistryKey();
										List<String> registryList = appAddressMap.get(appname);
										if (registryList == null) {
											registryList = new ArrayList<String>();
										}

										if (!registryList.contains(item.getRegistryValue())) {
											registryList.add(item.getRegistryValue());
										}
										appAddressMap.put(appname, registryList);
									}
								}
							}

							// fresh group address
							for (XxlJobGroup group: groupList) {
								List<String> registryList = appAddressMap.get(group.getAppname());
								String addressListStr = null;
								if (registryList!=null && !registryList.isEmpty()) {
									Collections.sort(registryList);
									StringBuilder addressListSB = new StringBuilder();
									for (String item:registryList) {
										addressListSB.append(item).append(",");
									}
									addressListStr = addressListSB.toString();
									addressListStr = addressListStr.substring(0, addressListStr.length()-1);
								}
								group.setAddressList(addressListStr);
								group.setUpdateTime(new Date());

								XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().update(group);
							}
						}
					} catch (Exception e) {
						if (!toStop) {
							logger.error(">>>>>>>>>>> xxl-job, job registry monitor thread error:{}", e);
						}
					}
					try {
                        // 等待心跳时间,默认30s
						TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT);
					} catch (InterruptedException e) {
						if (!toStop) {
							logger.error(">>>>>>>>>>> xxl-job, job registry monitor thread error:{}", e);
						}
					}
				}
				logger.info(">>>>>>>>>>> xxl-job, job registry monitor thread stop");
			}
		});
		registryMonitorThread.setDaemon(true);
		registryMonitorThread.setName("xxl-job, admin JobRegistryMonitorHelper-registryMonitorThread");
		registryMonitorThread.start();
	}

而这个Client的心跳是在这个类的方法:

com.xxl.job.core.thread.ExecutorRegistryThread:

java 复制代码
public void start(final String appname, final String address){

        // valid
        if (appname==null || appname.trim().length()==0) {
            logger.warn(">>>>>>>>>>> xxl-job, executor registry config fail, appname is null.");
            return;
        }
        if (XxlJobExecutor.getAdminBizList() == null) {
            logger.warn(">>>>>>>>>>> xxl-job, executor registry config fail, adminAddresses is null.");
            return;
        }

        registryThread = new Thread(new Runnable() {
            @Override
            public void run() {

                // registry
                while (!toStop) {
                    try {
                        RegistryParam registryParam = new RegistryParam(RegistryConfig.RegistType.EXECUTOR.name(), appname, address);
                        for (AdminBiz adminBiz: XxlJobExecutor.getAdminBizList()) {
                            try {
                                ReturnT<String> registryResult = adminBiz.registry(registryParam);
                                if (registryResult!=null && ReturnT.SUCCESS_CODE == registryResult.getCode()) {
                                    registryResult = ReturnT.SUCCESS;
                                    logger.debug(">>>>>>>>>>> xxl-job registry success, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult});
                                    break;
                                } else {
                                    logger.info(">>>>>>>>>>> xxl-job registry fail, registryParam:{}, registryResult:{}", new Object[]{registryParam, registryResult});
                                }
                            } catch (Exception e) {
                                logger.info(">>>>>>>>>>> xxl-job registry error, registryParam:{}", registryParam, e);
                            }

                        }
                    } catch (Exception e) {
                        if (!toStop) {
                            logger.error(e.getMessage(), e);
                        }

                    }

                    try {
                        if (!toStop) {
                            // 在线程中,睡眠心跳时间,然后在循环中执行注册(其实就是心跳任务)
                            TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT);
                        }
                    } catch (InterruptedException e) {
                        if (!toStop) {
                            logger.warn(">>>>>>>>>>> xxl-job, executor registry thread interrupted, error msg:{}", e.getMessage());
                        }
                    }
                }

com.xxl.job.admin.core.thread.JobRegistryHelper#registry

java 复制代码
public ReturnT<String> registry(RegistryParam registryParam) {

		// valid
		if (!StringUtils.hasText(registryParam.getRegistryGroup())
				|| !StringUtils.hasText(registryParam.getRegistryKey())
				|| !StringUtils.hasText(registryParam.getRegistryValue())) {
			return new ReturnT<String>(ReturnT.FAIL_CODE, "Illegal Argument.");
		}

		// async execute
		registryOrRemoveThreadPool.execute(new Runnable() {
			@Override
			public void run() {
                // 执行器的注册,其实也是心跳任务,这个可以从调度中心的代码能看出来
				int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registryUpdate(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date());
				if (ret < 1) {
					XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registrySave(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date());

					// fresh
					freshGroupRegistryInfo(registryParam);
				}
			}
		});

		return ReturnT.SUCCESS;
	}

整个流程如下:

相关推荐
Knight_AL2 小时前
线程池满了怎么办?用 RabbitMQ 做任务补偿不丢失
分布式·rabbitmq·ruby
難釋懷4 小时前
分布式锁-redission锁的MutiLock原理
分布式
小北方城市网5 小时前
RabbitMQ 生产级实战:可靠性投递、高并发优化与问题排查
开发语言·分布式·python·缓存·性能优化·rabbitmq·ruby
乾元5 小时前
拒绝服务的进化:AI 调度下的分布式协同攻击策略
人工智能·分布式
听麟6 小时前
HarmonyOS 6.0+ PC端多设备文件拖拽协同开发实战:手眼同行增强与分布式软总线深度应用
分布式·华为·harmonyos
前端世界7 小时前
鸿蒙分布式网络性能优化实战:从通信建连到多设备协同
网络·分布式·harmonyos
雪碧聊技术8 小时前
什么是Zookeeper?
分布式·zookeeper
李白你好8 小时前
基于腾讯云函数 (SCF) 的分布式 IP 代理池.
分布式·tcp/ip·腾讯云
鱼跃鹰飞8 小时前
大厂面试真题-说说Kafka消息的不重复和不丢失
java·分布式·kafka