分布式系统中的定时任务全解(二)

2019-08-21 14:24:33 浏览数 (1)

概述

上一篇分布式系统中的定时任务全解(一)中对定时任务和定时任务的基础使用方式进行了说明。这一小节,把分布式场景下的定时任务进行一个大致的讲解。

什么是分布式场景呢,当单台服务器服务能力不够的时候,就需要更多的服务器进行水平向的扩展,由多台服务器分工(任务量上的水平划分,而非业务线上的垂直划分)的方式来增强服务能力,提供更强的并发请求处理,更短的时间响应。

像第一节说到的定时任务使用场景,大多是一次任务执行仅能有一个服务器在执行,如果是所有服务器都在执行相同的任务,一个是会造成错误,就算不会造成错误,很多服务器在做重复的工作也是极大的浪费。

所以,分布式场景下定时任务要做的一个基本难点就是:怎么让某一个定时任务,在一个触发时点上仅有一台服务器在执行。

更进一步,如果,你的定时任务涉及到很多同类型的数据要处理,比如说要处理100个输入文件,处理方式相同;再比如说你的数据库已经做了分库处理,业务数据被写入到了10个数据库实例中,处理方式相同。那么此时,可以让更多台服务器执行定时任务,每台执行其中的一部分,比如10个输入文件;再比如1个数据库实例中的业务数据。

以上两种场景怎么办呢?第一种很简单,后续会提供三种方式去做:1.设置某一台为任务执行服务器,其他服务器不执行;2.使用quartz的集群功能,实现某一台执行;3.使用当当开源的elastic-job,实现某一台执行。第二种场景只有第一种场景中的第3中方式可以做到。

接下来逐个看一下。

实现分布式的方式

设置某一台为任务执行服务器

这种方式可以采用环境变量的方式来实现,定时任务运行时检查本机的环境变量值是否为可执行,如果是则执行定时任务,如果不是则直接返回。

代码语言:javascript复制
@Value("${ISTIMERRUNNER}")
private String isTimerRunner;

@Scheduled(cron="0 0 0 * * ? ")
public void task(){
    try {
        if("true".equals(isTimerRunner)){
                //do something.....
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

这里有一个需要注意的事项,如果集群环境下,你使用了脚本部署的方式,而且是类似于作者的方式。也就是先把文件拷贝到一台服务器,启动好后,调用脚本(脚本参见:http://www.voidcn.com/blog/BuquTianya/article/p-5760349.html),逐个部署到其他服务器。那么,你就需要注意一下了。

远程ssh调用startup.sh时,tomcat取不到环境变量,这里需要把startup.sh的顶部进行修改:

代码语言:javascript复制
#!/bin/sh --login

当然这里还有另外一种方式,就是在tomcat的bin目录中添加一个setenv.sh文件,startup.sh执行时会加载其中的内容。

代码语言:javascript复制
export ISTIMERRUNNER=true

这种方式有十分明显的缺陷:1.单点,当任务执行节点出现问题时,整个定时任务全部over;2.资源分配不均衡,随着定时任务的增多,任务执行服务器的资源占用压力会越来越大。

当然了,这是在技术能力不够的时候,最简单有效的实现方式。

使用quartz的集群功能

quartz这个老牌的定时任务执行工具,在集群方面也提供了很好的支持。quartz的集群是借助数据库来实现的,所有的服务器实例共享一套数据库表中存储的任务、触发器和调度器信息,实现一个时间点,同一个任务仅有一台服务器在执行。而且提供了负载均衡和failover失败转移功能。

quartz的负载均衡大概是这样的策略:对于需要调度很多任务的调度器,会近似随机的选择服务器执行;对于调度的任务数比较少的(1个或者2个)的触发器,会尽量的在同一台服务器上执行。(可以参见这里:http://www.quartz-scheduler.org/documentation/quartz-2.2.x/tutorials/tutorial-lesson-11.html)

quartz的集群使用也不复杂,接下来一起看一下:

1.导入数据库表

quartz的集群是基于数据库实现的,所以首先要把数据库表结构创建好。创建脚本在quartz的完整下载包里可以找到(官网下载地址:http://www.quartz-scheduler.org/downloads/)。

解压之后,在docs/dbTable目录下可以找到你想要的所有常见数据库类型的创建脚本。

我使用的是sql数据库,所以使用了tables_mysql_innodb.sql。

这里导入的时候遇到了一个问题,就是创建索引时索引字段过长。这里我采用的方法是把所有的scheduler的长度变成了50,修改之后也就是会限制所有的scheduler的名字在50个字节以内。

2.创建支持集群的scheduler

集群和非集群的配置不同,关键就在于scheduler,集群时需要给scheduler配置数据源、将org.quartz.jobStore.isClustered设置为true、以及配置quartz.properties属性文件。详细的实现方式可以参见:http://sundoctor.iteye.com/blog/486055

使用elastic-job

elastic-job是基于quartz实现的,最大的不同点是elastic-job把做为共享中心的数据库换成了zookeeper。所以要使用elastic-job首先要安装zookeeper。

安装好zookeeper之后,使用elastic-job也不难,它做了很好的spring集成支持,只需要配置注册中心和执行任务的job即可。

以下是一个配置文件的示例:

代码语言:javascript复制
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context" 
    xmlns:reg="http://www.dangdang.com/schema/ddframe/reg" 
    xmlns:job="http://www.dangdang.com/schema/ddframe/job" 
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.dangdang.com/schema/ddframe/reg http://www.dangdang.com/schema/ddframe/reg/reg.xsd http://www.dangdang.com/schema/ddframe/job http://www.dangdang.com/schema/ddframe/job/job.xsd ">
    <context:component-scan base-package="com.dangdang.example.elasticjob" />
    <context:property-placeholder location="classpath:conf/*.properties" />

    <reg:zookeeper id="regCenter" server-lists="${serverLists}" namespace="${namespace}" base-sleep-time-milliseconds="${baseSleepTimeMilliseconds}" max-sleep-time-milliseconds="${maxSleepTimeMilliseconds}" max-retries="${maxRetries}" nested-port="${nestedPort}" nested-data-dir="${nestedDataDir}" />

    <job:simple id="simpleElasticJob" class="com.dangdang.example.elasticjob.spring.job.SimpleJobDemo" registry-center-ref="regCenter" sharding-total-count="${simpleJob.shardingTotalCount}" cron="${simpleJob.cron}" sharding-item-parameters="${simpleJob.shardingItemParameters}" monitor-execution="${simpleJob.monitorExecution}" monitor-port="${simpleJob.monitorPort}" failover="${simpleJob.failover}" description="${simpleJob.description}" disabled="${simpleJob.disabled}" overwrite="${simpleJob.overwrite}" />
</beans>

更详细的示例可以到github下载:https://github.com/ZhangShufan15/elastic-job

这里简单的说一下elastic-job相对于quartz的优势:

1.使用zookeeper做为协调,更加轻量级,这一点对于使用者来说也是一个困难项,因为无论再小的服务也有一个数据库,所以定时任务也使用数据库,那么用起来省事一点。但使用zookeeper一方面速度快,另一方面是不占用现有数据库的连接和计算资源。

2.支持任务的分片,quartz同一时点,同一任务只能在一台机器上运行,但是elastic-job可以在多台机器上运行,并且能够指定每台服务器上运行的输入分片。比如业务数据在10个数据库,这里总共有5台服务器,那么每台服务器在同一个时点,仅处理其中的2个数据库。做到将可纵向切分的任务,切分给不同的服务器,充分利用资源,加快计算速度。

elastic-job怎么做到的分片,在不同的场景下我们该怎么使用elastic-job,接下来的一节将从源代码的角度进行讲解。

0 人点赞