xxl-job路由策略浅析

一、概况

调度平台在进行任务调度时,除分片任务外,需要从执行器列表中选择一个来调度,如何选择就需要路由策略。策略实现在xxl-job-admin项目的core.route包中。

抽象类com.xxl.job.admin.core.route.ExecutorRouter仅有一个抽象接口。枚举类ExecutorRouteStrategyEnum声明了支持的策略。

java 复制代码
// 返回执行器地址即ip:port
public abstract ReturnT<String> route(TriggerParam triggerParam, List<String> addressList); 

二、路由策略实现

2.1 RouteFirst、RouteLast和RouteRandom

  • RouteFirst返回addressList.get(0)
  • RouteLast返回addressList.get(addressList.size()-1)。
  • RouteRandom返回addressList.get(new Random().nextInt(addressList.size()));

2.2 RouteRound

轮训算法的常规实现:定义一个计数器即int counter=0,每路由一次counter加1,当counter>Integer.MAX_VALUE时让ounter归零;路由时addressList.get(counter%addressList.size())。

xxl-job中对轮训做了增强:

  1. counter初始值随机获取100以内的数;
  2. counter>1000000,或初始赋值后24小时后,主动再次初始化。

2.3 RouteConsistentHash

即一致性hash路由。效果为:

  • 每个JOB固定调度执行器列表中一台机器,但是执行器数量发生变化时,调度的节点可能会发生变化;
  • 假设有10个job,它们的执行器列表相同;那么这些JOB将均匀散列在不同机器上。

实现中有下面两点优化:

  • a、virtual node:解决不均衡问题。每个执行器虚拟为100个hash环节点。
  • b、计算hash值时,没有使用String的hashCode(可能重复),使用md5散列计算hash值,将取值范围扩大到2^32。 查找时,找大于jobId的hash值的第一个entry,entry.value就是目标address。 这儿hash环的实现方式,值得借鉴。

2.4 RouteLFU

LFU(Least Frequently Used),使用频率最低的优先被选举。 此处,频率统计使用计数器。

  • 对某个JOB的每个执行器,每调用一次,计数器加1;
  • 初始化计数器时,取值是new Random().nextInt(addressList.size());当计数大于1000000时,重新初始化。
  • 每次调用,需将新增的执行器计数器初始化,将下线的执行器计数器删除。
  • 选择时,将内层Map.Entry按照计数顺序排列,获取第一个(即使用次数最小的节点)。
java 复制代码
// 使用次数的缓存,24小时强制重置
// 外层Map的key即jobId,内存Map的key即执行器address,value即计数器。
private static ConcurrentMap<Integer, HashMap<String, Integer>> jobLfuMap = new ConcurrentHashMap<Integer, HashMap<String, Integer>>();

2.5 RouteLRU

LRU(Least Recently Used),(时间上)最近最久未使用。使用LinkedHashMap实现lru算法。

java 复制代码
// 缓存
// 外层Map的key即jobId,内存Map的key和value,都是address
private static ConcurrentMap<Integer, LinkedHashMap<String, String>> jobLRUMap = new ConcurrentHashMap<Integer, LinkedHashMap<String, String>>();
java 复制代码
// lru算法使用LinkedHashMap实现
LinkedHashMap<String, String> lruItem = jobLRUMap.get(jobId);
if (lruItem == null) {
    /**
     * LinkedHashMap
     *      a、accessOrder:true=访问顺序排序(get/put时排序);false=插入顺序排期;
     *      b、removeEldestEntry:新增元素时将会调用,返回true时会删除最老元素;可封装LinkedHashMap并重写该方法,比如定义最大容量,超出时返回true即可实现固定长度的LRU算法;
     */
    lruItem = new LinkedHashMap<String, String>(16, 0.75f, true);
    jobLRUMap.putIfAbsent(jobId, lruItem);
}

每次被访问的节点,被移到到队尾。选择时,获取链条的第一个节点即可

2.6 RouteBusyover

忙碌转移。该策略在路由时,会遍历执行器列表,依次发起空闲检测,只要有节点返回true,就将向该节点发起调度。(节点处于何种状态时才算空闲?随后介绍)

java 复制代码
// 简化后代码
for (String address : addressList) {
        ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address);
        // 空闲检查
        idleBeatResult = executorBiz.idleBeat(new IdleBeatParam(triggerParam.getJobId()));
        // 返回第一个空闲节点
        if (idleBeatResult.getCode() == ReturnT.SUCCESS_CODE) {
                idleBeatResult.setMsg(idleBeatResultSB.toString());
                idleBeatResult.setContent(address);
                return idleBeatResult;
        }
}

idleBeat由调度平台向执行器发起,是单向的。

2.7 RouteFailover

故障转移。执行列表手动注册时,调度平台不会定期维护如添加新节点、移除下线节点。即使采取主动注册,在心跳间隙,某个节点突然下线,此时正好调度到该节点,本次执行将会失败(如果配置了邮件告警,我们还能及时感知到调度异常)。 该策略在路由时,会遍历执行器列表,依次发起心跳检测。返回第一个响应成功的节点。

java 复制代码
// 简化后代码
for (String address : addressList) {
        // beat
        ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address);
        beatResult = executorBiz.beat();
        if (beatResult.getCode() == ReturnT.SUCCESS_CODE) {
                beatResult.setMsg(beatResultSB.toString());
                beatResult.setContent(address);
                return beatResult;
        }
}

2.8 分片广播

准确来说,分片广播不是路由策略,因为它将调度所有已注册的执行器,而不是从中选择一个。因此,它并不是ExecutorRouter接口的实现类。在com.xxl.job.admin.core.trigger.XxlJobTrigger#trigger中有如下代码:

java 复制代码
	// 分片广播
	if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST==ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null)
			&& group.getRegistryList()!=null && !group.getRegistryList().isEmpty()
			&& shardingParam==null) {
                 // 遍历调度每一个执行器
		for (int i = 0; i < group.getRegistryList().size(); i++) {
			processTrigger(group, jobInfo, finalFailRetryCount, triggerType, i, group.getRegistryList().size());
		}
	} else {
		// 其他路由策略,分片索引是0,分片总数为1
		if (shardingParam == null) {
			shardingParam = new int[]{0, 1};
		}
		processTrigger(group, jobInfo, finalFailRetryCount, triggerType, shardingParam[0], shardingParam[1]);
	}

processTrigger()方法最后两个参数,分别是sharding index、sharding total,其中获取address代码如下:

java 复制代码
	// 3、init address
	String address = null;
	ReturnT<String> routeAddressResult = null;
	if (group.getRegistryList()!=null && !group.getRegistryList().isEmpty()) {
		// 分片广播
		if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST == executorRouteStrategyEnum) {
			// 正常不会走else分支
			if (index < group.getRegistryList().size()) {
				address = group.getRegistryList().get(index);
			} else {
				address = group.getRegistryList().get(0);
			}
		} else {
		// 路由
			routeAddressResult = executorRouteStrategyEnum.getRouter().route(triggerParam, group.getRegistryList());
			if (routeAddressResult.getCode() == ReturnT.SUCCESS_CODE) {
				address = routeAddressResult.getContent();
			}
		}
	} else {
		routeAddressResult = new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("jobconf_trigger_address_empty"));
	}

三、选择路由策略

  • 通常选择随机或轮训即可。
  • 如果要保证每次调度成功,可选择故障转移
  • 如果要保证任务执行的及时性,可选择忙碌转移
  • 分片任务,自然得选分片广播。
  • 对于刷新程序内存缓存的定时任务,其实可以用spring的@Schedule来实现。用xxl-job实现的好处在于,支持主动执行来提前刷新缓存。此时,策略应该配置为分片广播,使所有执行器中缓存保持一致。
    而博主在工作中曾遇到,同事将刷新项目进程缓存的任务配置为故障转移,导致始终只有一个节点的内存定期刷新,而其他节点没有被调度的机会,从而导致业务异常。
相关推荐
paopaokaka_luck几秒前
基于Spring Boot+Vue的多媒体素材管理系统的设计与实现
java·数据库·vue.js·spring boot·后端·算法
guoruijun_2012_47 分钟前
fastadmin多个表crud连表操作步骤
android·java·开发语言
Hello-Brand17 分钟前
Java核心知识体系10-线程管理
java·高并发·多线程·并发·多线程模型·线程管理
乐悠小码23 分钟前
数据结构------队列(Java语言描述)
java·开发语言·数据结构·链表·队列
史努比.25 分钟前
Pod控制器
java·开发语言
2的n次方_28 分钟前
二维费用背包问题
java·算法·动态规划
皮皮林55128 分钟前
警惕!List.of() vs Arrays.asList():这些隐藏差异可能让你的代码崩溃!
java
莳光.28 分钟前
122、java的LambdaQueryWapper的条件拼接实现数据sql中and (column1 =1 or column1 is null)
java·mybatis
程序猿麦小七33 分钟前
基于springboot的景区网页设计与实现
java·spring boot·后端·旅游·景区
weisian15140 分钟前
认证鉴权框架SpringSecurity-2--重点组件和过滤器链篇
java·安全