• 分布式定时任务系列10: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路由策略

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

    不忘初心

    好几个月前就打算分析一下XXL-job路由策略的源码,所以有了XXL-job路由策略。不过当时偷懒,只从官网上把介绍贴出来了:

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

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

    再谈前置条件

    一般提到路由,可能更多的是理解为对请求做转发时的路由匹配:比如nginx的Location路由,或者SpringCloud组件的Gateway网关上请求Predicate的URL路由匹配。

    不过这里说的路由,其实指的在集群条件下对执行器进行的路由选择,是一种负载均衡策略。所以这里假设了一个场景就是,在分布式环境下,有多个执行器组成的集群。这里回顾一下xxl-rpc部署示意图

    •  这里指的路由策略是执行器集群部署
    • 关于调度器集群部署不在此范围暂不讨论,
    • 后面会单开一节具体讨论如何集群部署,达到高性能、高可用目的!

    路由策略

    这里继续引用XXL-job源码分析之任务触发里面关于代码执行的流程

    可以看到路由策略的执行代码类路径在:com.xxl.job.admin.core.trigger.XxlJobTrigger ,方法路径在:com.xxl.job.admin.core.trigger.XxlJobTrigger#processTrigger:

    executorRouteStrategyEnum.getRouter().route(triggerParam, group.getRegistryList());

    策略定义

    XXL-job定义了策略枚举:

    1. public enum ExecutorRouteStrategyEnum {
    2. /** FIRST(第一个):固定选择第一个机器; */
    3. FIRST(I18nUtil.getString("jobconf_route_first"), new ExecutorRouteFirst()),
    4. /** (最后一个):固定选择最后一个机器; */
    5. LAST(I18nUtil.getString("jobconf_route_last"), new ExecutorRouteLast()),
    6. /** (轮询):; */
    7. ROUND(I18nUtil.getString("jobconf_route_round"), new ExecutorRouteRound()),
    8. /** (随机):随机选择在线的机器; */
    9. RANDOM(I18nUtil.getString("jobconf_route_random"), new ExecutorRouteRandom()),
    10. /** (一致性HASH):每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上。 */
    11. CONSISTENT_HASH(I18nUtil.getString("jobconf_route_consistenthash"), new ExecutorRouteConsistentHash()),
    12. /** (最不经常使用):使用频率最低的机器优先被选举; */
    13. LEAST_FREQUENTLY_USED(I18nUtil.getString("jobconf_route_lfu"), new ExecutorRouteLFU()),
    14. /** (最近最久未使用):最久未使用的机器优先被选举; */
    15. LEAST_RECENTLY_USED(I18nUtil.getString("jobconf_route_lru"), new ExecutorRouteLRU()),
    16. /** (故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度; */
    17. FAILOVER(I18nUtil.getString("jobconf_route_failover"), new ExecutorRouteFailover()),
    18. /** (忙碌转移):按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度; */
    19. BUSYOVER(I18nUtil.getString("jobconf_route_busyover"), new ExecutorRouteBusyover()),
    20. /** (分片广播):广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务; */
    21. SHARDING_BROADCAST(I18nUtil.getString("jobconf_route_shard"), null);
    22. ExecutorRouteStrategyEnum(String title, ExecutorRouter router) {
    23. this.title = title;
    24. this.router = router;
    25. }
    26. private String title;
    27. private ExecutorRouter router;
    28. public String getTitle() {
    29. return title;
    30. }
    31. public ExecutorRouter getRouter() {
    32. return router;
    33. }
    34. public static ExecutorRouteStrategyEnum match(String name, ExecutorRouteStrategyEnum defaultItem){
    35. if (name != null) {
    36. for (ExecutorRouteStrategyEnum item: ExecutorRouteStrategyEnum.values()) {
    37. if (item.name().equals(name)) {
    38. return item;
    39. }
    40. }
    41. }
    42. return defaultItem;
    43. }
    44. }

    其中枚举里面有一个属性router,真正的路由策略实现都在这个接口:com.xxl.job.admin.core.route.ExecutorRouter。

    路由接口

    看一看路由接口定义代码:

    1. public abstract class ExecutorRouter {
    2. protected static Logger logger = LoggerFactory.getLogger(ExecutorRouter.class);
    3. /**
    4. * route address
    5. *
    6. * @param addressList
    7. * @return ReturnT.content=address
    8. */
    9. public abstract ReturnT route(TriggerParam triggerParam, List addressList);
    10. }

    而上面的各种策略都实现了这个接口:

    这种是典型的策略模式应用,这里也可以看出好的代码通过设计模式可以很方便的做到扩展! 

    路由策略详解

    对于这些路由策略实现,从简单到复杂一个个的来解析。

    FIRST(第一个)

    此策略的定义是:固定选择第一个机器!意思就是不论执行器有多少个,始终选择执行器列表的第一个进行任务执行。

    这个策略的实现也相当简单:

    1. public class ExecutorRouteFirst extends ExecutorRouter {
    2. @Override
    3. public ReturnT route(TriggerParam triggerParam, List addressList){
    4. return new ReturnT(addressList.get(0));
    5. }
    6. }

    这个代码里面就是固定从addressList里面get第一个执行器

    LAST(最后一个)

    此策略的定义是:固定选择最后一个机器!意思就是不论执行器有多少个,始终选择执行器列表的最后一个进行任务执行。

    这个策略的实现也相当简单:

    1. public class ExecutorRouteLast extends ExecutorRouter {
    2. @Override
    3. public ReturnT route(TriggerParam triggerParam, List addressList) {
    4. return new ReturnT(addressList.get(addressList.size()-1));
    5. }
    6. }

     这个代码里面就是固定从addressList里面get最后一个个执行器

    ROUND(轮询)

    此策略的定义是:意思就是不论执行器有多少个,从执行器列表逐个选择进行任务执行。

    这个策略的实现如下:

    1. public class ExecutorRouteRound extends ExecutorRouter {
    2. private static ConcurrentMap routeCountEachJob = new ConcurrentHashMap<>();
    3. private static long CACHE_VALID_TIME = 0;
    4. private static int count(int jobId) {
    5. // cache clear
    6. if (System.currentTimeMillis() > CACHE_VALID_TIME) {
    7. routeCountEachJob.clear();
    8. CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24;
    9. }
    10. AtomicInteger count = routeCountEachJob.get(jobId);
    11. if (count == null || count.get() > 1000000) {
    12. // 初始化时主动Random一次,缓解首次压力
    13. count = new AtomicInteger(new Random().nextInt(100));
    14. } else {
    15. // count++
    16. count.addAndGet(1);
    17. }
    18. routeCountEachJob.put(jobId, count);
    19. return count.get();
    20. }
    21. @Override
    22. public ReturnT route(TriggerParam triggerParam, List addressList) {
    23. String address = addressList.get(count(triggerParam.getJobId())%addressList.size());
    24. return new ReturnT(address);
    25. }
    26. }

    这个代码稍微复杂一点,为了实现轮询的效果在内存中声明了一个Map来来进行计数,其中的key为任务jobId,value为每个任务的调用次数:

    • 声明一个Map类型字段routeCountEachJob来进行每个任务的调用次数计数,其中为ConcurrentMap、AtomicInteger类型的原因是防止并发
    • count(int jobId)方法的作用对当前触发的任务进行计数,这里AtomicInteger原子类对每次执行后就+1
    • 每24小时后重新开始计数
    • 然后根据当前任务(jobId)的调用次数从addressList里面选择执行器:即count % 执行器个数

    这样文字可能理解起来还是不太直接,其实就是类似如下的示例:

    RANDOM(随机)

    此策略的定义是:意思就是不论执行器有多少个,从执行器列表随机选择在线的机器。

    这个策略的实现也比较直观:

    1. public class ExecutorRouteRandom extends ExecutorRouter {
    2. private static Random localRandom = new Random();
    3. @Override
    4. public ReturnT route(TriggerParam triggerParam, List addressList) {
    5. String address = addressList.get(localRandom.nextInt(addressList.size()));
    6. return new ReturnT(address);
    7. }
    8. }

    这个代码里面就是随机从addressList里面get一个执行器:

    localRandom.nextInt(addressList.size())

    LEAST_FREQUENTLY_USED(最不经常使用) 

    此策略的定义是:意思就是不论执行器有多少个,使用频率最低的机器优先被选择出来进行任务执行。

    这个策略的实现如下:

    1. public class ExecutorRouteLFU extends ExecutorRouter {
    2. // 任务调用计算器,其中key为jobId-任务ID,value为HashMap:记录每个实例的调用次数
    3. private static ConcurrentMap> jobLfuMap = new ConcurrentHashMap>();
    4. private static long CACHE_VALID_TIME = 0;
    5. public String route(int jobId, List addressList) {
    6. // 缓存1天(24小时),然后重新计数
    7. // cache clear
    8. if (System.currentTimeMillis() > CACHE_VALID_TIME) {
    9. jobLfuMap.clear();
    10. CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24;
    11. }
    12. // 初始化
    13. // lfu item init
    14. HashMap lfuItemMap = jobLfuMap.get(jobId); // Key排序可以用TreeMap+构造入参Compare;Value排序暂时只能通过ArrayList;
    15. if (lfuItemMap == null) {
    16. lfuItemMap = new HashMap();
    17. jobLfuMap.putIfAbsent(jobId, lfuItemMap); // 避免重复覆盖
    18. }
    19. // put new
    20. for (String address: addressList) {
    21. if (!lfuItemMap.containsKey(address) || lfuItemMap.get(address) >1000000 ) {
    22. lfuItemMap.put(address, new Random().nextInt(addressList.size())); // 初始化时主动Random一次,缓解首次压力
    23. }
    24. }
    25. // 这里有一个删除动作,其实是因为实例可能动态上下线,对于下线的节点需要排除
    26. // remove old
    27. List delKeys = new ArrayList<>();
    28. for (String existKey: lfuItemMap.keySet()) {
    29. if (!addressList.contains(existKey)) {
    30. delKeys.add(existKey);
    31. }
    32. }
    33. // 移除下线节点,尽量防止调度到下线的节点上导致失败
    34. if (delKeys.size() > 0) {
    35. for (String delKey: delKeys) {
    36. lfuItemMap.remove(delKey);
    37. }
    38. }
    39. // 进行调用次数排序
    40. // load least userd count address
    41. List> lfuItemList = new ArrayList>(lfuItemMap.entrySet());
    42. Collections.sort(lfuItemList, new Comparator>() {
    43. @Override
    44. public int compare(Map.Entry o1, Map.Entry o2) {
    45. return o1.getValue().compareTo(o2.getValue());
    46. }
    47. });
    48. // 调用次数+1
    49. Map.Entry addressItem = lfuItemList.get(0);
    50. String minAddress = addressItem.getKey();
    51. addressItem.setValue(addressItem.getValue() + 1);
    52. return addressItem.getKey();
    53. }
    54. @Override
    55. public ReturnT route(TriggerParam triggerParam, List addressList) {
    56. String address = route(triggerParam.getJobId(), addressList);
    57. return new ReturnT(address);
    58. }
    59. }

    这个代码比较复杂一点, 不过如果对比缓存的淘汰策略的话,这个其实就是所谓的"LFU":

    LFU(The Least Frequently Used)最近不多使用算法,与LRU的区别在于LRU是以时间衡量,LFU是以时间段内的次数

    • 算法:若是一个数据在必定时间内被访问的次数很低,那么被认为在将来被访问的几率也是最低的,当规定空间用尽且需要放入新数据的时候,会优先淘汰时间段内访问次数最低的数据。

    • 优势:LFU也能够有效的保护缓存,相对场景来说,比LRU有更好的缓存命中率。由于是以次数为基准,因此更加准确,天然能有效的保证和提升命中率。

    • 缺点:由于LFU须要记录数据的访问频率,所以需要额外的空间;当访问模式改变的时候,算法命中率会急剧降低,这也是他最大弊端

    所以这个策略里面为了实现LFU的效果在内存中声明了一个Map来来进行计数,其中的key为任务jobId,value为每个任务的调用次数:

    • 声明一个Map类型字段jobLfuMap来进行每个任务的调用次数计数,其中为ConcurrentMap原因是防止并发
    • jobLfuMap的value是一个HashMap:其中key为任务的实例地址address,value为调用次数。这样设计的目的是为了通过这样的数据结构来达到,记录每一个任务jobId在每一个实例上的调用次数!与ROUND的区别是:ROUND记录的每个任务jobId的所有调用数次,LFU多了一个维度
    • 通过上面的这种数据结构,最终可以对每个实例的调用次数进行排序:所以要依赖一个ArrayList来排序,最终选取调用次数最少的实例来作为任务执行目标机器!

    LEAST_RECENTLY_USED(最近最久未使用)

    此策略的定义是:意思就是不论执行器有多少个,最久未使用的机器优先被选举。

    这个策略的实现如下:

    1. public class ExecutorRouteLRU extends ExecutorRouter {
    2. private static ConcurrentMap> jobLRUMap = new ConcurrentHashMap>();
    3. private static long CACHE_VALID_TIME = 0;
    4. public String route(int jobId, List addressList) {
    5. // 缓存1天(24小时),然后重新计数
    6. // cache clear
    7. if (System.currentTimeMillis() > CACHE_VALID_TIME) {
    8. jobLRUMap.clear();
    9. CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24;
    10. }
    11. // init lru
    12. LinkedHashMap lruItem = jobLRUMap.get(jobId);
    13. if (lruItem == null) {
    14. /**
    15. * LinkedHashMap
    16. * a、accessOrder:true=访问顺序排序(get/put时排序);false=插入顺序排期;
    17. * b、removeEldestEntry:新增元素时将会调用,返回true时会删除最老元素;可封装LinkedHashMap并重写该方法,比如定义最大容量,超出是返回true即可实现固定长度的LRU算法;
    18. */
    19. lruItem = new LinkedHashMap(16, 0.75f, true);
    20. jobLRUMap.putIfAbsent(jobId, lruItem);
    21. }
    22. // 新加入的节点处理:添加到lru中进行统计
    23. // put new
    24. for (String address: addressList) {
    25. if (!lruItem.containsKey(address)) {
    26. lruItem.put(address, address);
    27. }
    28. }
    29. // 这里有一个删除动作,其实是因为实例可能动态上下线,对于下线的节点需要排除
    30. // remove old
    31. List delKeys = new ArrayList<>();
    32. for (String existKey: lruItem.keySet()) {
    33. if (!addressList.contains(existKey)) {
    34. delKeys.add(existKey);
    35. }
    36. }
    37. if (delKeys.size() > 0) {
    38. for (String delKey: delKeys) {
    39. lruItem.remove(delKey);
    40. }
    41. }
    42. // 排序最后一个节点
    43. // load
    44. String eldestKey = lruItem.entrySet().iterator().next().getKey();
    45. String eldestValue = lruItem.get(eldestKey);
    46. return eldestValue;
    47. }
    48. @Override
    49. public ReturnT route(TriggerParam triggerParam, List addressList) {
    50. String address = route(triggerParam.getJobId(), addressList);
    51. return new ReturnT(address);
    52. }
    53. }

     这个代码比较复杂一点, 不过如果对比缓存的淘汰策略的话,这个其实就是所谓的"LRU":

    LRU(The Least Recently Used)最近最久未使用算法。相比于FIFO算法智能些。

    • 算法:若是一个数据最近不多被访问到,那么被认为在将来被访问的几率也是最低的,当规定空间用尽且需要放入新数据的时候,会优先淘汰最久未被访问的数据。

    • 优势:LRU能够有效的对访问比较频繁的数据进行保护,也就是针对热点数据的命中率提升有明显的效果。

    缺点:对于周期性、偶发性的访问数据,有大几率可能形成缓存污染,也就是置换出去了热点数据,把这些偶发性数据留下了,从而致使LRU的数据命中率急剧降低。

    所以这个策略里面为了实现LFU的效果在内存中声明了一个Map来来进行计数,其中的key为任务jobId,value为每个任务的调用次数: 

    • 声明一个Map类型字段jobLRUMap来进行每个任务的调用次数计数,其中为ConcurrentMap原因是防止并发
    • jobLRUMap的value是一个LinkedHashMap:其中key为任务的实例地址address,value为address。这里比较巧妙的是直接利用了LinkedHashMap的排序能力
    1. /**
    2. * Constructs an empty LinkedHashMap instance with the
    3. * specified initial capacity, load factor and ordering mode.
    4. *
    5. * @param initialCapacity the initial capacity
    6. * @param loadFactor the load factor
    7. * @param accessOrder the ordering mode - true for
    8. * access-order, false for insertion-order
    9. * @throws IllegalArgumentException if the initial capacity is negative
    10. * or the load factor is nonpositive
    11. */
    12. public LinkedHashMap(int initialCapacity,
    13. float loadFactor,
    14. boolean accessOrder) {
    15. super(initialCapacity, loadFactor);
    16. this.accessOrder = accessOrder;
    17. }
    LinkedHashMap的排序模式

    a、accessOrder:true=访问顺序排序(get/put时排序);false=插入顺序排期;

    LinkedHashMap中'accessOrder‘字段的用途是什么?

    LinkedHashMap是Java中的一个类,它是HashMap的一个子类,具有HashMap的所有特性,并且还保持了插入顺序或访问顺序的特性。

    'accessOrder'字段是LinkedHashMap类中的一个布尔类型的属性,用于指定迭代顺序是否基于访问顺序。当accessOrder为true时,表示迭代顺序将基于最近访问顺序,即最近访问的元素将排在迭代顺序的末尾;当accessOrder为false时,表示迭代顺序将基于插入顺序,即元素将按照插入的顺序进行迭代。

    使用accessOrder字段可以方便地实现LRU(Least Recently Used,最近最少使用)缓存淘汰算法。通过将accessOrder设置为true,当访问某个元素时,该元素会被移到链表的末尾,这样在需要淘汰元素时,只需要移除链表头部的元素即可。

    LinkedHashMap的应用场景包括但不限于:

    1. 缓存系统:通过设置accessOrder为true,可以实现基于访问顺序的缓存淘汰策略。
    2. LRU缓存:通过继承LinkedHashMap并重写removeEldestEntry方法,可以实现固定大小的LRU缓存。
    3. 记录访问顺序:当需要按照访问顺序记录某些数据时,可以使用LinkedHashMap。

    FAILOVER(故障转移)

    此策略的定义是:按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度。

    这个策略的实现如下:

    1. public class ExecutorRouteFailover extends ExecutorRouter {
    2. @Override
    3. public ReturnT route(TriggerParam triggerParam, List addressList) {
    4. StringBuffer beatResultSB = new StringBuffer();
    5. for (String address : addressList) {
    6. // beat
    7. ReturnT beatResult = null;
    8. try {
    9. ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address);
    10. beatResult = executorBiz.beat();
    11. } catch (Exception e) {
    12. logger.error(e.getMessage(), e);
    13. beatResult = new ReturnT(ReturnT.FAIL_CODE, ""+e );
    14. }
    15. beatResultSB.append( (beatResultSB.length()>0)?"

      "
      :"")
    16. .append(I18nUtil.getString("jobconf_beat") + ":")
    17. .append("
      address:"
      ).append(address)
    18. .append("
      code:"
      ).append(beatResult.getCode())
    19. .append("
      msg:"
      ).append(beatResult.getMsg());
    20. // beat success
    21. if (beatResult.getCode() == ReturnT.SUCCESS_CODE) {
    22. beatResult.setMsg(beatResultSB.toString());
    23. beatResult.setContent(address);
    24. return beatResult;
    25. }
    26. }
    27. return new ReturnT(ReturnT.FAIL_CODE, beatResultSB.toString());
    28. }
    29. }

    这个策略的实现也比较直观,根据注册的实例列表依次发起心跳检测,如果成功,则选取为执行节点!代码并不复杂,不过可能对FAILOVER(故障转移)这个术语所震撼,觉得很高大上。

    对于FAILOVER这种故障处理策略来说,不同的框架或者场景实现不同,难易程度也不同,而且也不是所有的系统/接口适合故障转移。比如对一个有超时机制的微服务架构来说:

    如果链路比较多,一个业务请求需要经过A->B->C3个服务,每个服务有2个节点。假设在A服务调用B的时候,存在网络问题,A的实例A1失败,这里转移到A2成功;A->B的时候,B1失败,B2成功;同理B->C,C1失败,C2成功,虽然最终请求成功,但是整体耗时会增大一倍,早已经进过了网关(整体)响应时长导致timeout了,所以有些专题的FAILOVER并不一定是有益甚至有害的(重试也增加了服务调用次数,服务的压力)。关于这一块会后面单独开一节服务故障模式的讨论

    BUSYOVER(忙碌转移)

    此策略的定义是:按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度。

    这个策略的实现如下:

    1. public class ExecutorRouteBusyover extends ExecutorRouter {
    2. @Override
    3. public ReturnT route(TriggerParam triggerParam, List addressList) {
    4. StringBuffer idleBeatResultSB = new StringBuffer();
    5. for (String address : addressList) {
    6. // beat
    7. ReturnT idleBeatResult = null;
    8. try {
    9. ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address);
    10. idleBeatResult = executorBiz.idleBeat(new IdleBeatParam(triggerParam.getJobId()));
    11. } catch (Exception e) {
    12. logger.error(e.getMessage(), e);
    13. idleBeatResult = new ReturnT(ReturnT.FAIL_CODE, ""+e );
    14. }
    15. idleBeatResultSB.append( (idleBeatResultSB.length()>0)?"

      "
      :"")
    16. .append(I18nUtil.getString("jobconf_idleBeat") + ":")
    17. .append("
      address:"
      ).append(address)
    18. .append("
      code:"
      ).append(idleBeatResult.getCode())
    19. .append("
      msg:"
      ).append(idleBeatResult.getMsg());
    20. // beat success
    21. if (idleBeatResult.getCode() == ReturnT.SUCCESS_CODE) {
    22. idleBeatResult.setMsg(idleBeatResultSB.toString());
    23. idleBeatResult.setContent(address);
    24. return idleBeatResult;
    25. }
    26. }
    27. return new ReturnT(ReturnT.FAIL_CODE, idleBeatResultSB.toString());
    28. }
    29. }

     这个策略的实现也比较直观,根据注册的实例列表依次发起空闲检测,如果成功,则选取为执行节点!代码并不复杂,不过可能需要联合起前面XXL-rpc的章节来配合理解

    CONSISTENT_HASH(一致性HASH)

    此策略的定义是:每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上。

    这个策略的实现如下:

    1. /**
    2. * 分组下机器地址相同,不同JOB均匀散列在不同机器上,保证分组下机器分配JOB平均;且每个JOB固定调度其中一台机器;
    3. * a、virtual node:解决不均衡问题
    4. * b、hash method replace hashCode:String的hashCode可能重复,需要进一步扩大hashCode的取值范围
    5. * Created by xuxueli on 17/3/10.
    6. */
    7. public class ExecutorRouteConsistentHash extends ExecutorRouter {
    8. private static int VIRTUAL_NODE_NUM = 100;
    9. /**
    10. * get hash code on 2^32 ring (md5散列的方式计算hash值)
    11. * @param key
    12. * @return
    13. */
    14. private static long hash(String key) {
    15. // md5 byte
    16. MessageDigest md5;
    17. try {
    18. md5 = MessageDigest.getInstance("MD5");
    19. } catch (NoSuchAlgorithmException e) {
    20. throw new RuntimeException("MD5 not supported", e);
    21. }
    22. md5.reset();
    23. byte[] keyBytes = null;
    24. try {
    25. keyBytes = key.getBytes("UTF-8");
    26. } catch (UnsupportedEncodingException e) {
    27. throw new RuntimeException("Unknown string :" + key, e);
    28. }
    29. md5.update(keyBytes);
    30. byte[] digest = md5.digest();
    31. // hash code, Truncate to 32-bits
    32. long hashCode = ((long) (digest[3] & 0xFF) << 24)
    33. | ((long) (digest[2] & 0xFF) << 16)
    34. | ((long) (digest[1] & 0xFF) << 8)
    35. | (digest[0] & 0xFF);
    36. long truncateHashCode = hashCode & 0xffffffffL;
    37. return truncateHashCode;
    38. }
    39. public String hashJob(int jobId, List addressList) {
    40. // ------A1------A2-------A3------
    41. // -----------J1------------------
    42. TreeMap addressRing = new TreeMap();
    43. for (String address: addressList) {
    44. for (int i = 0; i < VIRTUAL_NODE_NUM; i++) {
    45. long addressHash = hash("SHARD-" + address + "-NODE-" + i);
    46. addressRing.put(addressHash, address);
    47. }
    48. }
    49. long jobHash = hash(String.valueOf(jobId));
    50. SortedMap lastRing = addressRing.tailMap(jobHash);
    51. if (!lastRing.isEmpty()) {
    52. return lastRing.get(lastRing.firstKey());
    53. }
    54. return addressRing.firstEntry().getValue();
    55. }
    56. @Override
    57. public ReturnT route(TriggerParam triggerParam, List addressList) {
    58. String address = hashJob(triggerParam.getJobId(), addressList);
    59. return new ReturnT(address);
    60. }
    61. }

     这个策略的实现也比较直观,这里就不展开讨论了,有兴趣的可以在评论区晒出理解分享给大家!

    路由策略的选择

    上面介绍了各种路由策略的实现,关于这里面的路由策略的选择就不过多讨论了,建议默认情况采用下轮询!

    各种路由策略比较

    XXL-job路由策略对比
    序号名称适用场景
    1FIRST(第一个)
    • 执行器单机部署,因为只有一台也就无所谓什么路由策略
    • 调试任务不频繁,量比较小的时候
    • 这个策略一般都不是首选,除非的确有一台机器的性能明显高于其它节点,可以考虑。而且在实际操作过程中,还要保证执行器的注册顺序,鸡肋!
    2LAST(最后一个)这个策略本质上跟FIRST没有区别...
    3ROUND(轮询)
    • 在执行器分布式(集群部署)场景下的首选策略:因为它兼顾了平均节点利用率
    • 适合节点多、调度频繁:一般的生产物理机或云主机其实性能都差不多,这个策略能把任务相对平均的分配到各个节点,提升整体利用率
    4RANDOM(随机)
    • 在执行器分布式(集群部署)场景下的次选策略
    • 适合节点非多、调度非常频繁:一般的生产物理机或云主机其实性能都差不多,这个策略在概率上能把任务相对平均的分配到各个节点(节点足够多),提升整体利用率
    5LEAST_FREQUENTLY_USED(最不经常使用)LFU其实是为了解决多节点利用率的问题,跟下面的LRU类似,不过由于其实现是依赖本地JVM里面的内存操作,可能对CPU占用会高一些,无特殊需求也非首选
    6LEAST_RECENTLY_USED(最近最久未使用)同LFU
    7FAILOVER(故障转移)
    • 执行器集群部署,能实现任务调度的高可用(尽量大努力重试),所以它有一定的限制就是不能在调度里面设置超时时间(或者不能太长)
    • 而且它即做不到调度任务的平均分发,也就无法提升整体利用率(因为是按照注册列表addressList顺序遍历),其实就FIRST的加强PLUS版本
    • 所以只有在某些特殊场景下:比如调度频率很低(日终\日结类业务,一天可能就跑一次的),但是又需要尽可能成功的时候,可以考虑此策略
    8BUSYOVER(忙碌转移)这个策略本质上跟FAILOVER没有区别,,只不过它可以根据执行器自身的执行情况反馈的结果进行适当的调整(idleBeat),后面会从注册发现来探讨这个问题
    9CONSISTENT_HASH(一致性HASH)分组下机器地址相同,不同JOB均匀散列在不同机器上,保证分组下机器分配JOB平均;且每个JOB固定调度其中一台机器;(可以自行看下一致性HASH

  • 相关阅读:
    关于电商API接口接入|接口请求重试的8种方法,你用哪种?
    使用Spring WebSocket实现实时通信功能
    解析vue.config.js文件
    Java核心技术 卷1
    jwt not active
    Hexo搭建个人博客模板(附源码)
    请求的转发和重定向
    小程序上新(2022.08.04~10.12)
    天干地支(蓝桥杯)
    哈工大李治军老师操作系统笔记【5】:操作系统的历史(Learning OS Concepts By Coding Them !)
  • 原文地址:https://blog.csdn.net/weigeshikebi/article/details/139882040