寫在前面
Azkaban官網:https://azkaban.github.io/
1. azkaban簡單介紹
Azkaban是由Linkedin公司推出的一個批量工作流任務調度器,主要用於在一個工作流內以一個特定的順序運行一組工作和流程。Azkaban使用job配置文件建立任務之間的依賴關係,並提供一個易於使用的web用戶界面維護和跟蹤你的工作流。 其Web UI界面如下圖所示。
由於我們團隊內部使用Java作爲主流開發語言,並且Spark算子之間確實存在着依賴關係。我們選擇Azkaban的原因基於以下幾點:
- 提供功能清晰,簡單易用的Web UI界面
- 提供job配置文件快速建立任務和任務之間的依賴關係
- 提供模塊化和可插拔的插件機制,原生支持command、Java、Hive、Pig、Hadoop
- 基於Java開發,代碼結構清晰,易於二次開發
- 提供了Restful接口,方面我們平臺定製化。
2. Azkaban的適用場景
實際項目中經常有這些場景:每天有一個大任務,這個大任務可以分成A,B,C,D四個小任務,A,B任務之間沒有依賴關係,C任務依賴A,B任務的結果,D任務依賴C任務的結果。一般的做法是,開兩個終端同時執行A,B,兩個都執行完了再執行C,最後再執行D。這樣的話,整個的執行過程都需要人工參加,並且得盯着各任務的進度。但是我們的很多任務都是在深更半夜執行的,通過寫腳本設置crontab執行。這樣子很不好維維護。
其實,整個過程類似於一個有向無環圖(DAG)。每個子任務相當於大任務中的一個流,任務的起點可以從沒有度的節點開始執行,任何沒有通路的節點之間可以同時執行,比如上述的A,B。總結起來的話,我們需要的就是一個工作流的調度器,而Azkaban就是能解決上述問題的一個調度器。
3. Azkaban架構
Azkaban在LinkedIn上實施,以解決Hadoop作業依賴問題。我們有工作需要按順序運行,Spark各個算子之間有執行依賴關係,比如下一個算子執行的數據源依賴於上一個算子執行產生的結果數據。最初是單一服務器解決方案,隨着多年來Hadoop用戶數量的增加,Azkaban 已經發展成爲一個更強大的解決方案。
Azkaban由三個關鍵組件構成:
- 關係型數據庫(MySQL)
- AzkabanWebServer
- AzkabanExecutorServer
3.1 關係型數據庫(MySQL)
Azkaban使用數據庫存儲大部分狀態,AzkabanWebServer和AzkabanExecutorServer都需要訪問數據庫。
AzkabanWebServer使用數據庫的原因如下:
- 項目管理:項目、項目權限以及上傳的文件。
- 執行流狀態:跟蹤執行流程以及執行程序正在運行的流程。
- 以前的流程/作業:通過以前的作業和流程執行以及訪問其日誌文件進行搜索。
- 計劃程序:保留計劃作業的狀態。
- SLA:保持所有的SLA規則
AzkabanExecutorServer使用數據庫的原因如下:
- 訪問項目:從數據庫檢索項目文件。
- 執行流程/作業:檢索和更新正在執行的作業流的數據
- 日誌:將作業和工作流的輸出日誌存儲到數據庫中。
- 交互依賴關係:如果一個工作流在不同的執行器上運行,它將從數據庫中獲取狀態。
3.2 AzkabanWebServer
AzkabanWebServer是整個Azkaban工作流系統的主要管理者,它負責project管理、用戶登錄認證、定時執行工作流、跟蹤工作流執行進度等一系列任務。同時,它還提供Web服務操作的接口,利用該接口,用戶可以使用curl或其他ajax的方式,來執行azkaban的相關操作。操作包括:用戶登錄、創建project、上傳workflow、執行workflow、查詢workflow的執行進度、殺掉workflow等一系列操作,且這些操作的返回結果均是json的格式。並且Azkaban使用方便,Azkaban使用以.job爲後綴名的鍵值屬性文件來定義工作流中的各個任務,以及使用dependencies屬性來定義作業間的依賴關係鏈。這些作業文件和關聯的代碼最終以*.zip的方式通過Azkaban UI上傳到Web服務器上。
3.3 AzkabanExecutorServer
以前版本的Azkaban在單個服務中具有AzkabanWebServer和AzkabanExecutorServer功能,目前Azkaban已將AzkabanExecutorServer分離成獨立的服務器,拆分AzkabanExecutorServer的原因有如下幾點:
- 某個任務流失敗後,可以更方便的將其重新執行
- 便於Azkaban升級
AzkabanExecutorServer主要負責具體的工作流的提交、執行,可以啓動多個執行服務器,它們通過mysql數據庫來協調任務的執行以及實現高可用性。
4. Azkaban作業流執行過程
Webserver根據內存中緩存的各Executor的資源狀態(Webserver有一個線程會遍歷各個active executor,去發送http請求獲取其資源狀態信息緩存到內存中),按照選擇策略(包括executor資源狀態、最近執行流個數等)選擇一個executor下發作業流;executor判斷是否設置作業粒度分配,如果未設置作業粒度分配,則在當前executor執行所有作業;如果設置了作業粒度分配,則當前節點會成爲作業分配的決策者,即分配節點;分配節點從zookeeper獲取各個executor的資源狀態信息,然後根據策略選擇一個executor分配作業;被分配到作業的executor即成爲執行節點,執行作業,然後更新數據庫。
5. Azkaban架構的三種運行模式
在版本3.0中,Azkaban提供了以下三種模式:
- solo server mode:最簡單的模式,數據庫內置的H2數據庫,AzkabanWebServer和AzkabanExecutorServer都在一個進程中運行,任務量不大項目可以採用此模式。
- two server mode:數據庫爲MySQL,管理服務器和執行服務器在不同進程,這種模式下,AzkabanWebServer和AzkabanExecutorServer互不影響。
- multiple executor mode:該模式下,AzkabanWebServer和AzkabanExecutorServer運行在不同主機上,且AzkabanExecutorServer可以有多個。
大數據平臺要求其具有高可用性,所以目前我們採用的是multiple executor mode方式,分別在不同的主機上部署多個AzkabanExecutorServer以應對高併發定時任務執行的情況,從而減輕單個服務器的壓力。 下面是集羣架構圖:
6.核心調度概述
Azkaban WebServer需要根據Executor Server的運行狀態信息,選擇一個合適的Executor Server來運行WorkFlow,然後會將提交到隊列中的WorkFlow調度到選定的Executor Server上運行。我們整理了與核心調度相關的各個組件,主要包括Azkaban WebServer端和Azkaban ExecutorServer端,他們之間的關係如下圖所示:
其實,從調度層面來看,Azkaban WebServer與Executor Server之間的交互方式非常簡單,是通過REST API的方式來進行交互,基本的模式是,Azkaban WebServer根據調度的需要,主動調用Executor Server暴露的REST API來獲取相應的資源信息,比如Executor Server的狀態信息、分配WorkFlow到指定Executor Server上運行,等等。
我們可以在QueueProcessorThread.selectExecutorAndDispatchFlow()方法中看到,選擇Executor Server並進行調度的實現,代碼片段如下所示:
final Executor selectedExecutor = selectExecutor(exflow, availableExecutors);
if (selectedExecutor != null) {
try {
dispatch(reference, exflow, selectedExecutor);
ExecutorManager.this.commonMetrics.markDispatchSuccess();
} catch (final ExecutorManagerException e) {
ExecutorManager.this.commonMetrics.markDispatchFail();
logger.warn(String.format(
"Executor %s responded with exception for exec: %d",
selectedExecutor, exflow.getExecutionId()), e);
handleDispatchExceptionCase(reference, exflow, selectedExecutor,
availableExecutors);
}
}
QueueProcessorThread是運行在Azkaban WebServer端的一個線程,它在ExecutorManager中定義,是內部調度中最核心的線程。
selectExecutor()方法處理如何選擇一個合適的Executor Server,然後通過dispatch()方法將需要運行的WorkFlow調度到該Executor Server上運行。
選擇Executor Server
Azkaban WebServer選擇Executor,調用selectExecutor()方法,實現如下所示:
private Executor selectExecutor(final ExecutableFlow exflow,
final Set<Executor> availableExecutors) {
Executor choosenExecutor =
getUserSpecifiedExecutor(exflow.getExecutionOptions(),
exflow.getExecutionId());
// If no executor was specified by admin
if (choosenExecutor == null) {
logger.info("Using dispatcher for execution id :"
+ exflow.getExecutionId());
final ExecutorSelector selector = new ExecutorSelector(ExecutorManager.this.filterList,
ExecutorManager.this.comparatorWeightsMap);
choosenExecutor = selector.getBest(availableExecutors, exflow);
}
return choosenExecutor;
}
首先,查看當前exflow的配置中,是否要求將該exflow調度到指定的Executor Server上運行,如果是的話,則會返回該指定的Executor Server的信息,後續直接調度到該Executor Server上;否則會按照一定的計算規則去選出一個Executor Server。
在創建ExecutorSelector時,傳入參數值ExecutorManager.this.filterList,該filterList是從azkanban.properties文件中讀取azkaban.executorselector.filters的配置值,並創建了一個ExecutorFilter對象,而該對象中包含了一組FactorFilter,後面我們會說明。
使用ExecutorSelector來選出一個Executor Server,具體選擇的邏輯,我們可以查看ExecutorSelector.getBest()方法。
首先通過定義的CandidateFilter(它是一個抽象類,具體實現類爲ExecutorFilter)進行預篩選:
for (final K candidateInfo : candidateList) {
if (this.filter.filterTarget(candidateInfo, dispatchingObject)) {
filteredList.add(candidateInfo);
}
}
上面的filter就是FactorFilter類的實例,Azkaban內部定義瞭如下3種:
private static final String STATICREMAININGFLOWSIZE_FILTER_NAME = "StaticRemainingFlowSize";
private static final String MINIMUMFREEMEMORY_FILTER_NAME = "MinimumFreeMemory";
private static final String CPUSTATUS_FILTER_NAME = "CpuStatus";
目前3.40.0版本不支持自定義,只能使用內建實現的,如果需要增加新的FactorFilter,可以在此基礎上做一個簡單改造,配置使用自己定義的FactorFilter實現。FactorFilter是一個泛型類:FactorFilter<Executor, ExecutableFlow>,根據上面定義的3種指標對Executor Server進行一個預過濾,滿足要求的會進行後面的比較,加入到調度WorkFlow執行的Executor Server的候選集中。
然後,通過如下方式進行比較排序,選擇合適的Executor Server:
// final work - find the best candidate from the filtered list.
final K executor = Collections.max(filteredList, this.comparator);
logger.debug(String.format("candidate selected %s",
null == executor ? "(null)" : executor.toString()));
return executor;
這裏關鍵的就是this.comparator,它有一個實現類ExecutorComparator,該類中給出了需要對兩個Executor Server的哪些指標進行綜合比較,亦即一組比較器的定義,可以看到目前考慮了4種比較器:
private static final String NUMOFASSIGNEDFLOW_COMPARATOR_NAME = "NumberOfAssignedFlowComparator";
private static final String MEMORY_COMPARATOR_NAME = "Memory";
private static final String LSTDISPATCHED_COMPARATOR_NAME = "LastDispatched";
private static final String CPUUSAGE_COMPARATOR_NAME = "CpuUsage";
通過上面代碼可以看出,在選擇調度一個WorkFlow到Azkaban集羣中的某個Executor Server時,需要比較Executor Server的如下4個指標:
- 能夠運行WorkFlow的剩餘容量,數值越大越優先
- 剩餘內存用量,數值越大越優先
- 最近分配Flow的時間,數值越大越優先
- CPU使用用量,數值越小越優先
基於上面4個指標,創建了4個比較器,使用FactorComparator來表示,對需要比較的一組Executor Server,使用這4個比較器進行比較,通過加權後得到一個得分值,根據該得分值選定Executor Server,核心邏輯如下所示:
final Collection<FactorComparator<T>> comparatorList = this.factorComparatorList.values();
for (final FactorComparator<T> comparator : comparatorList) {
final int result = comparator.compare(object1, object2);
result1 = result1 + (result > 0 ? comparator.getWeight() : 0);
result2 = result2 + (result < 0 ? comparator.getWeight() : 0);
logger.debug(String.format("[Factor: %s] compare result : %s (current score %s vs %s)",
comparator.getFactorName(), result, result1, result2));
}
上面選取了待比較的兩個Executor Server都不爲空的情況,分別遍歷每個FactorComparator進行比較,在分別對每個Executor Server的比較結果值進行累加求和,加權得到一個分數值。從一組Executor Server中,根據最終比較的分數值,分數值最大的Executor Server爲最終選定的Executor Server。
獲取Executor Server的運行統計信息
在Azkaban WebServer內部,會維護集羣中每個Executor Server的運行狀態信息,該信息的獲取是在QueueProcessorThread線程中實現的,定期去更新所維護的Executor Server的運行狀態信息,如下所示:
if (currentTime - lastExecutorRefreshTime > activeExecutorsRefreshWindow
|| currentContinuousFlowProcessed >= maxContinuousFlowProcessed) {
// Refresh executorInfo for all activeExecutors
refreshExecutors();
lastExecutorRefreshTime = currentTime;
currentContinuousFlowProcessed = 0;
}
上面refreshExecutors()方法遍歷內存中維護的所有的Executor Server,調用每個Executor Server的/serverStatistics接口,拉取Executor Server的運行狀態信息。
另外,Azkaban WebServer還需要能夠獲取到各個Executor Server上運行的WorkFlow的狀態信息,可以在ExecutorManager.ExecutingManagerUpdaterThread中看到實現,代碼片段如下所示:
results =
ExecutorManager.this.apiGateway.callWithExecutionId(executor.getHost(),
executor.getPort(), ConnectorParams.UPDATE_ACTION,
null, null, executionIds, updateTimes);
上面調用Executor Server的/executor?action=update接口來拉取WorkFlow的狀態信息,然後更新內存中維護的狀態信息數據結構。其中,有些WorkFlow可能已經運行完成,需要釋放資源;有些WorkFlow狀態發生變更,也需要更新Azkaban WebServer端內存中維護的數據結構。
調度WorkFlow到Executor Server上執行
上面已經選定Executor Server,結合前面代碼,是通過調用ExecutorManager.dispatch()方法來實現,調度WorkFlow到該選定的Executor Server上運行,代碼片段如下所示:
try {
this.apiGateway.callWithExecutable(exflow, choosenExecutor,
ConnectorParams.EXECUTE_ACTION);
} catch (final ExecutorManagerException ex) {
logger.error("Rolling back executor assignment for execution id:"
+ exflow.getExecutionId(), ex);
this.executorLoader.unassignExecutor(exflow.getExecutionId());
throw new ExecutorManagerException(ex);
}
通過跟蹤查看apiGateway.callWithExecutable()實現,可以看到,最終是調用了Executor Server端的一個REST API接口:/executor,然後帶上相關的請求參數,如action=execute、execId等。
Executor Server執行WorkFlow
很顯然,Azkaban WebServer調度WorkFlow後,Executor Server在ExecutorServlet中接收到對應的請求,核心方法如下所示:
private void handleAjaxExecute(final HttpServletRequest req,
final Map<String, Object> respMap, final int execId) throws ServletException {
try {
this.flowRunnerManager.submitFlow(execId);
} catch (final ExecutorManagerException e) {
e.printStackTrace();
logger.error(e.getMessage(), e);
respMap.put(RESPONSE_ERROR, e.getMessage());
}
}
在收到Azkaban WebServer的調度請求後,Executor Server使用內部的FlowRunnerManager來提交WorkFlow執行。在這個過程中,首先使用ExecutorLoader從數據庫中讀取WorkFlow對應的信息;然後使用FlowPreparer進行初始化,創建對應的數據目錄等;最後創建FlowRunner來執行WorkFlow,並跟蹤其執行狀態。
參考