分布式任务调度的解决方案

简介

 随着系统规模的发展,定时任务数量日益增多,任务也变得越来越复杂,尤其是在分布式环境下,存在多个业务系统,每个业务系统都有定时任务的需求,如果都在自身系统中调度,一方面增加业务系统的复杂度,另一方面也不方便管理,因此需要有一个任务平台对分散的任务进行统一管理调度,基于目前的情况,任务平台需要支持以下几个方面:

1、任务统一管理,提供图形化界面对任务进行配置和调度。
2、任务并发控制,同一个任务在同一时间只能允许一个执行。
3、任务弹性扩容,可根据繁忙情况动态增减服务器分摊压力,对大任务进行分片处理。
4、任务依赖问题,能够处理任务包含子任务的情况,前一个完成后触发子任务执行。
5、支持多类型的任务,支持Spring Bean、Shell等。
6、任务节点高可用,任务节点异常或者繁忙时能够转移到其他节点执行。
7、调度中心高可用,支持集群部署,避免出现单点故障。
8、执行状态监控,方便查看任务执行状态,异常情况告警,支持多渠道通知。

发展史

定时任务随着技术发展,从单线程调度到多线程调度,从单机部署到集群部署,从独立执行到多任务协同执行。
file

第一阶段
单线程调度,在Java1.5之前,基于线程的等待(sleep或wait)机制定时执行,需要开发者实现调度逻辑,单个线程(Thread)处理单个任务有些浪费,但是一个线程(Timer)处理多个任务容易因为某个任务繁忙导致其他任务阻塞。

第二阶段
线程池调度,在Java1.5开始提供ScheduledExecutorService调度线程池,调度线程池支持固定的延时和固定间隔模式,对于需要在某天或者某月的时间点执行就不大方便,需要计算时间间隔,转换成启动延时和固定间隔,处理起来比较麻烦。

第三阶段
Spring任务调度,Spring简化了任务调度,通过@Scheduled注解支持将某个Bean的方法定时执行,除了支持固定延时和固定间隔模式外,还支持cron表达式,使得定时任务的开发变得极其简单。

第四阶段
Quartz任务调度,在任务服务集群部署下,Quartz通过数据库锁,实现任务的调度并发控制,避免同一个任务同时执行的情况。Quartz通过Scheduler提供了任务调度API,开发可以基于此开发自己的任务调度管理平台。

第五阶段
分布式任务平台,提供一个统一的平台,无需再去做和调度相关的开发,业务系统只需要实现具体的任务逻辑,自动注册到任务调度平台,在上面进行相关的配置就完成了定时任务的开发。

解决方案

现在分布式下任务调度有很多解决方案,可以基于Quartz开发任务管理平台,也可以使用开源的任务调度平台,比如xxl-job,elastic-job。

XXL-JOB
大众点评员工徐雪里于2015年发布的分布式任务调度平台,是一个轻量级分布式任务调度框架,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。官方地址:https://www.xuxueli.com/xxl-job/

file

ELASTIC-JOB
当当开发的弹性分布式任务调度系统,功能丰富强大,采用zookeeper实现分布式协调,实现任务高可用以及分片,并且可以支持云开发,由两个相互独立的子项目Elastic-Job-Lite和Elastic-Job-Cloud组成。官方地址:http://elasticjob.io/docs/elastic-job-lite/00-overview/

file

方案对比

file

集成示例

下面以集成xxl-job为例,xxl-job将定时任务分为两个部分:1、调度中心;2、执行器。因此集成xxl-job需要分成两个步骤,1、部署调度中心,2、业务系统对接(执行器)。架构图如下:
file

部署调度中心

部署前先确定部署方案,测试环境可以使用1个调度中心 + 1个mysql服务,生产环境建议使用2个调度中心 + mysql主从服务,保证高可用。部署前需确保已经准备:Jdk1.8,Maven、mysql,部署步骤如下:

下载xxl-job源码:http://gitee.com/xuxueli0323/xxl-job/,使用maven编译打包,生成部署的xxl-job-admin.jar。

创建数据库,并初始化相关的表,脚本参考源码目录doc/db/tables_xxl_job.sql

创建部署目录,并配置数据库等配置,可在打包之前,在源码里面application.properties进行配置,也可以在部署目录里面单独创建application.properties文件里面进行配置(推荐,spring boot优先加载启动目录下的配置,可以避免以后更改数据库等配置时还需要重新打包源码)。

运行管理平台(请先确保已经配置好Java执行环境,Jdk1.8或者以上)

具体步骤参考许雪里博客:https://www.cnblogs.com/xuxueli/p/5021979.html,部署可参考以下脚本:

#下载源码
cd /root/
wget https://github.com/xuxueli/xxl-job/archive/v2.0.1.zip
unzip v2.0.1.zip
mv xxl-job-2.0.1 xxl-job

#修改配置文件
vim /root/xxl-job/xxl-job-admin/src/main/resources/application.properties
## 修改mysql、邮件等配置

#编译
cd /root/xxl-job
mvn clean package

# 创建部署目录
mkdir -p /xxl-job
cd /root/xxl-job/xxl-job-admin/target/
cp /root/xxl-job/xxl-job-admin/target/xxl-job-admin-2.0.1.jar /xxl-job/xxl-job-admin-2.0.1.jar


#mysql 数据初始化(用户名和密码为root,数据库编码推荐使用utf8mb4)
mysql -u root -proot -e "source /root/xxl-job/doc/db/tables_xxl_job.sql;"

#启动
nohup java -jar /xxl-job/xxl-job-admin-2.0.1.jar > /dev/null >& 1 &

#检查 admin 123456
curl http://localhost:8080/xxl-job-admin

调度中心启动成功登录后如下,默认用户名admin,默认密码123456,密码可在配置文件中更改。

业务系统对接

业务系统对接调度中心,需要根据当前项目的框架进行配置,可以参考源码xxl-job-executor-samples下例子,下面以业务系统基于spring boot框架进行集成。

配置执行器

@Configuration
public class XxlJobConfig {
    @Value("${spring.application.name:}")
    private String springAppName;

    @Value("${xxl.job.admin.addresses}")
    private String adminAddresses;

    @Value("${xxl.job.executor.appname:}")
    private String appName;

    @Value("${xxl.job.executor.ip:}")
    private String ip;

    @Value("${xxl.job.executor.port:9999}")
    private int port;

    @Value("${xxl.job.accessToken:}")
    private String accessToken;

    @Value("${xxl.job.executor.logpath:job-logs}")
    private String logPath;

    @Value("${xxl.job.executor.logretentiondays:7}")
    private int logRetentionDays;

    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
        if (StringUtils.isEmpty(appName)) {
            if (StringUtils.isEmpty(springAppName)) {
                throw new IllegalStateException("missing xxl-job appname config");
            }
            appName = springAppName;
        }
        xxlJobSpringExecutor.setAppName(appName);
        xxlJobSpringExecutor.setIp(ip);
        xxlJobSpringExecutor.setPort(port);
        xxlJobSpringExecutor.setAccessToken(accessToken);
        xxlJobSpringExecutor.setLogPath(logPath);
        xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);

        return xxlJobSpringExecutor;
    }
}

简化配置说明:
1、spring boot应用基本都有appname,默认使用spring app name配置。
2、ip地址在多网卡、容器的时候需要指定,否则的话,使用默认就可以,spring-cloud-commons中提供了InetUtils工具类,可以帮助获取IP
3、port可以默认指定一个,如果多个服务部署在同一台服务器上,可以通过检测获取或者规划分配。
4、logpath最好指定在应用目录下,最好不要使用绝对路径,避免和其应用冲突。
5、logretentiondays日志保留天数不用太大,根据需要设置,默认给一个较短的时间即可。

开发定时任务

定义定时任务有两种方式:1、2.1.2或者之后版本可以直接在方法上加@XxlJob来声明任务;2、2.1.2之前版本每个任务需要单独开发一个Bean,实现IJobHandler接口,并且在类上加@JobHandler注解。第二种方式较麻烦,推荐使用第一种方式(目前还没稳定版)。

基于@XxlJob注解代码方式(建议制定名称,和调度中心配置保持一致)

@Component
public class SampleXxlJob {
    private static Logger logger = LoggerFactory.getLogger(SampleXxlJob.class);

    /**
     * 1、简单任务示例(Bean模式)
     */
    @XxlJob("demoJobHandler")
    public ReturnT<String> demoJobHandler(String param) throws Exception {
        XxlJobLogger.log("XXL-JOB, Hello World.");
        for (int i = 0; i < 5; i++) {
            XxlJobLogger.log("beat at:" + i);
            TimeUnit.SECONDS.sleep(2);
        }
        return ReturnT.SUCCESS;
    }

    /**
     * 2、分片广播任务
     */
    @XxlJob("shardingJobHandler")
    public ReturnT<String> shardingJobHandler(String param) throws Exception {
        // 分片参数
        ShardingUtil.ShardingVO shardingVO = ShardingUtil.getShardingVo();
        XxlJobLogger.log("分片参数:当前分片序号 = {}, 总分片数 = {}", shardingVO.getIndex(), shardingVO.getTotal());
        // 业务逻辑
        for (int i = 0; i < shardingVO.getTotal(); i++) {
            if (i == shardingVO.getIndex()) {
                XxlJobLogger.log("第 {} 片, 命中分片开始处理", i);
            } else {
                XxlJobLogger.log("第 {} 片, 忽略", i);
            }
        }
        return ReturnT.SUCCESS;
    }
}
基于@JobHandler代码方式

@JobHandler(value="demoJobHandler")
@Component
public class DemoJobHandler extends IJobHandler {
    @Override
    public ReturnT<String> execute(String param) throws Exception {
        XxlJobLogger.log("XXL-JOB, Hello World.");
        for (int i = 0; i < 5; i++) {
            XxlJobLogger.log("beat at:" + i);
            TimeUnit.SECONDS.sleep(2);
        }
        return SUCCESS;
    }
}

配置定时任务

1、配置定时任务,需要先配置执行器,推荐使用自动注册方式,避免集群部署时还需要调整机器地址,添加界面如下(注意appname要和业务系统中配置一致):


file

2、添加完执行器后,添加任务,JobHandler要和代码中配置的名称一致,执行器集群部署可以通过配置路由方式来控制执行,xxl-job调度只支持cron表达式。


file

3、启动或者执行任务,查询执行日志、注册节点等


file

集成踩坑记录

1、任务服务器必须做时钟同步,执行器时钟不能调度中心180秒,否则将会导致调度失败(RPC框架限制)
2、调度任务的时间间隔低于实际执行耗时,导致产生较大的调度日志;
3、尽量避免短任务,比如秒级的任务会导致大量数据库锁影响性能;
4、调度日志量偏大导致查询慢,由于日志都记录在数据库,需要定时清理;
5、自动注册时服务器多网卡导致调度失败,注册时需指定网卡IP;

参考:
http://blog.freshfood.cn/article/39
https://www.cnblogs.com/xuxueli/p/5021979.html
https://www.xuxueli.com/page/projects.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容