面向流批一體的 Flink Runtime 新進展

ApacheFlink發表於2022-03-16

摘要:本文整理自阿里巴巴技術專家高贇 (雲騫) 在 Flink Forward Asia 2021 核心技術專場的演講。主要內容包括:

  1. 流批一體
  2. 語義完善與增強
  3. 效能優化
  4. Remote Shuffle
  5. 總結與展望

點選檢視直播回放 & 演講PDF

一、流批一體

img

流批一體的目標是希望能夠為有限資料和無限資料提供一套統一的處理 API,包括 Datastream API 與 Table/SQL API,其中有限資料的處理對應離線處理,而無限資料的處理則對應線上處理。

img

之所以需要這麼一套流批一體的處理 API,主要有以下兩個原因:

  • 首先,隨著實時計算的不斷髮展,大多數企業資料處理的 pipeline 都是由離線處理和線上處理組成的,使用同一套開發 API 就可以減少流處理和批處理作業開發過程中的學習與維護的成本;
  • 另外,在許多場景下,使用者的流處理作業可能受限於延遲資料或者線上邏輯變更的情況,例如使用者可能會過很長時間才會發起評論或線上處理的邏輯可能需要進行升級,這種情況下就需要使用離線作業對之前處理的結果進行修正,即 backfill 的情況。

這種情況下如果使用兩套不同的 API 會很難維護處理結果的一致性,因此使用者需要一套統一的 API 來解決上述問題。

img

Flink 流批一體的 API 由 Datastream API 與 Table/SQL API 兩部分組成,其中 Table/SQL API 是相對比較 high level 的 API,主要提供標準的 SQL 以及與它等價的 table 操作,datastream 則相對 low level,使用者可以顯式地操作運算元 time 和 state,這兩套 API 可以互相轉換結合使用。

img

針對兩套 API,Flink 提供了兩種不同的執行模式:

  • 其中流執行模式是通過保留運算元的 state 等新資料到達的時候進行增量計算來實現的。它可以同時用於有限資料集和無限資料集的處理,可以支援任意處理邏輯,比如 salt 操作,它允許保留所有歷史資料並支援 retraction,當一條新資料到達時,就可以更新保留的歷史資料並對歷史上收到的所有資料進行重新排序,最後對之前傳送的結果進行 retraction 來實現。比如 reduce 運算元,可以進一步優化來避免實際儲存無限大的歷史狀態。另外增量計算中,由於資料到達時是無序的,因此它對 sql 的訪問也是無序的,從而可能導致隨機 io。最後,流處理模式依賴於定時 checkpoint 來支援 failover,這種情況也會導致一定的處理開銷。
  • 因此對於有限資料集的處理,我們也提供了專用的批處理模式,運算元通過逐級運算的方式來處理,因此只能用於有限資料的處理。這種情況下運算元實現可以進行特定的優化,比如對資料先進行排序,然後按 key 進行逐個處理,從而避免無限大的狀態隨機 io 的問題。

Flink 能夠保證,在兩種執行模式下,相同的有限輸入資料的處理結果可以保持一致。此外,它對兩種不同的模式也提供了統一的 pipelined region 排程器、統一的 shuffle service 外掛介面以及統一的 connector 介面,為兩種介面提供了統一的支援。

img

目前 Flink 的架構如上圖所示,無論是在 API 上還是在具體實現上,已經整體做到了流批一體的狀態。

二、語義增強與完善

對於上述流批一體的定義,我們在最近幾個版本中也進行了持續的完善和優化,第一部分是關於流批一體語義的增強與完善。

img

首先是在流模式下支援部分 task 結束後繼續做 checkpoint 工作。

目前流程之下作業的結束可以分為兩種情況:

  • 如果 source 是有限的,作業最終會執行結束;
  • 在無限 source 的情況下,使用者可以通過 stop-with-savepoint--drain 命令來終止作業,並保留一個 savepoint。如果不指定 drain 引數,就不會進行 drain 操作,這種情況一般是為了保留 savepoint 來重啟作業,不屬於作業終止的情況。

之前的 Flink 不支援部分 task 結束後進行 checkpoint,因為這會導致兩個問題:

  • 第一,兩階段提交的 sink 在流模式下依賴於 checkpoint 實現資料端到端的一致性。這種情況下,兩階段提交的 sink 會首先將資料寫入臨時檔案或外部事務中,只有當 Flink 內部的 checkpoint 成功之後,在保證 checkpoint 之前的資料不會進行重放的前提下,兩階段提交的 sink 才可以放心地通過重新命名檔案或提交事務的方式來進行事務實際的提交。如果部分 task 結束之後不能做 checkpoint,那麼最後一部分資料總是無法進行提交,也就無法保證流模式與批模式處理結果的一致性。
  • 第二,對於同時包括有限資料 source 和無限資料 source 的混合作業,如果部分執行結束後不能進行 checkpoint,那麼後續執行一旦發生 failover 就會由於回退導致較大的開銷。

img

為了解決上述問題,我們需要支援部分 task 結束之後也可以進行 checkpoint 的功能,並且修改作業結束的流程,使得使用兩階段提交的 task 最後可以等待一個 checkpoint 完成之後再退出。對於正常結束的情況,可以等待下一個 checkpoint 完成後退出;對於 drain 的情況,可以等待 savepoint 完成後退出。

img

為了實現部分 task 結束之後能進行 checkpoint,我們對 checkpoint 的流程進行了修改。首先重新識別新的 source task,就是那些前序任務都已經終止但本身尚未終止的 task,然後從這些 task 開始傳送 barrier 進行正常的 checkpoint 操作。由於 checkpoint 中 state 是以 jobvertext 為單位進行記錄的,因此如果一個 jobvertext 中所有 task 都已結束,會在它的狀態中記錄一個特殊的標記 ver,如果是部分 task 結束,會保留所有正在執行的 task state 作為 jobvertext state,而所有其他 jobvertext 的處理流程與正常 checkpoint 保持一致。作業發生 failover 重啟之後,會跳過完全終止的 jobvertext,對其他的 task 的處理邏輯與正常的處理邏輯保持一致的。

img

基於上述工作,我們對作業結束後的流程和語義也進行了重新的梳理。為了保證兩階段提交的sink能夠在提交最後一部分資料後再退出,必須使這些 task 能夠等待最後一個 checkpoint 之後再退出。目前的邏輯下,作業在自然結束的時候,首先會傳送 max watermark,然後傳送 EndOfPadtitionEvent。一個 task 收到 endofPadtitionEvent 之後會分別呼叫運算元的 endOfEInput()、close() 和 dispose() 操作。如果要在最後插入一個 checkpoint,那麼最好的方式插入到 close 方法之後。因為在這裡,作業已經完成了所有工作。

但是在實際場景下卻有所區別,因為實際場景下會觸發一個 savepoint,savepoint 成功之後,source 會呼叫分析式方法來結束執行,併傳送 max watermark EndOfPadtitionEvent,後續邏輯和 checkpoint 情況下一致。由於前面已經進行了 savepoint,如果在 close 之後再進行 checkpoint,會顯得非常冗餘。在這種情況下更合適的方式是先進行作業的結束,然後再進行 savepoint 操作,並且在 savepoint 操作的同時提交最後一部分資料。

但是這也存在一個問題,如果要保留最後一個 savepoint,那麼所有 task 就必須等待同一個 savepoint 才能結束,在自然結束的情況下,不同的 task 可以等待不同的 checkpoint 來退出。但是在 savepoint 情況下,作業結束之前已經傳送過 EndOfPadtitionEvent,它會關閉 task 之間的網路通訊,因此在作業終止之後無法再從最開始的 source 做 savepoint。

img

為了解決這些問題,必須能夠先通知所有 task 進行結束但不關閉網路連結,等所有 task 結束之後再發起一個 savepoint 操作,並且在成功之後再關閉網路連結,就能實現所有 task 等待最後同一個 savepoint 狀態而結束。

為了支援這一修改,我們引入了一條新的 EndOfDataEvent。任務收到 EndOfDataEvent 後會呼叫之前在 EndOfPadtitionEvent 進行的處理。之後 source 會立刻傳送一個 barrier 來觸發 savepoint 操作,運算元會它結束之後再執行退出邏輯。

此外,我們也對之前比較有歧義的 close() 和 dipose() 操作進行了重新命名,分別改成了 finish() 和 close(),其中 finish() 只會在任務正常結束時進行呼叫,而 close() 會在作業正常結束和異常結束的時候都進行呼叫。

img

在語義部分,我們進行的另外一個工作是 Hybrid source。

Hybrid source 支援使用者讀取歷史批資料之後,再切回到有限流資料上進行處理,它適用於處理邏輯一致的情況下進行流批互轉的操作。也就是在實時資料已經落盤使用者需要進行 backfill,且流批處理邏輯一致的情況下,使用者可以方便地使用 hybrid source 來實現作業。

三、效能優化

除了在語義方面進行的工作之外,我們在 runtime 層也進行了一些效能方面的優化。

3.1 排程部署效能優化

img

首先是關於排程部分的效能優化。Flink 由於存在 all to all 的連線關係,兩個併發為 n 的運算元之間會有 n² 條邊,這 n² 條邊顯式地存在 jm 的記憶體中,並且許多排程和部署邏輯也會直接依賴於它進行處理,從而導致 jm 記憶體空間和許多計算的時間和空間複雜度都是 on²。由於 batch 作業一般具有更大的規模,並且排程更加細粒度,因此這會加重排程和部署的效能問題。

img

為了解決這一問題,我們利用 all to all 邊的對稱性,對記憶體中的資料結構和計算邏輯進行了重構,引入了 comsumergroup 的資料結構來代替之前的 excutionEdge 對運算元之間的連線關係進行統一描述。這種方式不再重複描述堆對稱的資訊,從而避免了 n² 的複雜度。基於這一新的描述方式,我們不再在記憶體中維護 excutionEdge。

此外我們調整了許多排程的演算法,比如計算 pipeline region、在一個 task 結束之後計算後續需要排程的 task 等,將它們的時間複雜度也降低到了 O(n)。

img

計算 pipeline region 過程中還有一部分特殊邏輯,Flink 在作業 dag 圖中包含兩種邊, pipeline 邊和 blocking 邊。前者要求上下游的任務必須同時啟動並通過網路傳輸資料,後者則要求上下游任務依次啟動並通過檔案來傳輸資料。在排程前首先需要計算 pipeling region,一般來說可以按照 blocking 邊進行打斷,將所有通過 pipeline 邊相連的 task 放到同一個region裡,但這種邏輯存在一個問題,如上圖所示可以看出,因為併發 1 的 task 和併發 2 的 task 之間是通過 blocking 邊分成兩個 region 的,如果直接通過 blocking 邊打斷將它分為兩個 region。而因為 task1 和 task2 之間存在 all to all 的 shuffle 關係,最後在 region 組成的圖上會存在環形依賴的問題,在排程的時候會產生死鎖。

在之前的實踐中,我們通過 tarjan 強聯通分支演算法來識別這種環境依賴,彼時的識別是直接在 excutiongraph 上進行,所以它的時間複雜度是 O(n²),因為 all to all 的邊存在 n² 的連線。通過進一步分析發現,如果在 jobgraph 中直接進行 pipeline 的認證識別,只要圖中有 all to all 的邊就一定存在環形依賴,因此可以直接在 jobgraph 上先進行判斷,識別出所有 all to all 的邊,然後在 excutiongraph 上再對非 all to all 的邊進行處理。

通過這種方式,可以將環形依賴識別的複雜度降低到 O(n)。

3.2 部署效能優化

img

另一部分優化是關於部署效能。

Flink 在部署 task 的時候會攜帶它的 shuffle descriptors。對於上游來說,shuffle descriptors 描述了資料產出的位置,而對於下游來說,它描述了需要拉取資料的位置。shuffle descriptors 與 ExcutionEdge 的數量是相等的,因此這個數量級也是 O(n²)。在記憶體中進行計算序列化儲存的時候,shuffle descriptors 會消耗大量 CPU 和記憶體,卡死主執行緒,導致 TM 及耗盡記憶體的問題。

但是由於上下游存在對稱性,因此有很多 shuffle descriptors 其實是重複的,我們可以通過快取 shuffle descriptors 的方式來降低維護它的數量。

另外為了進一步防止併發量過大導致 shuffle descriptors 過大,導致記憶體 oom,我們改用 BlobServer 來傳輸 shuffle descriptors。

img

實現了上述優化以後,我們採用一個 10000×10000 的 all to all 兩級作業進行測試,可以看出排程和記憶體佔用縮減了 90% 以上,部署時間縮減 65% 以上,極大提高了排程和部署的效能。

img

流執行模式排程和部署的優化極大地減少作業 failover 時重新啟動的時間,但是一旦發生 failover,仍然需要花費一定的時間來進行重新部署以及初始化、載入 state 等工作。為了進一步減少這個時間,我們正在嘗試在作業發生 failover 的時候,只重啟出錯節點。其中的難點在於如何保證資料的一致性,我們目前正在探索中。

img

另外一部分 runtime 的優化是在流模式下通過 Buffer Debloating 來動態調整 buffer 的大小,從而在反壓的情況下減少 checkpoint 的 buffer 對齊所需要的時間,避免 checkpoint 超時。如果產生反壓,當作業中間快取的資料量過大時,可以及時減少 buffer 的大小來控制中間快取的資料大小,從而避免因為處理資料而阻塞 barrier 的情況。

四、Remote Shuffle

img

shuffle 是批處理作業執行中非常重要的一部分,由於雲原生可以提供統一的運維 API、減少運維開銷,以及在離線混部和動態伸縮的情況下提供更好的支援,Flink 在最近的幾個版本里也在積極擁抱雲原生,比如提供了 Flink on k8s 的完整的實現,以及支援動態伸縮的 schedule。但是由於 Flink shuffle 需要使用本地磁碟,如果要支援雲原生的 Flink,我們也需要實現儲存計算分離的 shuffle。儲存計算分離的架構可以使得計算資源與儲存資源單獨伸縮,避免 task manager 無法在計算完成後立刻退出,從而提高整個資源的利用率。同時也可以避免 task 執行失敗導致 TM 退出而影響 shuffle 檔案服務的穩定性,從而對下游的執行造成影響。

針對上述儲存計算分離 shuffle 的需求,我們在內部也研發了 remote shuffle service,這一功能已經於今年年初在內部上線。經過一段時間的試用,我們在前段時間對這一系統進行了開源,下面將對這一系統進行詳細介紹。

img

Flink 可以支援多種不同的場景,不同場景下的 shuffle 在儲存介質傳輸方式和部署方式方面是存在較大差異的。比如流處理模式下,Flink 一般採用的基於網路的線上傳輸方式,資料快取在上游 TM 的記憶體中,並在下游 task 有空閒 buffer 的時候及時進行傳送。而分析處理模式下,為了支援運算元的逐級執行,Flink 還需要支援基於檔案的離線傳輸方式,先寫入離線檔案中,下游 task 啟動之後再通過網路傳送給下游的 task,離線檔案可以存在本地的 TM 中,也可以存在遠端的服務中。

另外,不同的 shuffle 在生命週期管理、後設資料管理和資料分發策略方面也存在許多共同需求,所有 shuffle 都需要排程器在啟動上游 task 的時候,申請相應的 shuffle 資源,並對其進行記錄。還需要排程器在部署下游 task 的同時,攜帶 shuffle 的資源描述符,從而使下游 task 可以順利讀取相應的資料。最後 shuffle 還依賴排程器在特定的生命週期比如結束或者執行失敗的時候,對它的資源進行清理。

為了對不同的 shuffle 提供統一的支援,Flink 從 1.9 版本開始引入了外掛化的 shuffle 架構。一個 shuffle 的外掛主要由兩部分組成,shuffle master 負責在 jm 端與排程器進行互動,從而實現申請和釋放 shuffle 資源的功能;而 result partition 和 input gate 分別作為資料的 write 和 read 端,根據排程器提供的 shuffle 資源描述符,將資料輸出到特定位置或從資料位置進行讀取。而所有 shuffle 實現中,共性的部分則由 Flink 統一實現,排程器會通過 partition track 對已經申請的 shuffle 資源進行記錄,並根據作業的執行模式維護 shuffle 資源的生命週期。

通過統一的外掛化 shuffle 介面,Flink 可以簡化不同 shuffle 實現的複雜度,且允許不同的 shuffle 在實際儲存與傳輸的方式上進行自由選擇。

img

基於 Flink 統一的外掛化 shuffle 介面,Flink remote shuffle 的整體架構如上圖所示。它的 shuffle 服務由一個單獨叢集提供,其中 shuffle manager 作為整個叢集的 master 節點,負責管理 worker 節點,並對 shuffle 資料集進行分配和管理。Shuffle worker 作為整個叢集的 slave 節點,負責讀寫和清理資料集,Shuffle manager 還通過心跳對 Shuffle worker 和 Shuffle master 進行監聽,在心跳超時的時候做資料刪除和同步,從而使得叢集中資料集的狀態保持一致。

img

我們對傳輸過程也進行了大量優化。網路部分基於 credit-based 協議來實現,它與 Flink 目前的網路傳輸協議類似,我們還在其中實現了 tcp 連線複用,以及壓縮可控記憶體管理和零拷貝等一系列優化。io 部分我們提供了支援 io 排程優化的 mapPartition 儲存格式。通過 io 排程優化,它在 hdd 上的訪問速度達到 140M/s 以上。

此外我們目前也正在開發基於預先 merge 的 reducepartition 的儲存格式,它會將資料根據下游預先進行 merge,並儲存到特定的 worker 上,下游不能全部同時啟動時,可以取得比 mapPartition 更好的效果。

img

在部署上,Remote shuffle 可以支援多種不同的部署方式,此外我們也提供了版本間的協議相容,使得當伺服器端進行升級的時候,無需升級客戶端。最後我們還在系統中提供了常用 metric 的操作,更多的運維工具也正在積極開發中。

五、總結與展望

img

總的來說,Flink 目前已經具備可以上線的流批一體的資料處理能力,未來我們也將進一步提升這項能力,並在更多的流批融合場景下提供支援,例如相對 Hybrid source,在 backfill 的場景下,如果流批處理的邏輯不一致,我們也在考慮支援批處理結束後保留狀態用於啟動流處理作業的方式。

另外我們也將進一步提高整個系統的穩定性與效能,並更深入地考慮流模式和批處理模式的本質差異,以及流批一體更深刻的內涵。


點選檢視直播回放 & 演講PDF

更多 Flink 相關技術問題,可掃碼加入社群釘釘交流群
第一時間獲取最新技術文章和社群動態,請關注公眾號~

image.png

相關文章