1. 程式人生 > >分散式系統中的定時任務全解(二)

分散式系統中的定時任務全解(二)

概述

上一篇分散式系統中的定時任務全解(一)中對定時任務和定時任務的基礎使用方式進行了說明。這一小節,把分散式場景下的定時任務進行一個大致的講解。

什麼是分散式場景呢,當單臺伺服器服務能力不夠的時候,就需要更多的伺服器進行水平向的擴充套件,由多臺伺服器分工(任務量上的水平劃分,而非業務線上的垂直劃分)的方式來增強服務能力,提供更強的併發請求處理,更短的時間響應。

像第一節說到的定時任務使用場景,大多是一次任務執行僅能有一個伺服器在執行,如果是所有伺服器都在執行相同的任務,一個是會造成錯誤,就算不會造成錯誤,很多伺服器在做重複的工作也是極大的浪費。

所以,分散式場景下定時任務要做的一個基本難點就是:怎麼讓某一個定時任務,在一個觸發時點上僅有一臺伺服器在執行。

更進一步,如果,你的定時任務涉及到很多同類型的資料要處理,比如說要處理100個輸入檔案,處理方式相同;再比如說你的資料庫已經做了分庫處理,業務資料被寫入到了10個數據庫例項中,處理方式相同。那麼此時,可以讓更多臺伺服器執行定時任務,每臺執行其中的一部分,比如10個輸入檔案;再比如1個數據庫例項中的業務資料。

以上兩種場景怎麼辦呢?第一種很簡單,後續會提供三種方式去做:1.設定某一臺為任務執行伺服器,其他伺服器不執行;2.使用quartz的叢集功能,實現某一臺執行;3.使用噹噹開源的elastic-job,實現某一臺執行。第二種場景只有第一種場景中的第3中方式可以做到。

接下來逐個看一下。

實現分散式的方式

設定某一臺為任務執行伺服器

這種方式可以採用環境變數的方式來實現,定時任務執行時檢查本機的環境變數值是否為可執行,如果是則執行定時任務,如果不是則直接返回。

@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://blog.csdn.net/buqutianya/article/details/51062384),逐個部署到其他伺服器。那麼,你就需要注意一下了。

遠端ssh呼叫startup.sh時,tomcat取不到環境變數,這裡需要把startup.sh的頂部進行修改:

#!/bin/sh --login

當然這裡還有另外一種方式,就是在tomcat的bin目錄中新增一個setenv.sh檔案,startup.sh執行時會載入其中的內容。

export ISTIMERRUNNER=true

這種方式有十分明顯的缺陷:1.單點,當任務執行節點出現問題時,整個定時任務全部over;2.資源分配不均衡,隨著定時任務的增多,任務執行伺服器的資源佔用壓力會越來越大。

當然了,這是在技術能力不夠的時候,最簡單有效的實現方式。

使用quartz的叢集功能

quartz這個老牌的定時任務執行工具,在叢集方面也提供了很好的支援。quartz的叢集是藉助資料庫來實現的,所有的伺服器例項共享一套資料庫表中儲存的任務、觸發器和排程器資訊,實現一個時間點,同一個任務僅有一臺伺服器在執行。而且提供了負載均衡和failover失敗轉移功能。

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即可。

以下是一個配置檔案的示例:

<?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>

這裡簡單的說一下elastic-job相對於quartz的優勢:

1.使用zookeeper做為協調,更加輕量級,這一點對於使用者來說也是一個困難項,因為無論再小的服務也有一個數據庫,所以定時任務也使用資料庫,那麼用起來省事一點。但使用zookeeper一方面速度快,另一方面是不佔用現有資料庫的連線和計算資源。

2.支援任務的分片,quartz同一時點,同一任務只能在一臺機器上執行,但是elastic-job可以在多臺機器上執行,並且能夠指定每臺伺服器上執行的輸入分片。比如業務資料在10個數據庫,這裡總共有5臺伺服器,那麼每臺伺服器在同一個時點,僅處理其中的2個數據庫。做到將可縱向切分的任務,切分給不同的伺服器,充分利用資源,加快計算速度。

elastic-job怎麼做到的分片,在不同的場景下我們該怎麼使用elastic-job,接下來的一節將從原始碼的角度進行講解。