关于作业分片,首先先看下有哪些地方会促使作业进行分片
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路径下发生变化时,会设置重新分片标记,从而在下一次执行的时候重新进行分片。