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实现的好处在于,支持主动执行来提前刷新缓存。此时,策略应该配置为分片广播,使所有执行器中缓存保持一致。
    而博主在工作中曾遇到,同事将刷新项目进程缓存的任务配置为故障转移,导致始终只有一个节点的内存定期刷新,而其他节点没有被调度的机会,从而导致业务异常。
相关推荐
一线大码几秒前
Java 使用国密算法实现数据加密传输
java·spring boot·后端
我命由我123456 分钟前
Android Gradle - Gradle 自定义插件(Build Script 自定义插件、buildSrc 自定义插件、独立项目自定义插件)
android·java·java-ee·kotlin·android studio·android-studio·android runtime
Riu_Peter10 分钟前
【技术】Maven 配置 settings.xml 轮询下载
xml·java·maven
十六年开源服务商42 分钟前
2026年WordPress网站地图完整指南
java·前端·javascript
Edward111111111 小时前
3月17枚举
java·开发语言
凡。。。2961 小时前
阿里云产品说明
java
蓝天守卫者联盟11 小时前
2026乙酸乙酯回收设备厂家选型与技术实践
java·jvm·python·算法
于先生吖1 小时前
教育数字化转型 JAVA 国际版答题练习系统完整开发教程
java·开发语言
lakernote1 小时前
EasyPostman 重大更新:正式支持插件模式,当前已上线 5 个官方插件
java·测试工具·开源·postman