elastic-job-lite 使用的一點心得和坑

a_wei發表於2019-09-29

開篇詞

elastic-job-lite在專案中使用也有兩個多月的時間了,從一開始搜尋網上教程,參考別人使用方法,到後面閱讀原始碼,理解其架構,實現。也寫了幾篇關於ejl的架構,流程處理,資料結構的文章,中間也經歷了很多坑,也有了一些最佳實踐,這篇文章寫一下總結,但不是ejl的結尾,後續還會有ejl的文章,下面附上前幾篇的連結:

  1. elastic-job-lite入門以及架構原理分析
  2. elastic-job-lite 既然去中心化,為何要選舉主節點
  3. elastic-job-lite 資料結構分析

使用心得

ejl中有三種job作業型別,simple, dataflow, script,這三種任務型別都支援cron表示式定時呼叫,也支援頁面單次觸發。

simple是簡單型別,提供介面如下,入參為當前分片的上下文,介面如下,這種簡單型別介面適合處理一些業務邏輯不是很複雜的情況,且在短時間內能完成,ejl中還好,是本地呼叫,如果誇服務呼叫,那麼時間一長,是很容易超時的,排程中心會認為處理失敗,總的來說適合一些高頻,低時的任務,且沒有相對複雜的資料分離處理。

void execute(ShardingContext var1);

dataflow是一種流失資料處理,fetchData入參為當前分片的上下文,processData入參為需要處理的資料,介面如下,這種流式處理介面一般適合大資料量計算,fetchData負責需要處理的資料,processData負責對抓取的資料計算生成結果儲存,也適合一些大量任務的執行,將資料的準備和資料的處理進行了分離,從而在程式碼組織上比較清晰,這種型別的作業一般也會耗時比較長少則幾分鐘,多則幾小時,但一般低頻,一天一次,其任務觸發後在本地機器while(list.size>0)執行,直到fetchData返回的資料集合大小為0,此處一般建議processData中可以開起多執行緒處理資料,加快執行處理時間。如果你需要處理的資料是斷斷續續的生成,那麼這種是不適合的,因為fetchData的時候,可能你的資料還在生成中,導致任務已經完畢,這是可以考慮第一種。

List<T> fetchData(ShardingContext var1);
void processData(ShardingContext var1, List<T> var2);

scritp是一種指令碼型別,其介面為一個介面,它適合一些指令碼作業觸發,也可以說是實現語言不是Java的一些指令碼任務,比如shell寫了一段業務邏輯,python寫個一個彙總邏輯,需要每隔一小時執行一次,這是script就派上用場了,透過命令進行觸發,在script的配置中有一個scriptCommandLine引數,這個引數就是配置執行這個指令碼的命令,執行器會把當前上下文資訊轉化為json引數拼在命令列後面,ejl會定時透過命令觸發你寫的指令碼,script指令碼執行器如下:

public final class ScriptJobExecutor extends AbstractElasticJobExecutor{

protected void process(final ShardingContext shardingContext) {
    //scriptCommandLine從上下文中獲取到,宣告為final
    final String scriptCommandLine = shardingContext.get...
    this.executeScript(shardingContext, scriptCommandLine);
}

private void executeScript(final ShardingContext shardingContext, final String scriptCommandLine){

    CommandLine commandLine = CommandLine.parse(scriptCommandLine);
    commandLine.addArgument("上下文json串" false);
    try {
            //org.apache.commons.exec apache的包
            new DefaultExecutor().execute(commandLine);
    } catch (final IOException ex) {
            throw new JobConfigurationException("Execute script failure.", ex);
    }
}
}

上面介紹了三種作業型別的使用方法和場景,不管是以上那種作業型別,ejl都支援為其新增監聽器,能夠實現在作業開始執行前和執行完成後做一些準備和清理工作,並且是在分散式環境下進行,也就是保證所有節點還未開始執行時,就執行監聽器中的befor方法,在所有節點都執行完成後,在執行after方法,這是非常有幫助的,通常我們都會用到,比如我們在任務開始前需要準備一些基礎資料,在結束需要告知下游系統完成,其介面如下:

public interface ElasticJobListener {    
    void beforeJobExecuted(ShardingContexts var1);    
    void afterJobExecuted(ShardingContexts var1);
}

ejl是不支援作業連貫的,比如,我們的job依賴上游系統作業的完成,怎麼做呢?

  1. 第一種方式在before中我們輪詢監聽上游作業狀態,就不準備資料,這樣fetchData就抓不到資料,作業就不執行,事實上我覺監聽器中兩個方法應該有個bool返回值更好
  2. 我們可以利用web提供的單詞觸發的api呼叫,我們不配置cron表示式,當上遊系統作業完成時可以透過http請求呼叫我們的api啟動我們的job

坑和解決方案

這裡記錄一下使用過程中踩過的坑

第一坑,在spring boot中假設我們開啟兩個任務,配置如下,這一看沒什麼問題,而且一般情況下程式設計師都習慣cv大法,然後改一改,但總會有一些忽略的地方,下面的配置兩個job都有共同的jobConfig方法,這就是copy的結果,類名改了,Bean的Name改了,job名稱也改了,就是方法名沒改,並且編譯階段和啟動都不會報錯,但導致的問題就是隻有一個job作業會註冊成功,當時我們排查了好久才發現這個問題,因為以前也確實沒遇到不同類中方法重名導致的問題,這是因為spting 會把方法名,返回值作為構造一個物件的key,此處方法返回型別和方法名一致,導致此物件只會被建立一次。

public class JobConfig1 {        
    @Resource    
    private ZookeeperRegistryCenter regCenter;    
    @Bean(name = "job1")    
    public DataflowJob jobConifg() {        
        return new Job1()();    
    }
}
public class JobConfig2 {        
    @Resource    
    private ZookeeperRegistryCenter regCenter;    
    @Bean(name = "job2")    
    public DataflowJob jobConifg() {        
        return new  Job1();    
    }
}

第二坑,就是上面提到的監聽器,問題是分散式環境下會出現多節點都呼叫befor和after的問題,其實也就是併發的問題,我們分析如下程式碼,透過分散式一致性協調服務判斷是否所有節點開始,如果是,則執行before方法,如果不是,則鎖住進入睡眠狀態,最終當其中某一個節點執行完before方法清楚標記,喚醒睡眠的節點,問題出在哪裡了?我們看isAllStarted()方法,就是判斷節點總數是不是和zk上寫入的節點數一樣,也就是說假設1,2,3三個節點此時同一時刻都進入isAllStarted()方法,他們都往zk上寫入了,都是有可能判斷成功的,則會都進入before方法。

  1. 正確的做法是我們應該在doBefore...方法加一個分散式鎖,即便isAllStarted都判斷成功了,也有分散式鎖保障,只有獲得鎖的方法才能執行doBefore...方法。
  2. 網上也有一種做法,就是在i'sAllStated()再加一個條件,只有指定的分片才能執行doBefore...方法,但這有一種問題,就是這個分片被禁用後,就會導致任務無法執行,因為所有分片都在睡眠中,都無法被喚醒。
if (guaranteeService.isAllStarted()) {
            doBeforeJobExecutedAtLastStarted(shardingContexts);
            guaranteeService.clearAllStartedInfo();
            return;
}
long before = timeService.getCurrentMillis();
try {
     synchronized (startedWait) {
          startedWait.wait(startedTimeoutMilliseconds);
     }
} catch (final InterruptedException ex) {
    Thread.interrupted();
}      
public boolean isAllStarted() {   
     return this.jobNodeStorage.isJobNodeExisted("guarantee/started") 
     && 
     this.configService.load(false).getTypeConfig()
        .getCoreConfig().getShardingTotalCount() == 
     this.jobNodeStorage.getJobNodeChildrenKeys("guarantee/started").size();   
}

第一種解決方案程式碼:

leaderLatch = new LeaderLatch(client, path, id);
   LeaderLatchListener leaderLatchListener = new LeaderLatchListener() {
        @Override
         public void isLeader() {
            doBefore....
            clearAllStartedInfo
            //釋放鎖
         }
        @Override
        public void notLeader() {
            //休眠,等待喚醒   
        }
   };
leaderLatch.addListener(leaderLatchListener);
leaderLatch.start();

第二種解決方案程式碼:

while(shardingContexts.getShardingItemParameters().containsKey(0)) {
    if (guaranteeService.isAllStarted()) {
            doBeforeJobExecutedAtLastStarted(shardingContexts);
            guaranteeService.clearAllStartedInfo();
            return;
    }
    Thread.sleep(10)
}
//否則直接休眠
try {
     synchronized (startedWait) {
          startedWait.wait(startedTimeoutMilliseconds);
     }
} catch (final InterruptedException ex) {
    Thread.interrupted();
}

elastic-job-lite 使用的一點心得和坑

本作品採用《CC 協議》,轉載必須註明作者和本文連結
那小子阿偉

相關文章