開篇詞
elastic-job-lite在專案中使用也有兩個多月的時間了,從一開始搜尋網上教程,參考別人使用方法,到後面閱讀原始碼,理解其架構,實現。也寫了幾篇關於ejl的架構,流程處理,資料結構的文章,中間也經歷了很多坑,也有了一些最佳實踐,這篇文章寫一下總結,但不是ejl的結尾,後續還會有ejl的文章,下面附上前幾篇的連結:
使用心得
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依賴上游系統作業的完成,怎麼做呢?
- 第一種方式在before中我們輪詢監聽上游作業狀態,就不準備資料,這樣fetchData就抓不到資料,作業就不執行,事實上我覺監聽器中兩個方法應該有個bool返回值更好
- 我們可以利用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方法。
- 正確的做法是我們應該在doBefore...方法加一個分散式鎖,即便isAllStarted都判斷成功了,也有分散式鎖保障,只有獲得鎖的方法才能執行doBefore...方法。
- 網上也有一種做法,就是在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();
}
歡迎大家關注微信公眾號:“golang那點事”,更多精彩期待你的到來