摘要: 原創出處 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. 彩蛋
旁白君:啊啊啊,有點繞。 芋道君:耐心,耐心,耐心。