分佈式作業系統 Elastic-Job-Lite 源碼分析 —— 作業失效轉移

摘要: 原創出處 http://www.iocoder.cn/Elastic-Job/job-failover/ 「芋道源碼」歡迎轉載,保留摘要,謝謝!

本文基於 Elastic-Job V2.1.5 版本分享

  • 1. 概述
  • 2. 作業節點崩潰監聽
  • 3. 作業失效轉移
  • 4. 獲取作業分片上下文集合
  • 5. 監聽作業失效轉移功能關閉
  • 666. 彩蛋

1. 概述

本文主要分享 Elastic-Job-Lite 作業失效轉移

當作業節點執行作業異常崩潰時,其所分配的作業分片項在下次重新分片之前不會被重新執行。開啓失效轉移功能後,這部分作業分片項將被其他作業節點抓取後“執行”。爲什麼此處的執行打引號呢??下文我們會分享到噢,賣個關子。

筆者對失效轉移理解了蠻久時間,因此引用官方對它的解釋,讓你能更好的理解:

來源地址:https://my.oschina.net/u/719192/blog/506062 失效轉移: 運行中的作業服務器崩潰不會導致重新分片,只會在下次作業啓動時分片。啓用失效轉移功能可以在本次作業執行過程中,監測其他作業服務器空閒,抓取未完成的孤兒分片項執行。 -- 分隔符 -- 來源地址:http://dangdangdotcom.github.io/elastic-job/elastic-job-lite/03-design/lite-design/ 實現失效轉移功能,在某臺服務器執行完畢後主動抓取未分配的分片,並且在某臺服務器下線後主動尋找可用的服務器執行任務。

這樣看概念可能還是比較難理解,代碼搞起來!

涉及到主要類的類圖如下( 打開大圖 ):

  • 粉色的類在 com.dangdang.ddframe.job.lite.internal.failover 包下,實現了 Elastic-Job-Lite 作業失效轉移。
  • FailoverService,作業失效轉移服務。
  • FailoverNode,作業失效轉移數據存儲路徑。
  • FailoverListenerManager,作業失效轉移監聽管理器。

你行好事會因爲得到讚賞而愉悅 同理,開源項目貢獻者會因爲 Star 而更加有動力 爲 Elastic-Job 點贊!傳送門

2. 作業節點崩潰監聽

當作業節點崩潰時,監聽器 JobCrashedJobListener 會監聽到該情況,進行作業失效轉移處理。

// JobCrashedJobListener.java
class JobCrashedJobListener extends AbstractJobListener {

   @Override
   protected void dataChanged(final String path, final Type eventType, final String data) {
       if (isFailoverEnabled() && Type.NODE_REMOVED == eventType
               && instanceNode.isInstancePath(path)) { // /${JOB_NAME}/instances/${INSTANCE_ID}
           String jobInstanceId = path.substring(instanceNode.getInstanceFullPath().length() + 1);
           if (jobInstanceId.equals(JobRegistry.getInstance().getJobInstance(jobName).getJobInstanceId())) {
               return;
           }
           List<Integer> failoverItems = failoverService.getFailoverItems(jobInstanceId); // /${JOB_NAME}/sharding/${ITEM_ID}/failover
           if (!failoverItems.isEmpty()) {
               for (int each : failoverItems) {
                   failoverService.setCrashedFailoverFlag(each);
                   failoverService.failoverIfNecessary();
               }
           } else {
               for (int each : shardingService.getShardingItems(jobInstanceId)) { // /${JOB_NAME}/sharding/${ITEM_ID}/instance
                   failoverService.setCrashedFailoverFlag(each);
                   failoverService.failoverIfNecessary();
               }
           }
       }
   }
}
  • 通過判斷 /${JOB_NAME}/instances/${INSTANCE_ID} 被移除,執行作業失效轉移邏輯。❓說好的作業節點崩潰呢?經過確認,目前這塊存在 BUG,未判斷作業節點是否爲奔潰。所以在當前版本,作業失效轉移面向的是所有作業節點關閉邏輯,不僅限於作業崩潰關閉。
  • 優先調用 FailoverService#getFailoverItems(...) 方法,獲得關閉作業節點( ${JOB_INSTANCE_ID} )對應的 ${JOB_NAME}/sharding/${ITEM_ID}/failover 作業分片項。 若該作業分片項爲空,再調用 ShardingService#getShardingItems(...) 方法,獲得關閉作業節點( ${JOB_INSTANCE_ID} )對應的 /${JOB_NAME}/sharding/${ITEM_ID}/instance 作業分片項。 爲什麼是這樣的順序呢?放在 FailoverService#failoverIfNecessary() 一起講。這裏先看下 FailoverService#getFailoverItems(...) 方法的實現: // FailoverService public List<Integer> getFailoverItems(final String jobInstanceId) { List<String> items = jobNodeStorage.getJobNodeChildrenKeys(ShardingNode.ROOT); List<Integer> result = new ArrayList<>(items.size()); for (String each : items) { int item = Integer.parseInt(each); String node = FailoverNode.getExecutionFailoverNode(item); // ${JOB_NAME}/sharding/${ITEM_ID}/failover if (jobNodeStorage.isJobNodeExisted(node) && jobInstanceId.equals(jobNodeStorage.getJobNodeDataDirectly(node))) { result.add(item); } } Collections.sort(result); return result; }
  • 調用 FailoverService#setCrashedFailoverFlag(...) 方法,設置失效的分片項標記 /${JOB_NAME}/leader/failover/items/${ITEM_ID}。該數據節點爲永久節點,存儲空串( "")。 // FailoverService.java public void setCrashedFailoverFlag(final int item) { if (!isFailoverAssigned(item)) { jobNodeStorage.createJobNodeIfNeeded(FailoverNode.getItemsNode(item)); // /¨E123EJOB¨E95ENAME¨E125E/leader/failover/items/<annotation encoding="application style="color: rgb(128, 128, 128);overflow-wrap: inherit !important;word-break: inherit !important;" span="" class="hljs-comment" encoding=""application"><span class="katex-html" aria-hidden="true" style="overflow-wrap: inherit !important;word-break: inherit !important;"><span class="strut" style="height:1em;vertical-align:-0.25em;" style="overflow-wrap: inherit !important;word-break: inherit !important;">¨<span class="mord mathit" style="margin-right:0.05764em;" style="overflow-wrap: inherit !important;word-break: inherit !important;">E123<span class="mord mathit" style="margin-right:0.05764em;" style="overflow-wrap: inherit !important;word-break: inherit !important;">E<span class="mord mathit" style="margin-right:0.09618em;" style="overflow-wrap: inherit !important;word-break: inherit !important;">J<span class="mord mathit" style="margin-right:0.02778em;" style="overflow-wrap: inherit !important;word-break: inherit !important;">O<span class="mord mathit" style="margin-right:0.05017em;" style="overflow-wrap: inherit !important;word-break: inherit !important;">B¨<span class="mord mathit" style="margin-right:0.05764em;" style="overflow-wrap: inherit !important;word-break: inherit !important;">E95<span class="mord mathit" style="margin-right:0.05764em;" style="overflow-wrap: inherit !important;word-break: inherit !important;">E<span class="mord mathit" style="margin-right:0.10903em;" style="overflow-wrap: inherit !important;word-break: inherit !important;">N<span class="mord mathit" style="overflow-wrap: inherit !important;word-break: inherit !important;">A<span class="mord mathit" style="margin-right:0.10903em;" style="overflow-wrap: inherit !important;word-break: inherit !important;">M<span class="mord mathit" style="margin-right:0.05764em;" style="overflow-wrap: inherit !important;word-break: inherit !important;">E¨<span class="mord mathit" style="margin-right:0.05764em;" style="overflow-wrap: inherit !important;word-break: inherit !important;">E125<span class="mord mathit" style="margin-right:0.05764em;" style="overflow-wrap: inherit !important;word-break: inherit !important;">E/<span class="mord mathit" style="margin-right:0.01968em;" style="overflow-wrap: inherit !important;word-break: inherit !important;">l<span class="mord mathit" style="overflow-wrap: inherit !important;word-break: inherit !important;">e<span class="mord mathit" style="overflow-wrap: inherit !important;word-break: inherit !important;">a<span class="mord mathit" style="overflow-wrap: inherit !important;word-break: inherit !important;">d<span class="mord mathit" style="overflow-wrap: inherit !important;word-break: inherit !important;">e<span class="mord mathit" style="margin-right:0.02778em;" style="overflow-wrap: inherit !important;word-break: inherit !important;">r/<span class="mord mathit" style="margin-right:0.10764em;" style="overflow-wrap: inherit !important;word-break: inherit !important;">f<span class="mord mathit" style="overflow-wrap: inherit !important;word-break: inherit !important;">a<span class="mord mathit" style="overflow-wrap: inherit !important;word-break: inherit !important;">i<span class="mord mathit" style="margin-right:0.01968em;" style="overflow-wrap: inherit !important;word-break: inherit !important;">l<span class="mord mathit" style="overflow-wrap: inherit !important;word-break: inherit !important;">o<span class="mord mathit" style="margin-right:0.03588em;" style="overflow-wrap: inherit !important;word-break: inherit !important;">v<span class="mord mathit" style="overflow-wrap: inherit !important;word-break: inherit !important;">e<span class="mord mathit" style="margin-right:0.02778em;" style="overflow-wrap: inherit !important;word-break: inherit !important;">r/<span class="mord mathit" style="overflow-wrap: inherit !important;word-break: inherit !important;">i<span class="mord mathit" style="overflow-wrap: inherit !important;word-break: inherit !important;">t<span class="mord mathit" style="overflow-wrap: inherit !important;word-break: inherit !important;">e<span class="mord mathit" style="overflow-wrap: inherit !important;word-break: inherit !important;">m<span class="mord mathit" style="overflow-wrap: inherit !important;word-break: inherit !important;">s/{ITEM_ID} } } private boolean isFailoverAssigned(final Integer item) { return jobNodeStorage.isJobNodeExisted(FailoverNode.getExecutionFailoverNode(item)); } </span class="mord mathit"></span class="mord mathit"></span class="mord mathit"></span class="mord mathit"></span class="mord mathit"></span class="mord mathit" style="margin-right:0.02778em;"></span class="mord mathit"></span class="mord mathit" style="margin-right:0.03588em;"></span class="mord mathit"></span class="mord mathit" style="margin-right:0.01968em;"></span class="mord mathit"></span class="mord mathit"></span class="mord mathit" style="margin-right:0.10764em;"></span class="mord mathit" style="margin-right:0.02778em;"></span class="mord mathit"></span class="mord mathit"></span class="mord mathit"></span class="mord mathit"></span class="mord mathit" style="margin-right:0.01968em;"></span class="mord mathit" style="margin-right:0.05764em;"></span class="mord mathit" style="margin-right:0.05764em;"></span class="mord mathit" style="margin-right:0.05764em;"></span class="mord mathit" style="margin-right:0.10903em;"></span class="mord mathit"></span class="mord mathit" style="margin-right:0.10903em;"></span class="mord mathit" style="margin-right:0.05764em;"></span class="mord mathit" style="margin-right:0.05764em;"></span class="mord mathit" style="margin-right:0.05017em;"></span class="mord mathit" style="margin-right:0.02778em;"></span class="mord mathit" style="margin-right:0.09618em;"></span class="mord mathit" style="margin-right:0.05764em;"></span class="mord mathit" style="margin-right:0.05764em;"></span class="strut" style="height:1em;vertical-align:-0.25em;"></span class="katex-html" aria-hidden="true"></annotation encoding="application>
  • 調用 FailoverService#failoverIfNecessary() 方法,如果需要失效轉移, 則執行作業失效轉移。

3. 作業失效轉移

調用 FailoverService#failoverIfNecessary() 方法,如果需要失效轉移, 則執行作業失效轉移。

// FailoverService.java
public void failoverIfNecessary() {
   if (needFailover()) {
       jobNodeStorage.executeInLeader(FailoverNode.LATCH, new FailoverLeaderExecutionCallback());
   }
}
  • 調用 #needFailover() 方法,判斷是否滿足失效轉移條件。 private boolean needFailover() { // ${JOB_NAME}/leader/failover/items/${ITEM_ID} 有失效轉移的作業分片項 return jobNodeStorage.isJobNodeExisted(FailoverNode.ITEMS_ROOT) && !jobNodeStorage.getJobNodeChildrenKeys(FailoverNode.ITEMS_ROOT).isEmpty() // 當前作業不在運行中 && !JobRegistry.getInstance().isJobRunning(jobName); }
    • 條件一:${JOB_NAME}/leader/failover/items/${ITEM_ID} 有失效轉移的作業分片項。
    • 條件二:當前作業不在運行中。此條件即是上文提交的作業節點空閒的定義。 失效轉移: 運行中的作業服務器崩潰不會導致重新分片,只會在下次作業啓動時分片。啓用失效轉移功能可以在本次作業執行過程中,監測其他作業服務器【空閒】,抓取未完成的孤兒分片項執行
  • 調用 JobNodeStorage#executeInLeader(…) 方法,使用 FailoverNode.LATCH( /${JOB_NAME}/leader/failover/latch ) 路徑構成的分佈式鎖,保證 FailoverLeaderExecutionCallback 的回調方法同一時間,即使多個作業節點調用,有且僅有一個作業節點進行執行。另外,雖然 JobNodeStorage#executeInLeader(…) 方法上帶有 Leader 關鍵字,實際非必須在主節點的操作,任何一個拿到分佈式鎖的作業節點都可以調用。目前和分佈式鎖相關的邏輯,在 Elastic-Job-Lite 裏,都會調用 JobNodeStorage#executeInLeader(…) 方法,數據都存儲在 /leader/ 節點目錄下。關於分佈式鎖相關的,在《Elastic-Job-Lite 源碼分析 —— 註冊中心》「3.1 在主節點執行操作」有詳細分享。

FailoverLeaderExecutionCallback 回調邏輯如下:

class FailoverLeaderExecutionCallback implements LeaderExecutionCallback {

   @Override
   public void execute() {
       // 判斷需要失效轉移
       if (JobRegistry.getInstance().isShutdown(jobName) || !needFailover()) {
           return;
       }
       // 獲得一個 `${JOB_NAME}/leader/failover/items/${ITEM_ID}` 作業分片項
       int crashedItem = Integer.parseInt(jobNodeStorage.getJobNodeChildrenKeys(FailoverNode.ITEMS_ROOT).get(0));
       log.debug("Failover job '{}' begin, crashed item '{}'", jobName, crashedItem);
       // 設置這個 `${JOB_NAME}/sharding/${ITEM_ID}/failover` 作業分片項 爲 當前作業節點
       jobNodeStorage.fillEphemeralJobNode(FailoverNode.getExecutionFailoverNode(crashedItem), JobRegistry.getInstance().getJobInstance(jobName).getJobInstanceId());
       // 移除這個 `${JOB_NAME}/leader/failover/items/${ITEM_ID}` 作業分片項
       jobNodeStorage.removeJobNodeIfExisted(FailoverNode.getItemsNode(crashedItem));
       // TODO 不應使用triggerJob, 而是使用executor統一調度 疑問:爲什麼要用executor統一,後面研究下
       // 觸發作業執行
       JobScheduleController jobScheduleController = JobRegistry.getInstance().getJobScheduleController(jobName);
       if (null != jobScheduleController) {
           jobScheduleController.triggerJob();
       }
   }
}
  • 再次調用 #needFailover() 方法,確保經過分佈式鎖獲取等待過程中,仍然需要失效轉移。因爲可能多個作業節點調用了該回調,第一個作業節點執行了失效轉移,可能第二個作業節點就不需要執行失效轉移了。
  • 調用 JobNodeStorage#getJobNodeChildrenKeys(FailoverNode.ITEMS_ROOT)#get(0) 方法,獲得一個 ${JOB_NAME}/leader/failover/items/${ITEM_ID} 作業分片項。 調用 JobNodeStorage#fillEphemeralJobNode(...) 方法,設置這個臨時數據節點 ${JOB_NAME}/sharding/${ITEM_ID}failover 作業分片項爲當前作業節點( ${JOB_INSTANCE_ID} )。 調用 JobNodeStorage#removeJobNodeIfExisted(...) 方法,移除這個${JOB_NAME}/leader/failover/items/${ITEM_ID} 作業分片項。
  • 調用 JobScheduleController#triggerJob() 方法,立即啓動作業。調用該方法,實際作業不會立即執行,而僅僅是進行觸發。如果有多個失效轉移的作業分片項,多次調用 JobScheduleController#triggerJob() 方法會不會導致作業是並行執行的?答案是不會,因爲一個作業的 Quartz 線程數設置爲 1。 // JobScheduler.java private Properties getBaseQuartzProperties() { Properties result = new Properties(); // ... 省略無關代碼 result.put("org.quartz.threadPool.threadCount", "1"); // Quartz 線程數:1 // ... 省略無關代碼 return result; }

如果說作業分片項實現轉移時,每個作業節點都不處於非空閒狀態,豈不是 FailoverLeaderExecutionCallback 一直無法被回調?答案當然不是的。作業在執行完分配給自己的作業分片項,會調用 LiteJobFacade#failoverIfNecessary() 方法,進行失效轉移的作業分片項抓取:

public final void execute() {
   // ...  省略無關代碼

   // 執行 普通觸發的作業
   execute(shardingContexts, JobExecutionEvent.ExecutionSource.NORMAL_TRIGGER);
   // 執行 被跳過觸發的作業
   while (jobFacade.isExecuteMisfired(shardingContexts.getShardingItemParameters().keySet())) {
       jobFacade.clearMisfire(shardingContexts.getShardingItemParameters().keySet());
       execute(shardingContexts, JobExecutionEvent.ExecutionSource.MISFIRE);
   }

   // 執行 作業失效轉移
   jobFacade.failoverIfNecessary();

   // ...  省略無關代碼
}

// LiteJobFacade.java
@Override
public void failoverIfNecessary() {
   if (configService.load(true).isFailover()) {
       failoverService.failoverIfNecessary();
   }
}

// FailoverService.java
public void failoverIfNecessary() {
   if (needFailover()) {
       jobNodeStorage.executeInLeader(FailoverNode.LATCH, new FailoverLeaderExecutionCallback());
   }
}

讓我們在翻回 JobCrashedJobListener 處代碼,爲什麼獲取失效轉移的作業分片項是這樣的優先順序?一個作業節點擁有 ${JOB_NAME}/sharding/${ITEM_ID}/failover 數據分片項,意味着分配給它的作業分片項已經執行完成,否則怎麼回調 FailoverLeaderExecutionCallback 方法,抓取失效轉移的作業分片項呢?!

旁白君:雙擊666,關注筆者公衆號一波。

此處 JobFacade#failoverIfNecessary() 方法,只會抓取一個失效轉移的作業分片,這樣帶來的好處是,多個作業分片可以一起承擔執行失效轉移的分片集合。舉個例子:一個作業集羣有 A / B / C 三個節點,分成六個作業分片,如果 C 節點掛了,A / B 節點分擔 C 節點的兩個分片。但是,也可能會存在失效轉移的分片被執行。舉個例子:一個作業集羣有 A / B / C 三個節點,分成九個作業分片,如果 C 節點掛了,A / B 節點分擔 C 節點的兩個分片,有一個被漏掉,只能等下次作業分片才能執行。未來這塊算法會進行優化。

4. 獲取作業分片上下文集合

在《Elastic-Job-Lite 源碼分析 —— 作業執行》「4.2 獲取當前作業服務器的分片上下文」中,我們可以看到作業執行器( AbstractElasticJobExecutor ) 執行作業時,會獲取當前作業服務器的分片上下文進行執行。獲取過程總體如下順序圖( 打開大圖 ):

  • 紅色叉叉在《Elastic-Job-Lite 源碼解析 —— 作業分片》有詳細分享。

實現代碼如下:

// LiteJobFacade.java
@Override
public ShardingContexts getShardingContexts() {
   // 獲得 失效轉移的作業分片項
   boolean isFailover = configService.load(true).isFailover();
   if (isFailover) {
       List<Integer> failoverShardingItems = failoverService.getLocalFailoverItems();
       if (!failoverShardingItems.isEmpty()) {
           // 【忽略,作業分片詳解】獲取當前作業服務器分片上下文
           return executionContextService.getJobShardingContext(failoverShardingItems);
       }
   }
   // 【忽略,作業分片詳解】作業分片,如果需要分片且當前節點爲主節點
   shardingService.shardingIfNecessary();
   // 【忽略,作業分片詳解】獲得 分配在本機的作業分片項
   List<Integer> shardingItems = shardingService.getLocalShardingItems();
   // 移除 分配在本機的失效轉移的作業分片項目
   if (isFailover) {
       shardingItems.removeAll(failoverService.getLocalTakeOffItems());
   }
   // 移除 被禁用的作業分片項
   shardingItems.removeAll(executionService.getDisabledItems(shardingItems));
   // 【忽略,作業分片詳解】獲取當前作業服務器分片上下文
   return executionContextService.getJobShardingContext(shardingItems);
}
  • 調用 FailoverService#getLocalFailoverItems() 方法,獲取運行在本作業節點的失效轉移分片項集合。 // FailoverService.java public List<Integer> getLocalFailoverItems() { if (JobRegistry.getInstance().isShutdown(jobName)) { return Collections.emptyList(); } return getFailoverItems(JobRegistry.getInstance().getJobInstance(jobName).getJobInstanceId()); // ${JOB_NAME}/sharding/${ITEM_ID}/failover }
  • 調用 ExecutionContextService#getJobShardingContext() 方法,獲取當前作業服務器分片上下文。在《Elastic-Job-Lite 源碼解析 —— 作業分片》「4. 獲取作業分片上下文集合」有詳細解析。
  • 當本作業節點不存在抓取的失效轉移分片項,則獲得分配給本作業分解的作業分片項。此時你會看到略奇怪的方法調用,shardingItems.removeAll(failoverService.getLocalTakeOffItems())。爲什麼呢?舉個例子,作業節點A持有作業分片項[0, 1],此時異常斷網,導致[0, 1]被作業節點B失效轉移抓取,此時若作業節點A恢復,作業分片項[0, 1]依然屬於作業節點A,但是可能已經在作業節點B執行,因此需要進行移除,避免多節點運行相同的作業分片項。FailoverService#getLocalTakeOffItems() 方法實現代碼如下: // FailoverService.java /** * 獲取運行在本作業服務器的被失效轉移的序列號. * * @return 運行在本作業服務器的被失效轉移的序列號 */ public List<Integer> getLocalTakeOffItems() { List<Integer> shardingItems = shardingService.getLocalShardingItems(); List<Integer> result = new ArrayList<>(shardingItems.size()); for (int each : shardingItems) { if (jobNodeStorage.isJobNodeExisted(FailoverNode.getExecutionFailoverNode(each))) { result.add(each); } } return result; }

5. 監聽作業失效轉移功能關閉

class FailoverSettingsChangedJobListener extends AbstractJobListener {

   @Override
   protected void dataChanged(final String path, final Type eventType, final String data) {
       if (configNode.isConfigPath(path) && Type.NODE_UPDATED == eventType
               && !LiteJobConfigurationGsonFactory.fromJson(data).isFailover()) { // 關閉失效轉移功能
           failoverService.removeFailoverInfo();
       }
   }
}

666. 彩蛋

旁白君:啊啊啊,有點繞。 芋道君:耐心,耐心,耐心。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章