elastic-job的分析

分布式定时任务

Overview

目前分布式架构在大数据量和高并发情况下有很好的表现,但由此带来的技术问题也显而易见。在集群情况下,定时任务存在于每个节点中,如果不对定时妥善任务管理,就会导致定时任务的重复执行造成数据不一致或者脏数据。又或者单机出现cpu或者内存瓶颈时候,在这种情况下,分布式定时任务应运而生。

技术方案

目前较为流行的分布式定时任务主要有quertz+DB,淘宝的TBSchedule、当当的elastic-job。

  1. quartz的集群方案是使用数据库来实现的数据的同步和任务的分配。当某个节点执行任务的时候,通过行锁锁住表,当执行完成之后,修改行中的标志位字段。对于失败的任务,可以又其他的任务继续执行。但是这只是一种串行策略,并不能提高执行效率。
  2. TBSchedule和elastic-job都采用看了分片的思想,通过zookepeer来完成节点的注册和信息的同步。当有节点宕机时候,zookepeer能够及时发现并且重新完成分片任务。两个框架的思想几乎是一样的,TBSchedule在稳定性方面有更大的优势。elastic-job在文档上更加全面,易开发。

一、elastic-job

elastic-job是当当的一个项目,包含Elastic-Job-Lite和Elastic-Job-Cloud两个子项目。 Elastic-Job-Cloud使用Mesos + Docker的解决方案,额外提供资源治理、应用分发以及进程隔离等服务,是一个全量级的解决方案。Elastic-Job-Lite定位为轻量级无中心化解决方案,使用jar包的形式提供分布式任务的协调服务。

1. quzrtz的介绍

elastic-job是基于quartz的二次开发,在quartz的基础之上,加入了选举和分片策略。要了解elastic-job首先要了解quartz。
quartz是由job、jobdetail和trigger、schedule组成。先给出一个比较简单的demo

    public class MyJob implements Job {
        @Override
        public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
            TriggerKey triggerKey = jobExecutionContext.getTrigger().getKey();
            System.out.println("堕落"+triggerKey.getGroup()+":"+triggerKey.getName());
            System.out.println(jobExecutionContext.getJobDetail().hashCode());
        }
    }
        SchedulerFactory schedulerFactory = new StdSchedulerFactory();
        Scheduler scheduler = schedulerFactory.getScheduler();
        //scheduler被停止后,除非重新实例化,否则不能重新启动
        scheduler.start();
        //绑定方法一,直接通过schedule绑定
        JobDetail jobDetail1 = JobBuilder.newJob(MyJob.class).build();
        Trigger trigger0 = TriggerBuilder.newTrigger().withSchedule(CronScheduleBuilder.cronSchedule("*/5 * * * * ?")).withIdentity("1","swc").startNow().build();
        Trigger trigger1 = TriggerBuilder.newTrigger().withSchedule(CronScheduleBuilder.cronSchedule("*/10 * * * * ?")).withIdentity("1","swc").startNow().build();
        scheduler.scheduleJob(jobDetail1,trigger1);
        //scheduler.scheduleJob(jobDetail1,trigger0);

        //绑定方法二,trigger通过名字来绑定job,这个时候,要指定Durability参数,指jobdetai在没有trigger的时候也不销毁。
        JobDetailImpl jobDetail2 = (JobDetailImpl)JobBuilder.newJob(MyJob.class).withIdentity("2","swc").build();
        Trigger trigger2 = TriggerBuilder.newTrigger().withSchedule(CronScheduleBuilder.cronSchedule("*/5 * * * * ?")).withIdentity("2","swc").forJob("2","swc").startNow().build();
        Trigger trigger3 = TriggerBuilder.newTrigger().withSchedule(CronScheduleBuilder.cronSchedule("*/10 * * * * ?")).withIdentity("3","swc").forJob("2","swc").startNow().build();
        jobDetail2.setDurability(true);//没有trigger绑定的job不销毁
        scheduler.addJob(jobDetail2,false);
        scheduler.scheduleJob(trigger2);
        scheduler.scheduleJob(trigger3);
  • Job 就是最基础的执行类,只有一个execute方法
  • JobDetail 表示一个具体的可执行的调度程序, Job 是这个可执行程调度程序所要执行的内容,另外 JobDetail 还包含了这个任务调度的方案和策略(上下文信息)
  • Trigger 是调度策略,和 JobDetail 绑定起来。一个 JobDetail 可以有多个 Trigger ,但是一个 Trigger 只能有一个 JobDetail
  • Scheduler 代表一个调度容器,一个调度容器中可以注册多个 JobDetail 和 Trigger。当 Trigger 与 JobDetail 组合,就可以被 Scheduler 容器调度了
    public void start() throws SchedulerException {

        if (shuttingDown|| closed) {
            throw new SchedulerException(
                    "The Scheduler cannot be restarted after shutdown() has been called.");
        }
        notifySchedulerListenersStarting();

        if (initialStart == null) {
            initialStart = new Date();
            this.resources.getJobStore().schedulerStarted();            
            startPlugins();
        } else {
            resources.getJobStore().schedulerResumed();
        }
        //schedThread这个线程在schedule初始化的时候会创建
        //false 唤醒sigLock对象的所有等待队列的线程
        //true,发出让主循环暂停的信号,以便线程在下一个可处理的时刻暂停
        schedThread.togglePause(false);

        notifySchedulerListenersStarted();
    }
    public Date scheduleJob(JobDetail jobDetail,Trigger trigger) throws SchedulerException {
        
        //把相应的job和trig放到jobstore中,默认使用的RAMJobStore,底层使用hashmap或者treemap来实现。
        resources.getJobStore().storeJobAndTrigger(jobDetail, trig);
        notifySchedulerListenersJobAdded(jobDetail);
        notifySchedulerThread(trigger.getNextFireTime().getTime());
        notifySchedulerListenersSchduled(trigger);

        return ft;
    }
  • schedule 初始化会生成 QuartzSchedulerThread ,并且启动。 QuartzSchedulerThread 的run中有一个大循环,用于控制整个 schedule 的运行,也用于去计时执行任务。
  • 线程池默认有一个 simpThreadPool ,会在 QuartzSchedulerThread.run() 方法中被调用,通过 JobRunShell 后续就是去执行 job 中 execuse 方法。

2. Elastic-Job-Lite的使用

  • 可以通过节点的水平扩展和任务的细化增减弹性伸缩任务处理能力。
  • 需要将一个任务拆解程多个子任务,保证子任务之间没有任何耦合,如果有共享数据需要加锁。
  • 分布式协调,可以支持自我故障的检测和修复,支持失效转移和任务重触发。
  • 节点需要先注册到zookepeer,然后通过选举得到主节点,主节点根据节点数量和分片策略,同步信息到各个节点。其实就是在quartz的基础之上增加了分片策略和注册选举内容。
2.1 选举过程

选举流程图.jpg

以下给出一个简单的demo:

    @Bean(initMethod = "init")
    public ZookeeperRegistryCenter regCenter(@Value("${regCenter.serverList}") final String serverList, @Value("${regCenter.namespace}") final String namespace) {
        //zookepeer的ip和域名
        return new ZookeeperRegistryCenter(new ZookeeperConfiguration(serverList, namespace));
    }
    
    @Resource
    private ZookeeperRegistryCenter regCenter;

    @Bean(initMethod = "init")
    public JobScheduler simpleScheduler(final MyJoB job, //job
    @Value("${simpleChannelCountJob.cron}") final String cron, //定时策略
    @Value("${simpleChannelCountJob.shardingTotalCount}") final int shardingTotalCount,//分片数量
    @Value("${simpleChannelCountJob.shardingItemParameters}") final String shardingItemParameters) {//分片策略0=abc
    	return new SpringJobScheduler(channelCountJob, regCenter, 
        getLiteJobConfiguration(channelCountJob.getClass(), cron, shardingTotalCount, shardingItemParameters));
    }

    private LiteJobConfiguration getLiteJobConfiguration(final Class<? extends SimpleJob> jobClass, final String cron, final int shardingTotalCount, final String shardingItemParameters) {
        return LiteJobConfiguration.newBuilder(new SimpleJobConfiguration(JobCoreConfiguration.newBuilder(
                jobClass.getName(), cron, shardingTotalCount).shardingItemParameters(shardingItemParameters).build(), jobClass.getCanonicalName())).overwrite(true).build();
    }

真正需要了解的类

    public final class ShardingContext {
        /**
        * 作业名称.
        */
        private final String jobName;
        
        /**
        * 作业任务ID.
        */
        private final String taskId;
        
        /**
        * 分片总数.
        */
        private final int shardingTotalCount;
        
        /**
        * 作业自定义参数.
        * 可以配置多个相同的作业, 但是用不同的参数作为不同的调度实例.
        */
        private final String jobParameter;
        
        /**
        * 分配于本作业实例的分片项.
        */
        private final int shardingItem;//上诉分片策略中的0
        
        /**
        * 分配于本作业实例的分片参数.
        */
        private final String shardingParameter;//上诉分片策略中的abc
        
        public ShardingContext(final ShardingContexts shardingContexts, final int shardingItem) {
            jobName = shardingContexts.getJobName();
            taskId = shardingContexts.getTaskId();
            shardingTotalCount = shardingContexts.getShardingTotalCount();
            jobParameter = shardingContexts.getJobParameter();
            this.shardingItem = shardingItem;
            shardingParameter = shardingContexts.getShardingItemParameters().get(shardingItem);
        }
    }
        public class MyJoB implements SimpleJob {
            @Override
            public void execute(final ShardingContext shardingContext) {
                //ShardingContext存储了分片信息和当前节点的上下文信息,根据分片信息去处理
                }
            }
        }
2.2 elastic-job 的选举

zk节点图.png

任务初始化

        //节点首先注册到
        private JobScheduler(final CoordinatorRegistryCenter regCenter, final LiteJobConfiguration liteJobConfig, final JobEventBus jobEventBus, final ElasticJobListener... elasticJobListeners) {
            JobRegistry.getInstance().addJobInstance(liteJobConfig.getJobName(), new JobInstance());
            this.liteJobConfig = liteJobConfig;
            this.regCenter = regCenter;
            List<ElasticJobListener> elasticJobListenerList = Arrays.asList(elasticJobListeners);
            setGuaranteeServiceForElasticJobListeners(regCenter, elasticJobListenerList);
            schedulerFacade = new SchedulerFacade(regCenter, liteJobConfig.getJobName(), elasticJobListenerList);
            jobFacade = new LiteJobFacade(regCenter, liteJobConfig.getJobName(), Arrays.asList(elasticJobListeners), jobEventBus);
        }

任务注册

        /**
        * 初始化作业.在conifg中配置的时候会调用init方法
        */
        public void init() {
            LiteJobConfiguration liteJobConfigFromRegCenter = schedulerFacade.updateJobConfiguration(liteJobConfig);
            JobRegistry.getInstance().setCurrentShardingTotalCount(liteJobConfigFromRegCenter.getJobName(), liteJobConfigFromRegCenter.getTypeConfig().getCoreConfig().getShardingTotalCount());
            JobScheduleController jobScheduleController = new JobScheduleController(
                    createScheduler(), createJobDetail(liteJobConfigFromRegCenter.getTypeConfig().getJobClass()), liteJobConfigFromRegCenter.getJobName());
            //注册任务,将任务名称作为节点名称添加到zk中
            JobRegistry.getInstance().registerJob(liteJobConfigFromRegCenter.getJobName(), jobScheduleController, regCenter);
            //添加任务信息并参与选举
            schedulerFacade.registerStartUpInfo(!liteJobConfigFromRegCenter.isDisabled());
            //启动作业
            jobScheduleController.scheduleJob(liteJobConfigFromRegCenter.getTypeConfig().getCoreConfig().getCron());
        }

节点选举,先到先得原则

    /**
     * 注册作业启动信息.
     * 
     * @param enabled 作业是否启用
     */
    public void registerStartUpInfo(final boolean enabled) {
        listenerManager.startAllListeners();
        //选举主节点
        leaderService.electLeader();
        //服务器信息持久化到zk上
        serverService.persistOnline(enabled);
        //将实例信息添加到zk的instances节点中
        instanceService.persistOnline();
        //重新分片
        shardingService.setReshardingFlag();
        monitorService.listen();
        if (!reconcileService.isRunning()) {
            reconcileService.startAsync();
        }
    }

    /**
     * 选举主节点.
     */
    public void electLeader() {
        log.debug("Elect a new leader now.");
        //进行节点选举,如果选举成功在zk的leader/election/instance路径中填写服务器信息。
        jobNodeStorage.executeInLeader(LeaderNode.LATCH, new LeaderElectionExecutionCallback());
        log.debug("Leader election completed.");
    }

    /**
     * 持久化作业服务器上线信息.
     * @param enabled 作业是否启用
     */
    public void persistOnline(final boolean enabled) {
        if (!JobRegistry.getInstance().isShutdown(jobName)) {
            jobNodeStorage.fillJobNode(serverNode.getServerNode(JobRegistry.getInstance().getJobInstance(jobName).getIp()), enabled ? "" : ServerStatus.DISABLED.name());
        }
    }
2.3 elastic-job 的分片

新的Job实例加入集群
现有的Job实例下线(如果下线的是leader节点,那么先选举然后触发分片算法的执行)
主节点选举

zk分片图.png

以上三种情况会让zookeeper上leader节点的sharding节点上多出来一个necessary的临时节点,主节点每次执行Job前,都会去看一下这个节点,如果有则执行分片算法。

    /**
     * 如果需要分片且当前节点为主节点, 则作业分片.
     * 
     * <p>
     * 如果当前无可用节点则不分片.
     * </p>
     */
    public void shardingIfNecessary() {
        List<JobInstance> availableJobInstances = instanceService.getAvailableJobInstances();
        if (!isNeedSharding() || availableJobInstances.isEmpty()) {
            return;
        }
        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, "");
        //重新在zk写入服务器信息
        resetShardingInfo(shardingTotalCount);
        //获取分片策略
        JobShardingStrategy jobShardingStrategy = JobShardingStrategyFactory.getStrategy(liteJobConfig.getJobShardingStrategyClass());
        //执行分片1、执行策略,返回实例和分片的对应关系2、构建回调函数
        jobNodeStorage.executeInTransaction(new PersistShardingInfoTransactionExecutionCallback(jobShardingStrategy.sharding(availableJobInstances, jobName, shardingTotalCount)));
        log.debug("Job '{}' sharding complete.", jobName);
    }

    /**
     * 分片结束之后.
     * 
     * @param callback 执行操作的回调
     */
    public void executeInTransaction(final TransactionExecutionCallback callback) {
        try {
            CuratorTransactionFinal curatorTransactionFinal = getClient().inTransaction().check().forPath("/").and();
            callback.execute(curatorTransactionFinal);
            curatorTransactionFinal.commit();
        //CHECKSTYLE:OFF
        } catch (final Exception ex) {
        //CHECKSTYLE:ON
            RegExceptionHandler.handleException(ex);
        }
    }

分片结束之后,会将信息同步给各个从节点服务器,从服务器通过监听,获取到服务器和分片的信息

2.4 失败转移,RegistryCenterConnectionStateListener会实时监听和注册中心的连接状态。当出现节点状态变了,会启动状态转移,也就是意味着重新选举主节点和分片等。
posted @ 2021-02-01 16:34  ElliottX4  阅读(200)  评论(0)    收藏  举报