Flink Slot 詳解與 Job Execution Graph 優化

轉載:https://www.infoq.cn/article/ZmL7TCcEchvANY-9jG1H

前言

近期公司內部將 Flink Job 從 Standalone 遷移至了 OnYarn,隨後發現 Job 性能較之前有所降低:遷移前有 8.3W+/S 的數據消費速度,遷移到 Yarn 後分配同樣的資源但消費速度降爲 7.8W+/S,且較之前的消費速度有輕微的抖動。經過原因分析和測試驗證,最終採用了在保持分配給 Job 的資源不變的情況下將總 Container 數量減半、每個 Container 持有的資源從 1C2G 1Slot 變更爲 2C4G 2Slot 的方式,使該問題得以解決。

經歷該問題後,發現深入理解 Slot 和 Flink Runtime Graph 是十分必要的,於是撰寫了這篇文章。本文內容分爲兩大部分,第一部分詳細的分析 Flink Slot 與 Job 運行的關係,第二部詳細的介紹遇到的問題和解決方案。

Flink Slot

Flink 集羣是由 JobManager(JM)、TaskManager(TM)兩大組件組成的,每個 JM/TM 都是運行在一個獨立的 JVM 進程中。JM 相當於 Master,是集羣的管理節點,TM 相當於 Worker,是集羣的工作節點,每個 TM 最少持有 1 個 Slot,Slot 是 Flink 執行 Job 時的最小資源分配單位,在 Slot 中運行着具體的 Task 任務。

對 TM 而言:它佔用着一定數量的 CPU 和 Memory 資源,具體可通過 taskmanager.numberOfTaskSlots, taskmanager.heap.size 來配置,實際上 taskmanager.numberOfTaskSlots 只是指定 TM 的 Slot 數量,並不能隔離指定數量的 CPU 給 TM 使用。在不考慮 Slot Sharing(下文詳述)的情況下,一個 Slot 內運行着一個 SubTask(Task 實現 Runable,SubTask 是一個執行 Task 的具體實例),所以官方建議 taskmanager.numberOfTaskSlots 配置的 Slot 數量和 CPU 相等或成比例。

當然,我們可以藉助 Yarn 等調度系統,用 Flink On Yarn 的模式來爲 Yarn Container 分配指定數量的 CPU 資源,以達到較嚴格的 CPU 隔離(Yarn 採用 Cgroup 做基於時間片的資源調度,每個 Container 內運行着一個 JM/TM 實例)。而 taskmanager.heap.size 用來配置 TM 的 Memory,如果一個 TM 有 N 個 Slot,則每個 Slot 分配到的 Memory 大小爲整個 TM Memory 的 1/N,同一個 TM 內的 Slots 只有 Memory 隔離,CPU 是共享的。

對 Job 而言:一個 Job 所需的 Slot 數量大於等於 Operator 配置的最大 Parallelism 數,在保持所有 Operator 的 slotSharingGroup 一致的前提下 Job 所需的 Slot 數量與 Job 中 Operator 配置的最大 Parallelism 相等。

關於 TM/Slot 之間的關係可以參考如下從官方文檔截取到的三張圖:

圖一: Flink On Yarn 的 Job 提交過程,從圖中我們可以瞭解到每個 JM/TM 實例都分屬於不同的 Yarn Container,且每個 Container 內只會有一個 JM 或 TM 實例;通過對 Yarn 的學習我們可以瞭解到,每個 Container 都是一個獨立的進程,一臺物理機可以有多個 Container 存在(多個進程),每個 Container 都持有一定數量的 CPU 和 Memory 資源,而且是資源隔離的,進程間不共享,這就可以保證同一臺機器上的多個 TM 之間是資源隔離的(Standalone 模式下,同一臺機器下若有多個 TM,是做不到 TM 之間的 CPU 資源隔離的)。

Flink Slot詳解與Job Execution Graph優化

圖二: Flink Job 運行圖,圖中有兩個 TM,各自有 3 個 Slot,2 個 Slot 內有 Task 在執行,1 個 Slot 空閒。若這兩個 TM 在不同 Container 或容器上,則其佔用的資源是互相隔離的。在 TM 內多個 Slot 間是各自擁有 1/3 TM 的 Memory,共享 TM 的 CPU、網絡(Tcp:ZK、 Akka、Netty 服務等)、心跳信息、Flink 結構化的數據集等。

Flink Slot詳解與Job Execution Graph優化

圖三: Task Slot 的內部結構圖,Slot 內運行着具體的 Task,它是在線程中執行的 Runable 對象(每個虛線框代表一個線程),這些 Task 實例在源碼中對應的類是 org.apache.flink.runtime.taskmanager.Task。每個 Task 都是由一組 Operators Chaining 在一起的工作集合,Flink Job 的執行過程可看作一張 DAG 圖,Task 是 DAG 圖上的頂點(Vertex),頂點之間通過數據傳遞方式相互鏈接構成整個 Job 的 Execution Graph。

Flink Slot詳解與Job Execution Graph優化

Operator Chain

Operator Chain 是指將 Job 中的 Operators 按照一定策略(例如: single output operator 可以 chain 在一起)鏈接起來並放置在一個 Task 線程中執行。Operator Chain 默認開啓,可通過 StreamExecutionEnvironment.disableOperatorChaining() 關閉,Flink Operator 類似 Storm 中的 Bolt,在 Strom 中上游 Bolt 到下游會經過網絡上的數據傳遞,而 Flink 的 Operator Chain 將多個 Operator 鏈接到一起執行,減少了數據傳遞 / 線程切換等環節,降低系統開銷的同時增加了資源利用率和 Job 性能。實際開發過程中需要開發者瞭解這些原理,並能合理分配 Memory 和 CPU 給到每個 Task 線程。
注: 【一個需要注意的地方】Chained 的 Operators 之間的數據傳遞默認需要經過數據的拷貝(例如:kryo.copy(…)),將上游 Operator 的輸出序列化出一個新對象並傳遞給下游 Operator,可以通過 ExecutionConfig.enableObjectReuse() 開啓對象重用,這樣就關閉了這層 copy 操作,可以減少對象序列化開銷和 GC 壓力等,具體源碼可閱讀 org.apache.flink.streaming.runtime.tasks.OperatorChain 與 org.apache.flink.streaming.runtime.tasks.OperatorChain.CopyingChainingOutput。官方建議開發人員在完全瞭解 reuse 內部機制後才使用該功能,冒然使用可能會給程序帶來 bug。

Operator Chain 效果可參考如下官方文檔截圖:

圖四: 圖的上半部分是 StreamGraph 視角,有 Task 類別無並行度,如圖:Job Runtime 時有三種類型的 Task,分別是 Source->Map、keyBy/window/apply、Sink,其中 Source->Map 是 Source() 和 Map()chaining 在一起的 Task;圖的下半部分是一個 Job Runtime 期的實際狀態,Job 最大的並行度爲 2,有 5 個 SubTask(即 5 個執行線程)。若沒有 Operator Chain,則 Source() 和 Map() 分屬不同的 Thread,Task 線程數會增加到 7,線程切換和數據傳遞開銷等較之前有所增加,處理延遲和性能會較之前差。補充:在 slotSharingGroup 用默認或相同組名時,當前 Job 運行需 2 個 Slot(與 Job 最大 Parallelism 相等)。

Flink Slot詳解與Job Execution Graph優化

Slot Sharing

Slot Sharing 是指,來自同一個 Job 且擁有相同 slotSharingGroup(默認:default)名稱的不同 Task 的 SubTask 之間可以共享一個 Slot,這使得一個 Slot 有機會持有 Job 的一整條 Pipeline,這也是上文提到的在默認 slotSharing 的條件下 Job 啓動所需的 Slot 數和 Job 中 Operator 的最大 parallelism 相等的原因。通過 Slot Sharing 機制可以更進一步提高 Job 運行性能,在 Slot 數不變的情況下增加了 Operator 可設置的最大的並行度,讓類似 window 這種消耗資源的 Task 以最大的並行度分佈在不同 TM 上,同時像 map、filter 這種較簡單的操作也不會獨佔 Slot 資源,降低資源浪費的可能性。

具體 Slot Sharing 效果可參考如下官方文檔截圖:

圖五: 圖的左下角是一個 soure-map-reduce 模型的 Job,source 和 map 是 4 parallelism,reduce 是 3 parallelism,總計 11 個 SubTask;這個 Job 最大 Parallelism 是 4,所以將這個 Job 發佈到左側上面的兩個 TM 上時得到圖右側的運行圖,一共佔用四個 Slot,有三個 Slot 擁有完整的 source-map-reduce 模型的 Pipeline,如右側圖所示;注:map 的結果會 shuffle 到 reduce 端,右側圖的箭頭只是說 Slot 內數據 Pipline,沒畫出 Job 的數據 shuffle 過程。

Flink Slot詳解與Job Execution Graph優化

圖六: 圖中包含 source-map[6 parallelism]、keyBy/window/apply[6 parallelism]、sink[1 parallelism] 三種 Task,總計佔用了 6 個 Slot;由左向右開始第一個 slot 內部運行着 3 個 SubTask[3 Thread],持有 Job 的一條完整 pipeline;剩下 5 個 Slot 內分別運行着 2 個 SubTask[2 Thread],數據最終通過網絡傳遞給 Sink 完成數據處理。

Flink Slot詳解與Job Execution Graph優化

Operator Chain & Slot Sharing API

Flink 在默認情況下有策略對 Job 進行 Operator Chain 和 Slot Sharing 的控制,比如:將並行度相同且連續的 SingleOutputStreamOperator 操作 chain 在一起(chain 的條件較苛刻,不止單一輸出這一條,具體可閱讀 org.apache.flink.streaming.api.graph.StreamingJobGraphGenerator.isChainable(…)),Job 的所有 Task 都採用名爲 default 的 slotSharingGroup 做 Slot Sharing。但在實際的需求場景中,我們可能會遇到需人爲干預 Job 的 Operator Chain 或 Slot Sharing 策略的情況,本段就重點關注下用於改變默認 Chain 和 Sharing 策略的 API。

StreamExecutionEnvironment.disableOperatorChaining(): 關閉整個 Job 的 Operator Chain,每個 Operator 獨自佔有一個 Task,如上圖四所描述的 Job,如果 disableOperatorChaining 則 source->map 會拆開爲 source(), map() 兩種 Task,Job 實際的 Task 數會增加到 7。這個設置會降低 Job 性能,在非生產環境的測試或 profiling 時可以藉助以更好分析問題,實際生產過程中不建議使用。
someStream.filter(…).map(…).startNewChain().map(): startNewChain() 是指從當前 Operator[map] 開始一個新的 chain,即:兩個 map 會 chaining 在一起而 filter 不會(因爲 startNewChain 的存在使得第一次 map 與 filter 斷開了 chain)。
someStream.map(…).disableChaining(): disableChaining() 是指當前 Operator[map] 禁用 Operator Chain,即:Operator[map] 會獨自佔用一個 Task。
someStream.map(…).slotSharingGroup(“name”): 默認情況下所有 Operator 的 slotGroup 都爲 default,可以通過 slotSharingGroup() 進行自定義,Flink 會將擁有相同 slotGroup 名稱的 Operators 運行在相同 Slot 內,不同 slotGroup 名稱的 Operators 運行在其他 Slot 內。
Operator Chain 有三種策略 ALWAYS、NEVER、HEAD,詳細可查看 org.apache.flink.streaming.api.operators.ChainingStrategy。startNewChain() 對應的策略是 ChainingStrategy.HEAD(StreamOperator 的默認策略),disableChaining() 對應的策略是 ChainingStrategy.NEVER,ALWAYS 是儘可能的將 Operators chaining 在一起;在通常情況下 ALWAYS 是效率最高,很多 Operator 會將默認策略覆蓋爲 ALWAYS,如 filter、map、flatMap 等函數。

遷移 OnYarn 後 Job 性能下降的問題

JOB 說明:
類似 StreamETL,100 parallelism,即:一個流式的 ETL Job,不包含 window 等操作,Job 的並行度爲 100;

環境說明:

  1. Standalone 下的 Job Execution Graph:10TMs * 10Slots-per-TM ,即:Job 的 Task 運行在 10 個 TM 節點上,每個 TM 上佔用 10 個 Slot,每個 Slot 可用 1C2G 資源,GCConf:-XX:+UseG1GC -XX:MaxGCPauseMillis=100;
  2. OnYarn 下初始狀態的 Job Execution Graph:100TMs * 1Slot-per-TM,即:Job 的 Task 運行在 100 個 Container 上,每個 Container 上的 TM 持有 1 個 Slot,每個 Container 分配 1C2G 資源,GCConf:-XX:+UseG1GC -XX:MaxGCPauseMillis=100;
  3. OnYarn 下調整後的 Job Execution Graph:50TMs * 2Slot-per-TM,即:Job 的 Task 運行在 50 個 Container 上,每個 Container 上的 TM 持有 2 個 Slot,每個 Container 分配 2C4G 資源,GCConfig:-XX:+UseG1GC -XX:MaxGCPauseMillis=100;

注: OnYarn 下使用了與 Standalone 一致的 GC 配置,當前 Job 在 Standalone 或 OnYarn 環境中運行時,YGC、FGC 頻率基本相同,OnYarn 下單個 Container 的堆內存較小使得單次 GC 耗時減少。生產環境中大家最好對比下 CMS 和 G1,選擇更好的 GC 策略,當前上下文中暫時認爲 GC 對 Job 性能影響可忽略不計。

問題分析:
引起 Job 性能降低的原因不難定位,從這張 Container 的線程圖(VisualVM 中的截圖)可見:
圖 7:在一個 1C2G 的 Container 內有 126 個活躍線程,守護線程 78 個。首先,在一個 1C2G 的 Container 中運行着 126 個活躍線程,頻繁的線程切換是會經常出現的,這讓本來就不充裕的 CPU 顯得更加的匱乏。其次,真正與數據處理相關的線程是紅色畫筆圈出的 14 條線程(2 條 Kafka Partition Consumer、Consumers 和 Operators 包含在這個兩個線程內;12 條 Kafka Producer 線程,將處理好的數據 sink 到 Kafka Topic),這 14 條線程之外的大多數線程在相同 TM、不同 Slot 間可以共用,比如:ZK-Curator、Dubbo-Client、GC-Thread、Flink-Akka、Flink-Netty、Flink-Metrics 等線程,完全可以通過增加 TM 下 Slot 數量達到多個 SubTask 共享的目的。
此時我們會很自然的得出一個解決辦法:在 Job 使用資源不變的情況下,在減少 Container 數量的同時增加單個 Container 持有的 CPU、Memory、Slot 數量,比如上文環境說明中從方案 2 調整到方案 3,實際調整後的 Job 運行穩定了許多且消費速度與 Standalone 基本持平。

Flink Slot詳解與Job Execution Graph優化

注: 當前問題是內部遷移類似 StreamETL 的 Job 時遇到的,解決方案簡單但不具有普適性,對於帶有 window 算子的 Job 需要更仔細縝密的問題分析。目前 Deploy 到 Yarn 集羣的 Job 都配置了 JMX/Prometheus 兩種監控,單個 Container 下 Slot 數量越多、每次 scrape 的數據越多,實際生成環境中需觀測是否會影響 Job 正常運行,在測試時將 Container 配置爲 3C6G 3Slot 時發現一次 java.lang.OutOfMemoryError: Direct buffer memory 的異常,初步判斷與 Prometheus Client 相關,可適當調整 JVM 的 MaxDirectMemorySize 來解決。所出現異常如圖 8

Flink Slot詳解與Job Execution Graph優化

總結

Operator Chain 是將多個 Operator 鏈接在一起放置在一個 Task 中,只針對 Operator;Slot Sharing 是在一個 Slot 中執行多個 Task,針對的是 Operator Chain 之後的 Task。這兩種優化都充分利用了計算資源,減少了不必要的開銷,提升了 Job 的運行性能。此外,Operator Chain 的源碼在 streaming 包下,只在流處理任務中有這個機制;Slot Sharing 在 flink-runtime 包下,似乎應用更廣泛一些(具體還有待考究)。
最後,只有充分的瞭解 Slot、Operator Chain、Slot Sharing 是什麼,以及各自的作用和相互間的關係,才能編寫出優秀的代碼並高效的運行在集羣上。

參考資料:

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