elastic-job源码分析(三)作业分片

关于作业分片,首先先看下有哪些地方会促使作业进行分片

public void init() {
        // 1.将配置更新到注册中心
        LiteJobConfiguration liteJobConfigFromRegCenter = schedulerFacade.updateJobConfiguration(liteJobConfig);
        // 2.缓存当前作业分片数到本地
        JobRegistry.getInstance().setCurrentShardingTotalCount(liteJobConfigFromRegCenter.getJobName(), liteJobConfigFromRegCenter.getTypeConfig().getCoreConfig().getShardingTotalCount());
        // 3.JobScheduleController 实例,controller类负责控制quartz调度引擎
        JobScheduleController jobScheduleController = new JobScheduleController(
                createScheduler(), createJobDetail(liteJobConfigFromRegCenter.getTypeConfig().getJobClass()), liteJobConfigFromRegCenter.getJobName());
        // 4.缓存作业对应的controller对应的
        JobRegistry.getInstance().registerJob(liteJobConfigFromRegCenter.getJobName(), jobScheduleController, regCenter);
        // 5.作业开始调度前,更改一些配置()
        schedulerFacade.registerStartUpInfo(!liteJobConfigFromRegCenter.isDisabled());
        // 6.根据cron表达式开始调度作业
        jobScheduleController.scheduleJob(liteJobConfigFromRegCenter.getTypeConfig().getCoreConfig().getCron());
    }

第一个地方在第五步,开始调度前,在注册作业启动信息时会设置setReshardingFlag,也就是在leader/sharding/ 下面保存一个necessary节点作为是否重新分片的标记。

public void registerStartUpInfo(final boolean enabled) {
        // 注册中心节点监听
        listenerManager.startAllListeners();
        // 选举leader(负责处理分片)
        leaderService.electLeader();
        // 更新server状态
        serverService.persistOnline(enabled);
        // 作业实例上线
        instanceService.persistOnline();
        // 作业分片标记设置
        shardingService.setReshardingFlag();
        // 初始化作业监听器
        monitorService.listen();
        // 开始调节分布式作业不一致问题
        if (!reconcileService.isRunning()) {
            reconcileService.startAsync();
        }
    }

设置好标记之后,每次任务执行时会调用AbstractElasticJobExecutor.execute(),会在其中调用getShardingContexts来获取分片信息

public final void execute() {
        try {
            jobFacade.checkJobExecutionEnvironment();
        } catch (final JobExecutionEnvironmentException cause) {
            jobExceptionHandler.handleException(jobName, cause);
        }
        // 获取分片信息
        ShardingContexts shardingContexts = jobFacade.getShardingContexts();
        if (shardingContexts.isAllowSendJobEvent()) {
            jobFacade.postJobStatusTraceEvent(shardingContexts.getTaskId(), State.TASK_STAGING, String.format("Job '%s' execute begin.", jobName));
        }
      }
public ShardingContexts getShardingContexts() {
        boolean isFailover = configService.load(true).isFailover();
        if (isFailover) {
            List<Integer> failoverShardingItems = failoverService.getLocalFailoverItems();
            if (!failoverShardingItems.isEmpty()) {
                return executionContextService.getJobShardingContext(failoverShardingItems);
            }
        }
        // 这个步骤是关键
        shardingService.shardingIfNecessary();
        List<Integer> shardingItems = shardingService.getLocalShardingItems();
        if (isFailover) {
            shardingItems.removeAll(failoverService.getLocalTakeOffItems());
        }
        shardingItems.removeAll(executionService.getDisabledItems(shardingItems));
        return executionContextService.getJobShardingContext(shardingItems);
    }

shardingService.shardingIfNecessary() 这个方法会判断当前是否需要分片,判断的依据就是是否存在necessary标记,当然分片的关键还在于是否有作业的instance节点,如果没有可用的节点当然就不用分片了。有了分片标记,可用节点,还需要一点,分片任务可不是人人可以做的,只有leader节点可以进行分片任务,这样可以保持分片的唯一性。如何成为leader相关的代码会在下节leader选举中来看。

public void shardingIfNecessary() {
        List<JobInstance> availableJobInstances = instanceService.getAvailableJobInstances();
        // 是否存在分片标记,是否有可用节点
        if (!isNeedSharding() || availableJobInstances.isEmpty()) {
            return;
        }
        // 是否是leader,如果不是leader则阻塞等待分片结束,只有leader可以进行分片。
        if (!leaderService.isLeaderUntilBlock()) {
            blockUntilShardingCompleted();
            return;
        }
        // 如果还有正在运行的作业分片节点,则等待运行结束后再重新分片
        waitingOtherJobCompleted();
        // 从注册中心加载作业配置
        LiteJobConfiguration liteJobConfig = configService.load(false);
        // 获取总的分片数
        int shardingTotalCount = liteJobConfig.getTypeConfig().getCoreConfig().getShardingTotalCount();
        log.debug("Job '{}' sharding begin.", jobName);
        // 分片开始时添加正在分片标记
        jobNodeStorage.fillEphemeralJobNode(ShardingNode.PROCESSING, "");
        // 重新分片
        resetShardingInfo(shardingTotalCount);
        // 将分片分给对应节点,具体策略有三种
        JobShardingStrategy jobShardingStrategy = JobShardingStrategyFactory.getStrategy(liteJobConfig.getJobShardingStrategyClass());
        // 分片结束后调用回调函数设置到注册中心
        jobNodeStorage.executeInTransaction(new PersistShardingInfoTransactionExecutionCallback(jobShardingStrategy.sharding(availableJobInstances, jobName, shardingTotalCount)));
        log.debug("Job '{}' sharding complete.", jobName);
    }

当然分片时是要等待其他的分片结束之后才进行。这个标记是sharding/分片号/running,等所有作业运行结束,添加正在分片节点,与necessary节点同级,作用也是给其他服务上的节点提供是否分片结束的信号。然后正式开始进行分片,分片则是在在sharind目录下面添加有序的序号目录

private void resetShardingInfo(final int shardingTotalCount) {
        for (int i = 0; i < shardingTotalCount; i++) {
        	// 重新设置分片节点之前先将之前的节点删除
            jobNodeStorage.removeJobNodeIfExisted(ShardingNode.getInstanceNode(i));
            jobNodeStorage.createJobNodeIfNeeded(ShardingNode.ROOT + "/" + i);
        }
        // 如果存在多余的分片节点,则删除
        int actualShardingTotalCount = jobNodeStorage.getJobNodeChildrenKeys(ShardingNode.ROOT).size();
        if (actualShardingTotalCount > shardingTotalCount) {
            for (int i = shardingTotalCount; i < actualShardingTotalCount; i++) {
                jobNodeStorage.removeJobNodeIfExisted(ShardingNode.ROOT + "/" + i);
            }
        }
    }

分片结束了,但都是些空目录,那么如何知道哪些分片是哪些机器的呢?如何将分片分给机器节点,则是通过策略模式来进行,具体使用那种策略可以进行配置,如果没有进行设置默认使用平均分片策略。

public static JobShardingStrategy getStrategy(final String jobShardingStrategyClassName) {
        // 默认采用平均分片
        /**
         * 1.如果 3 台作业服务器且分片总数为9,则分片结果为:1=[0,1,2], 2=[3,4,5], 3=[6,7,8];
         * 2.如果 3 台作业服务器且分片总数为8,则分片结果为:1=[0,1,6], 2=[2,3,7], 3=[4,5];
         * 3.如果 3 台作业服务器且分片总数为10,则分片结果为:1=[0,1,2,9], 2=[3,4,5], 3=[6,7,8]
         */
        if (Strings.isNullOrEmpty(jobShardingStrategyClassName)) {
            return new AverageAllocationJobShardingStrategy();
        }
        try {
            Class<?> jobShardingStrategyClass = Class.forName(jobShardingStrategyClassName);
            if (!JobShardingStrategy.class.isAssignableFrom(jobShardingStrategyClass)) {
                throw new JobConfigurationException("Class '%s' is not job strategy class", jobShardingStrategyClassName);
            }
            return (JobShardingStrategy) jobShardingStrategyClass.newInstance();
        } catch (final ClassNotFoundException | InstantiationException | IllegalAccessException ex) {
            throw new JobConfigurationException("Sharding strategy class '%s' config error, message details are '%s'", jobShardingStrategyClassName, ex.getMessage());
        }
    }

可以根据配置策略类名进行实例化,这样可配置比较灵活,但是也有个缺点,配置项过于麻烦,类名过长时便很不方便。在最新的master的分支上已经修改为比较短的策略类型。分片的策略总共有三种:

1.平均分片
2.奇偶分片
3.轮询分片

平均分片

public Map<JobInstance, List<Integer>> sharding(final List<JobInstance> jobInstances, final String jobName, final int shardingTotalCount) {
        if (jobInstances.isEmpty()) {
            return Collections.emptyMap();
        }
        // 处理整数部分分片
        Map<JobInstance, List<Integer>> result = shardingAliquot(jobInstances, shardingTotalCount);
        // 处理余数部分分片
        addAliquant(jobInstances, shardingTotalCount, result);
        return result;
    }

分为两步,先处理总的分片数 / 可用作业节点数 算出的每个节点可以获取多少分片,具体的分片如下

如果 3 台作业服务器且分片总数为9,则分片结果为:1=[0,1,2], 2=[3,4,5], 3=[6,7,8];
如果 3台作业服务器且分片总数为8,则分片结果为:1=[0,1,6], 2=[2,3,7], 3=[4,5];
如果 3台作业服务器且分片总数为10,则分片结果为:1=[0,1,2,9], 2=[3,4,5], 3=[6,7,8]。

上图中每个块连续的数字是处理整数时分配的,块中不连续的则是由处理余数部分分配的。

奇偶分片策略
奇偶分片则是以平均分片为基础,通过对作业名取hash值为奇数还是偶数来决定节点ip的升降序排列,最后调用平均分片来进行分片

// 作业名的哈希值为奇数则IP升序.
// 作业名的哈希值为偶数则IP降序.
public final class OdevitySortByNameJobShardingStrategy implements JobShardingStrategy {
    
    private AverageAllocationJobShardingStrategy averageAllocationJobShardingStrategy = new AverageAllocationJobShardingStrategy();
    @Override
    public Map<JobInstance, List<Integer>> sharding(final List<JobInstance> jobInstances, final String jobName, final int shardingTotalCount) {
        long jobNameHash = jobName.hashCode();
        if (0 == jobNameHash % 2) {
            Collections.reverse(jobInstances);
        }
        return averageAllocationJobShardingStrategy.sharding(jobInstances, jobName, shardingTotalCount);
    }
}

轮询分片
轮询分片依旧是以平均分片为基础,作业名的hash值模上作业节点数作为偏移量,一次取节点加入到集合,最后再使用平均分片策略。

public final class RotateServerByNameJobShardingStrategy implements JobShardingStrategy {
    private AverageAllocationJobShardingStrategy averageAllocationJobShardingStrategy = new AverageAllocationJobShardingStrategy();
    @Override
    public Map<JobInstance, List<Integer>> sharding(final List<JobInstance> jobInstances, final String jobName, final int shardingTotalCount) {
        return averageAllocationJobShardingStrategy.sharding(rotateServerList(jobInstances, jobName), jobName, shardingTotalCount);
    }
    private List<JobInstance> rotateServerList(final List<JobInstance> shardingUnits, final String jobName) {
        int shardingUnitsSize = shardingUnits.size();
        // 取作业名的hash值 模上 节点实例获取轮询开始节点
        int offset = Math.abs(jobName.hashCode()) % shardingUnitsSize;
        if (0 == offset) {
            return shardingUnits;
        }
        List<JobInstance> result = new ArrayList<>(shardingUnitsSize);
        for (int i = 0; i < shardingUnitsSize; i++) {
            int index = (i + offset) % shardingUnitsSize;
            result.add(shardingUnits.get(index));
        }
        return result;
    }
}

获取分片对应的作业结点之后,调用回调函数,将分配好的节点设置到注册中心,最后删除分片标识,和正在分片标识。

class PersistShardingInfoTransactionExecutionCallback implements TransactionExecutionCallback {

        private final Map<JobInstance, List<Integer>> shardingResults;

        @Override
        public void execute(final CuratorTransactionFinal curatorTransactionFinal) throws Exception {
            for (Map.Entry<JobInstance, List<Integer>> entry : shardingResults.entrySet()) {
                for (int shardingItem : entry.getValue()) {             
                // 将instance节点id设置到分片序号上       curatorTransactionFinal.create().forPath(jobNodePath.getFullPath(ShardingNode.getInstanceNode(shardingItem)), entry.getKey().getJobInstanceId().getBytes()).and();
                }
            }
            curatorTransactionFinal.delete().forPath(jobNodePath.getFullPath(ShardingNode.NECESSARY)).and();
            curatorTransactionFinal.delete().forPath(jobNodePath.getFullPath(ShardingNode.PROCESSING)).and();
        }
    }

分片完之后则是获取各个节点对应的分片,之前设置时候用的是instanceId,所以获取节点对应的分片时用的也是instanceId

public List<Integer> getShardingItems(final String jobInstanceId) {
        JobInstance jobInstance = new JobInstance(jobInstanceId);
        if (!serverService.isAvailableServer(jobInstance.getIp())) {
            return Collections.emptyList();
        }
        List<Integer> result = new LinkedList<>();
        int shardingTotalCount = configService.load(true).getTypeConfig().getCoreConfig().getShardingTotalCount();
        for (int i = 0; i < shardingTotalCount; i++) {
            if (jobInstance.getJobInstanceId().equals(jobNodeStorage.getJobNodeData(ShardingNode.getInstanceNode(i)))) {
                result.add(i);
            }
        }
        return result;
    }

到这里已经讲完了初始化,还有分片的整个过程,对于这个版本的elastic-job还有两处会触发节点重新进行作业进行分片,则是在registerStartUpInfo -> listenerManager.startAllListeners() ->shardingListenerManager.start(),shardingListenerManager开启了两个节点的监听器,一个市监听作业配置的ShardingTotalCount变化时会触发重新分片,另一个则是instances路径和servers路径下发生变化时,会设置重新分片标记,从而在下一次执行的时候重新进行分片。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三寸花笺

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值