[譯] 理解 Apache Airflow 的關鍵概念

Starrier發表於2018-08-21

四部分系列的第三系列

Quizlet尋找最優工作流管理系統第一部分第二部分中,我們促進了現代商業實踐中對工作流管理系統(WMS)的需求,並提供了一份希望獲得的特性以及功能列表,這使得我們最後選擇了 Apache Airflow 作為我們的 WMS 選擇。這篇文章旨在給好奇的讀者提供提供關於 Airflow 的元件和操作的詳細概述。我們會通過實現本系列第一部分中介紹的示例工作流(查閱 圖 3.1)來介紹 Airflow 的關鍵概念。

[譯] 理解 Apache Airflow 的關鍵概念

圖 3.1:資料處理工作流的示例。

Airflow 是一種 WMS,即:它將任務以及它們的依賴看作程式碼,按照那些計劃規範任務執行,並在 worker 程式之間分發需執行的任務。Airflow 提供了一個用於顯示當前活動任務和過去任務狀態的優秀 UI,並允許使用者手動管理任務的執行和狀態。

工作流都是“有向無環圖”

Airflow 中的工作流是具有方向性依賴的任務集合。具體說明則是 Airflow 使用有向有向無環圖 —— 或簡稱的 DAG —— 來表現工作流。圖中的每個節點都是一個任務,圖中的邊表示的是任務之間的依賴(該圖強制為無迴圈的,因此不會出現迴圈依賴,從而導致無限執行迴圈)。

圖 3.2 頂部演示了我們的示例工作流是如何在 Airflow 中變現為 DAG 的。注意在圖 1.1 中我們的示例工作流任務的執行計劃結構與圖 3.2 中的 DAG 結構相似。

[譯] 理解 Apache Airflow 的關鍵概念

圖 3.2 來自 Airflow UI 的螢幕截圖,表示示例工作流 DAG。皮膚頂部:1 月 25 號 DagRun 的圖表檢視。深綠色節點表示 TaskInstance 的“成功”狀態。淡綠色描繪了 TaskInstance 的“執行”狀態。底部子皮膚example_workflow DAG 的樹圖。Airflow 的主要元件在螢幕截圖中高亮顯示,包括 Sensor、Operator、任務、DagRunsTaskInstancesDagRuns 在圖視中表示為列 —— DagRun 在 1 月 25 號用青色表示。圖示中的每個方框表示一個 TaskInstance —— 1 月 25 號 為 perform_currency_conversion 任務的 TaskInstance(“執行態”)用藍色表示。

在高階別中,可以將 DAG 看作是一個包含任務極其依賴,何時以及如何設定那些任務的上下文的容器。每個 DAG 都有一組屬性,最重要的是它的 dag_id,在所有 DAG 中的唯一識別符號,它的 start_date 用於說明 DAG 任務被執行的時間,schedule_interval 用於說明任務被執行的頻率。此外,dag_idstart_dateschedule_interval,每個 DAG 都可以使用一組 default_arguments 進行初始化。這些預設引數由 DAG 中的所有任務繼承。

在下列程式碼塊中,我們在 Airflow 中定義了一個用於實現我們遊戲公司示例工作流的 DAG。

# 每個工作流/DAG 都必須要有一個唯一的文字識別符號
WORKFLOW_DAG_ID = 'example_workflow_dag'

# 開始/結束時間是 datetime 物件
# 這裡我們在 2017 年 1 月 1 號開始執行
WORKFLOW_START_DATE = datetime(2017, 1, 1)

# 排程器/重試間隔是 timedelta 物件
# 這裡我們每天都執行 DAG 任務
WORKFLOW_SCHEDULE_INTERVAL = timedelta(1)

# 預設引數預設應用於所有任務
# 在 DAG 中
WORKFLOW_DEFAULT_ARGS = {
    'owner': 'example',
    'depends_on_past': False,
    'start_date': WORKFLOW_START_DATE,
    'email': ['example@example_company.com'],
    'email_on_failure': True,
    'email_on_retry': False,
    'retries': 5,
    'retry_delay': timedelta(minutes=5)
}

# 初始化 DAG
dag = DAG(
    dag_id=WORKFLOW_DAG_ID,
    start_date=WORKFLOW_START_DATE,
    schedule_interval=WORKFLOW_SCHEDULE_INTERVAL,
    default_args=WORKFLOW_DEFAULT_ARGS,
)
複製程式碼

OperatorsSensors 和 Tasks

儘管 DAG 用於組織並設定執行上下文,但 DAG 不會執行任何實際計算。相反,任務實際上是 Airflow 中我們想要執行“所做工作”的元素。任務有兩種特點:它們可以執行一些顯示操作,在這種情況下,它們是 Operator,或者它們可以暫停執行依賴任務,直到滿足某些條件,在這種情況下,它們是 Sensors。原則上來說,Operator 可以執行在 Python 中被執行的任何函式。同樣,Sensors 可以檢查任何程式或者資料結構的狀態。

下述程式碼塊顯示瞭如何定義一些(假設的)Operator 和 Sensor 類來實現我們的工作流示例。

##################################################
# 自定義 Sensors 示例/ Operators (NoOps) #
##################################################

class ConversionRatesSensor(BaseSensorOperator):
    """
    An example of a custom Sensor. Custom Sensors generally overload
    the `poke` method inherited from `BaseSensorOperator`
    """
    def __init__(self, *args, **kwargs):
        super(ConversionRatesSensor, self).__init__(*args, **kwargs)

    def poke(self, context):
        print 'poking {}'.__str__()
        
        # poke functions should return a boolean
        return check_conversion_rates_api_for_valid_data(context)

class ExtractAppStoreRevenueOperator(BaseOperator):
    """
    An example of a custom Operator that takes non-default 
    BaseOperator arguments. 
    
    Extracts data for a particular app store identified by 
    `app_store_name`.
    """
    def __init__(self, app_store_name, *args, **kwargs):
        self.app_store_name = app_store_name
        super(ExtractAppStoreRevenueOperator, self).__init__(*args, **kwargs)

    def execute(self, context):
        print 'executing {}'.__str__()
        
        # pull data from specific app store
        json_revenue_data = extract_app_store_data(self.app_store_name, context)
        
        # upload app store json data to filestore, can use context variable for 
        # date-specific storage metadata
        upload_appstore_json_data(json_revenue_data, self.app_store_name, context)

class TransformAppStoreJSONDataOperator(BaseOperator):
    """
    An example of a custom Operator that takes non-default 
    BaseOperator arguments.
    
    Extracts, transforms, and loads data for an array of app stores 
    identified by `app_store_names`.
    """
    def __init__(self, app_store_names, *args, **kwargs):
        self.app_store_names = app_store_names
        super(TransformJSONDataOperator, self).__init__(*args, **kwargs)

    def execute(self, context):
        print 'executing {}'.__str__()
        
        # load all app store data from filestores. context variable can be used to retrieve
        # particular date-specific data artifacts
        all_app_stores_extracted_data = []
        for app_store in self.app_store_names:
            all_app_stores_extracted_data.append(extract_app_store_data(app_store, context))
        
        # combine all app store data, transform to proper format, and upload to filestore 
        all_app_stores_json_data = combine_json_data(all_app_stores_extracted_data)
        app_stores_transformed_data = transform_json_data(all_app_stores_json_data)
        upload_data(app_stores_transformed_data, context)
複製程式碼

程式碼定義了 BaseSensorOperator 的子類,即 ConversionRatesSensor。這個類實現了所有 BaseSensorOperator 物件必需的 poke 方法。如果下游任務要繼續執行,poke 方法必須返回 True,否則返回 False。在我們的示例中,這個 sensor 將用於決定何時外部 API 的交換率何時可用。

ExtractAppStoreRevenueOperatorTransformAppStoreJSONDataOperator 這兩個類都繼承自 Airflow 的BaseOperator 類,並實現了 execute 方法。在我們的示例中,這兩個類的 execute 方法都從應用程式儲存 API 中獲取資料,並將它們轉換為公司首選的儲存格式。注意 ExtractAppStoreRevenueOperator 也接受一個自定義引數 app_store_name,它告訴類應用程式儲存應該從哪裡獲取請求資料。

注意,Operator 和 Sensor 通常在單獨檔案中定義,並匯入到我們定義 DAG 的同名名稱空間中。但我們也可以將這些類定義新增到同一個 DAG 定義的檔案中。

形式上,Airflow 定義任務為 Sensor 或 Operator 類例項化。例項化任務需要提供一個唯一的 task_id 和 DAG 容器來新增任務(注意:在高於 1.8 的版本中,不再需要 DAG 物件)。下面的程式碼塊顯示瞭如何例項化執行示例工作流所需的所有任務。(注意:我們假設示例中引用的所有 Operator 都是在名稱空間中定義或匯入的)。

########################
# 例項化任務  #
########################

# 例項化任務來提取廣告網路收入
extract_ad_revenue = ExtractAdRevenueOperator(
    task_id='extract_ad_revenue',
    dag=dag)

# 動態例項化任務來提取應用程式儲存資料
APP_STORES = ['app_store_a', 'app_store_b', 'app_store_c']
app_store_tasks = []
for app_store in APP_STORES:
    task = ExtractAppStoreRevenueOperator(
        task_id='extract_{}_revenue'.format(app_store),
        dag=dag,
        app_store_name=app_store,
        )
    app_store_tasks.append(task)

# 例項化任務來等待轉換率、資料均衡
wait_for_conversion_rates = ConversionRatesSensor(
    task_id='wait_for_conversion_rates',
    dag=dag)

# 例項化任務,從 API 中提取轉化率
extract_conversion_rates = ExtractConversionRatesOperator(
    task_id='get_conversion_rates',
    dag=dag)

# 例項化任務來轉換電子表格資料
transform_spreadsheet_data = TransformAdsSpreadsheetDataOperator(
    task_id='transform_spreadsheet_data',
    dag=dag) 

# 從所有應用程式儲存中例項化任務轉換 JSON 資料
transform_json_data = TransformAppStoreJSONDataOperator(
    task_id='transform_json_data',
    dag=dag,
    app_store_names=APP_STORES)

# 例項化任務來應用
perform_currency_conversions = CurrencyConversionsOperator(
    task_id='perform_currency_conversions',
    dag=dag)

# 例項化任務來組合所有資料來源
combine_revenue_data = CombineDataRevenueDataOperator(
    task_id='combine_revenue_data',
    dag=dag)  

# 例項化任務來檢查歷史資料是否存在
check_historical_data = CheckHistoricalDataOperator(
    task_id='check_historical_data',
    dag=dag)

# 例項化任務來根據歷史資料進行預測
predict_revenue = RevenuePredictionOperator(
    task_id='predict_revenue',
    dag=dag)  
複製程式碼

此任務例項化程式碼在與 DAG 定義相同的檔案/名稱空間中執行。我們可以看到新增任務的程式碼非常簡潔,而且允許通過註解進行內聯文件。第 10–19 行展示了在程式碼中定義工作流的優勢之一。我們能夠動態地定義三個不同的任務,用於使用 for 迴圈從每個應用程式儲存中提取資料。這種方法可能在這個小示例中不會給我們帶來太大的好處,但隨著應用程式商店數量的增加,好處會日益顯著。

定義任務依賴關係

Airflow 的關鍵優勢是定義任務之間依賴關係的簡潔性和直觀約定。下述程式碼表明瞭我們如何為示例工作流定義任務依賴關係圖:

###############################
# 定義任務依賴關係 #
###############################

# 依賴設定使用 `.set_upstream` 和/或  
# `.set_downstream` 方法
# (in version >=1.8.1,也可以使用
# `extract_ad_revenue << transform_spreadsheet_data` 語法)

transform_spreadsheet_data.set_upstream(extract_ad_revenue)

# 動態定義應用程式儲存依賴項
for task in app_store_tasks:
    transform_json_data.set_upstream(task)

extract_conversion_rates.set_upstream(wait_for_conversion_rates)

perform_currency_conversions.set_upstream(transform_json_data)
perform_currency_conversions.set_upstream(extract_conversion_rates)

combine_revenue_data.set_upstream(transform_spreadsheet_data)
combine_revenue_data.set_upstream(perform_currency_conversions)

check_historical_data.set_upstream(combine_revenue_data)

predict_revenue.set_upstream(check_historical_data) 
複製程式碼

同時,此程式碼在與 DAG 定義相同的檔案/名稱空間中執行。任務依賴使用 set_upstreamset_downstream operators 來設定(但在高於 1.8 的版本中,使用移位運算子 <<>> 來更簡潔地執行相似操作是可行的)。一個任務還可以同時具有多個依賴(例如,combine_revenue_data),或一個也沒有(例如,所有的 extract_* 任務)。

圖 3.2 的頂部子皮膚顯示了由上述程式碼所建立的 Airflow DAG,渲染為 Airflow 的 UI(稍後我們會詳細介紹 UI)。 DAG 的依賴結構與在圖 1.1 顯示的我們為我們的示例工作流所提出的執行計劃非常相似。當 DAG 被執行時,Airflow 會使用這種依賴結構來自動確定哪些任務可以在任何時間點同時執行(例如,所有的 extract_* 任務)。

DagRuns 和 TaskInstances

一旦我們定義了 DAG —— 即,我們已經例項化了任務並定義了它們的依賴項 —— 我們就可以基於 DAG 的引數來執行任務。Airflow 中的一個關鍵概念是 execution_time。當 Airflow 排程器正在執行時,它會定義一個用於執行 DAG 相關任務的定期間斷的日期計劃。執行時間從 DAG start_date 開始,並重復每一個 schedule_interval。在我們的示例中,排程時間是 (‘2017–01–01 00:00:00’, ‘2017–01–02 00:00:00’, ...)。對於每一個 execution_time,都會建立 DagRun 並在執行時間上下文中進行操作。因此,DagRun 只是具有一定執行時間的 DAG(參見 圖 3.2 的底部子皮膚)。

所有與 DagRun 關聯的任務都稱為 TaskInstance。換句話說,TaskInstance 是一個已經例項化而且擁有 execution_date 上下文的任務(參見 圖 3.2 的底部子皮膚)。DagRuns 和 TaskInstance 是 Airflow 的核心概念。每個DagRun and TaskInstance 都與記錄其狀態的 Airflow 後設資料庫中的一個條目相關聯(例如 “queued”、“running”、“failed”、“skipped”、“up for retry”)。讀取和更新這些狀態是 Airflow 排程和執行過程的關鍵。

Airflow 的架構

在其核心中,Airflow 是建立在後設資料庫上的佇列系統。資料庫儲存佇列任務的狀態,排程器使用這些狀態來確定如何將其它任務新增到佇列的優先順序。此功能由四個主要元件編排。(請參閱圖 3.2 的左子皮膚):

  1. 後設資料庫:這個資料庫儲存有關任務狀態的資訊。資料庫使用在 SQLAlchemy 中實現的抽象層執行更新。該抽象層將 Airflow 剩餘元件功能從資料庫中乾淨地分離了出來。
  2. 排程器:排程器是一種使用 DAG 定義結合後設資料中的任務狀態來決定哪些任務需要被執行以及任務執行優先順序的過程。排程器通常作為服務執行。
  3. 執行器:Excutor 是一個訊息佇列程式,它被繫結到排程器中,用於確定實際執行每個任務計劃的工作程式。有不同型別的執行器,每個執行器都使用一個指定工作程式的類來執行任務。例如,LocalExecutor 使用與排程器程式在同一臺機器上執行的並行程式執行任務。其他像 CeleryExecutor 的執行器使用存在於獨立的工作機器叢集中的工作程式執行任務。
  4. Workers:這些是實際執行任務邏輯的程式,由正在使用的執行器確定。

[譯] 理解 Apache Airflow 的關鍵概念

圖 3.2:Airflow 的一般架構。Airflow 的操作建立於儲存任務狀態和工作流的後設資料庫之上(即 DAG)。排程器和執行器將任務傳送至佇列,讓 Worker 程式執行。WebServer 執行(經常與排程器在同一臺機器上執行)並與資料庫通訊,在 Web UI 中呈現任務狀態和任務執行日誌。每個有色框表明每個元件都可以獨立於其他元件存在,這取決於部署配置的型別。

排程器操作

首先,Airflow 排程器操作看起來更像是黑魔法而不是邏輯程式。也就是說,如果你發現自己正在除錯它的執行,那麼瞭解排程器的工作原理久可以節省大量的時間,為了讓讀者免於深陷 Airflow 的原始碼(儘管我們非常推薦它!)我們用虛擬碼概述了排程器的基本操作:

步驟 0. 從磁碟中載入可用的 DAG 定義(填充 DagBag)

當排程器執行時:
	步驟 1. 排程器使用 DAG 定義來標識並且/或者初始化在後設資料的 db 中的任何 DagRuns。
	
	步驟 2. 排程器檢查與活動 DagRun 關聯的 TaskInstance 的狀態,解析 TaskInstance 之間的任何依賴,標識需要被執行的 TaskInstance,然後將它們新增至 worker 佇列,將新排列的 TaskInstance 狀態更新為資料庫中的“排隊”狀態。
	
	步驟 3. 每個可用的 worker 從佇列中取一個 TaskInstance,然後開始執行它,將此 TaskInstance 的資料庫記錄從“排隊”更新為“執行”。
	
	步驟 4. 一旦一個 TaskInstance 完成執行,關聯的 worker 就會報告到佇列並更新資料庫中的 TaskInstance 的狀態(例如“完成”、“失敗”等)。
	
	步驟 5. 排程器根據所有已完成的相關 TaskInstance 的狀態更新所有活動 DagRuns 的狀態(“執行”、“失敗”、“完成”)。
	
	步驟 6. 重複步驟 1-5
複製程式碼

Web UI

除了主要的排程和執行元件外,Airflow 還支援包括全功能的 Web UI 元件(參閱圖 3.2 的一些 UI 示例),包括:

  1. Webserver:此過程執行一個簡單的 Flask 應用程式,它從後設資料庫中讀取所有任務狀態,並讓 Web UI 呈現這些狀態。
  2. Web UI:此元件允許客戶端使用者檢視和編輯後設資料庫中的任務狀態。由於排程器和資料庫之間的耦合,Web UI 允許使用者操作排程器的行為。
  3. 執行日誌:這些日誌由 worker 程式編寫,儲存在磁碟或遠端檔案儲存區(例如 GCSS3)中。Webserver 訪問日誌並將其提供給 Web UI。

儘管對於 Airflow 的基本操作來說,這些附加元件都不是必要的,但從功能性角度來說,它們確實使 Airflow 有別於當前的其他工作流管理。 具體來說,UI 和整合執行日誌允許使用者檢查和診斷任務執行,以及檢視和操作任務狀態。

命令列介面

除了排程程式和 Web UI,Airflow 還通過命令列介面(CLI)提供了健壯性的特性。尤其是,當我們開發 Airflow 時,發現以下的這些命令非常有用:

  • airflow test DAG_ID TASK_ID EXECUTION_DATE。允許使用者在不影響後設資料庫或關注任務依賴的情況下獨立執行任務。這個命令很適合獨立測試自定義 Operator 類的基本行為。
  • airflow backfill DAG_ID TASK_ID -s START_DATE -e END_DATE。在 START_DATEEND_DATE 之間執行歷史資料的回填,而不需要執行排程器。當你需要更改現有工作流的一些業務邏輯並需要更新歷史資料時,這是很好的。(請注意,回填不需要在資料庫中建立 DagRun 條目,因為它們不是由 [SchedulerJob](https://github.com/apache/incubator-airflow/blob/master/airflow/jobs.py#L471) 類執行的)。
  • airflow clear DAG_ID。移除 DAG_ID 後設資料庫中的 TaskInstance 記錄。當你迭代工作流/DAG 功能時,這會很有用。
  • airflow resetdb:雖然你通常不想經常執行這個命令,但如果你需要建立一個“乾淨的歷史記錄”,這是非常有幫助的,這種情況載最初設定 Airflow 時可能會出現(注意:這個命令隻影響資料庫,不刪除日誌)。

綜上所述,我們提供了一些更加抽象的概念,作為 Airflow 的基礎。在此係列的最後部分 installment 中,我們將討論在生產中部署 Airflow 時的一些更實際的注意事項。

感謝 Laura Oppenheimer

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章