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实现的好处在于,支持主动执行来提前刷新缓存。此时,策略应该配置为分片广播,使所有执行器中缓存保持一致。
    而博主在工作中曾遇到,同事将刷新项目进程缓存的任务配置为故障转移,导致始终只有一个节点的内存定期刷新,而其他节点没有被调度的机会,从而导致业务异常。
相关推荐
Good Note5 分钟前
Golang的静态强类型、编译型、并发型
java·数据库·redis·后端·mysql·面试·golang
我就是我35241 分钟前
记录一次SpringMVC的406错误
java·后端·springmvc
向哆哆43 分钟前
Java应用程序的跨平台性能优化研究
java·开发语言·性能优化
ekkcole1 小时前
windows使用命令解压jar包,替换里面的文件。并重新打包成jar包,解决Failed to get nested archive for entry
java·windows·jar
handsomestWei2 小时前
java实现多图合成mp4和视频附件下载
java·开发语言·音视频·wutool·图片合成视频·视频附件下载
全栈若城2 小时前
03 Python字符串与基础操作详解
java·开发语言·python
伯牙碎琴2 小时前
二、Spring Framework基础:IoC(控制反转)和DI(依赖注入)
java·spring·log4j
菲力蒲LY2 小时前
输入搜索、分组展示选项、下拉选取,全局跳转页,el-select 实现 —— 后端数据处理代码,抛砖引玉展思路
java·前端·mybatis
南宫生2 小时前
力扣每日一题【算法学习day.130】
java·学习·算法·leetcode
!!!5253 小时前
Java实现斗地主-做牌以及对牌排序
java·算法