流批一體架構在快手的實踐和思考

ApacheFlink發表於2023-02-17

摘要:本文整理自快手技術專家、Apache Flink & Apache Calcite Committer 張靜,在 FFA 流批一體專場的分享。本篇內容主要分為四個部分:


  1. Flink 在快手的發展

  2. 流批一體在快手的規劃

  3. 第一階段(加強批能力)的進展

  4. 第二階段(業務視角的流批一體)的挑戰

Tips:點選「閱讀原文」檢視原文影片&演講 ppt

01

Flink 在快手的發展


流批一體架構在快手的實踐和思考

在快手內部,Flink 的體量無論從作業規模還是叢集規模上,相對於去年都有大幅的提升,上圖列了幾個關鍵資料。峰值的 TPS 達到了每秒 13 億,作業數量上流作業有 6000 多個,批作業也到了 3000 個,物理資源上已經有 70 萬 Cores。在業務場景的覆蓋上,Flink 給公司的各個業務方,包括社科、電商、商業化、音影片、實時數倉都提供了實時計算能力。


流批一體架構在快手的實踐和思考

2017 年快手內部為了滿足直播、短影片實時質量監控的需求,初步引入了 Flink 引擎,後面幾年陸續建立了周邊體系,提高穩定性和效能,把 Flink 推廣到公司的各個業務線。


2020 年開始大力推動 Flink SQL 的落地,關於這些工作,去年 FFA 上也做了分享。2021 年底,有一些用 Flink 處理實時需求的業務方,希望也能用 Flink Batch 解決部分離線場景的需求,所以我們開始推流批一體這個方向,到去年也取得了一些階段性的成果。


今天我們分享的重點有兩個。


  • 第一個是回顧我們過去在流批一體方向踩過的坑,解決的過程,希望能夠給同行的朋友帶來參考意義。


  • 第二個重點的內容是我們遇到哪些新的挑戰以及有哪些新的思考。


流批一體架構在快手的實踐和思考

提到流批一體難免就要提到 Lambda 架構,每個公司都有自己的離線計算方案,這個方向已經發展了很多年成熟可靠。缺點是不能滿足越來越多的實時需求,所以又引入另外一條實時鏈路。在實時計算探索初期,結果的準確性不能得到保證,要離線計算結果來加以校正和修正,由業務來負責合併兩個結果,這就是常說的 Lambda 架構。


它的缺點也很明顯,首先需要提供兩套計算引擎,一個提供 Streaming 能力,一個提供 Batch 能力。不同的引擎提供的 API 不一樣,所以業務需要開發兩套業務邏輯。而且因為不同引擎的行為可能會有差異,尤其是在標準沒有明確規定的 case 裡,所以兩條計算鏈路結果的一致性也很難得到保證。


流批一體架構在快手的實踐和思考

過去幾年,一直都在嘗試解決 Lambda 架構帶來的問題,上圖列舉了其中三個比較典型的專案。


  • 第一個是 Bean,它引入統一的 API,不需要底層的引擎也統一。這樣之前提到的不同引擎造成結果不一致的問題,也很難去解。而且對一些複雜的流批一體的業務場景,比如有狀態作業的流批混跑,批模式執行完以後生成中間狀態,讓流模式啟動的時候載入這些快照資料。如果流和批用兩條單獨的執行引擎,很難通用的解決剛才說的這種場景。


  • 第二個是 Spark,它是一個已經得到廣泛使用的批處理引擎。不過它基於微批和頻繁排程的思路,也可以提供準實時的能力。但它因為是基於批來做流,很難滿足極致的實時要求。


  • 第三個是 Flink,經過這些年的發展,Flink 已經成為流計算領域的實時標準,同時它也提供批處理的能力。但它也喲一些缺點,就是它的批能力和傳統的離線計算引擎相比,還有待進一步增強。


流批一體架構在快手的實踐和思考

我們內部最開始收到流批一體需求的業務方,都對業務的實時性有較強的要求,希望在不降低實時性的前提下,用 Flink Batch 提高開發效率。


另外我們也看到流批一體也是社群重點的投入方向。Flink 在過去幾個版本里,幾乎每個版本都做了流批一體的能力建設。上圖是引自去年阿里雲分享的一篇文章,今年社群也做了很多在批上的工作,比如推測執行、Remote Shuffle Service、Hybrid Shuffle 等等。有一些較新的工作成果沒有反映在這個圖上,不過足以看出 Flink 已經在 API 層、Connector 層、排程層、底層的 Shuffle 上都做了非常多的流批一體統一的工作。所以我們選擇用 Flink 來做流批一體,確立了和社群合作共建的方式。


在我們內部落地的過程中,社群給了我們非常多的支援和啟發。今天我分享裡提到的工作也是和社群通力合作的結果。而且很多功能在我們內部得到驗證以後,已經推出社群版本,希望能夠普惠到更多使用者。


02

流批一體在快手的規劃


流批一體架構在快手的實踐和思考

我們最早收到的流批一體的訴求來源於兩個內部業務,機器學習和資料整合。機器學習場景一直是我們內部 Flink Streaming 的重點業務方,他們用 Flink 做實時特徵計算和實時樣本拼接。他們希望能夠用 Flink Batch 複用一套業務邏輯,來滿足回溯的需求,做資料修正和冷啟動生成歷史資料。而且用一個引擎也可以避免結果不一致的問題。


流批一體架構在快手的實踐和思考

另一個需求來自於資料同步團隊,資料同步產品在異構的資料來源之間做資料同步,分為離線同步和實時同步。以前老的架構,離線同步基於 MR 和 DataX,實時是基於 MR 和自研框架。這個架構的缺點是計算能力弱,擴充套件性不強。所以資料同步團隊正在基於 Flink 來打造新的版本,希望用 Flink 計算能力和可擴充套件性來增強資料同步這個產品。


流批一體架構在快手的實踐和思考

我們認為流批一體的終極目標有以下幾點。


  • 統一的使用者體驗。使用者只需要一套業務程式碼,並且在統一的平臺上開發運維和管理作業。


  • 統一的引擎 ,包括統一的計算引擎和儲存引擎。


  • 更智慧的引擎,提高使用者體驗。不需要使用者瞭解流模式和批模式是什麼,也不需要使用者操心應該選擇哪種執行模式、Shuffle 方式、排程器等底層細節。


  • 滿足更復雜的業務,比如流批融合需求。目標是要能支援更復雜的業務需求,比如有狀態作業的流批混跑。


流批一體架構在快手的實踐和思考

要想實現這些目標,需要對框架層做大量的改進,我們採用分階段建設的思路來逐步實現這些目標。


第一個階段的目標是支援好目前對流批一體有需求的兩個業務方,提供統一的使用者體驗和計算引擎,讓使用者先能用起來。規劃的重點是加強 Flink 的批能力,打通產品入口以及給業務進行貼身的支援。


第二階段的目標是讓產品更好用,包括智慧的引擎、極致的批處理效能,統一的儲存引擎,且能支援更廣泛、更復雜的業務需求。


03

第一階段(加強批能力)的進展


流批一體架構在快手的實踐和思考

我們第一期的使用者來源於機器學習場景和資料同步場景。使用者對批能力的需求,總結起來,穩定性是生產可用的必要條件,另外也希望在易用性上有所提升,對效能和功能的要求在這個階段相對弱化一些,希望大部分 pattern 下沒有明顯效能回退就可以。所以在第一階段加強能力建設上,我們內部的重心集中在穩定性和易用性上,這也是我今天重點分享的內容,補充一點,其實在效能上我們也在 Hive 的 Source/Sink 上也做了最佳化,感興趣的聽眾可以關注 生產實踐專場《Hive SQL 遷移 Flink SQL 在快手的實踐》這個分享。


流批一體架構在快手的實踐和思考

以下是 Flink Batch 穩定性的核心問題:


  • 慢節點問題。在一個分散式系統裡,個別的機器故障、資源緊張或者是網路問題都可能導致單個併發的效能下降,這些慢的節點可能成為整個作業的瓶頸。


  • TaskManager Shuffle 不穩定。之前採用的 Native TaskManager Shuffle 方式,Shuffle 服務不穩定。


  • 離線任務穩定性差。主要原因有兩個,第一個是離線叢集開啟資源搶佔,中低優任務的資源頻繁被搶佔。第二個是離線叢集資源緊張,導致併發間 splits 分配不均勻,failover 開銷大。


流批一體架構在快手的實踐和思考

第一個,慢節點問題。在分散式環境下,很可能出現個別節點比其他節點慢的情況。大概有兩類原因,一個是個別併發乾的活兒比其他的併發多,第二個是大家乾的活兒差不多的情況下,有一些併發乾的比較慢。


這兩種解決思路完全不同,我們今天主要分享的是第二類問題,也就是如何處理批模式下,機器原因或者網路原引發的慢節點問題。如上圖所示,聚合有三個併發,每一個併發處理的資料量基本一致,但由於第三個併發所在機器過於繁忙,導致聚合運算元處理資料速度遠遠慢於併發 1 和併發 2。


流批一體架構在快手的實踐和思考

所以 Flink 引入推測執行來解決慢節點的問題,和傳統 MapReduce、Spark 的思路類似。檢測到長尾任務後,在非熱的機器上部署長尾任務的映象例項。如上圖所示,第三個聚合的 worker 在另外一個機器上拉起了一個映象例項,即 Agg3’。這兩個哪個先執行完就用哪個結果,並把其他的取消掉。


流批一體架構在快手的實踐和思考

這裡假設是後來啟動的例項先執行完,也就是隻有它產出的資料對下游可見,並且會把原來的 Agg3 取消掉。


流批一體架構在快手的實踐和思考

上圖反映了框架層面支援推測執行的實現細節。在整體架構裡需要如下幾個元件:


  • JobMaster 裡的 Task Detector;它用來定期檢查是否出現了長尾任務。


  • Scheduler 為長尾任務臨時的建立部署新的映象例項。


  • 黑名單機制,它用來把映象任務分配到和原來不一樣的機器上。



流批一體架構在快手的實踐和思考

除了框架層面,還需要 connector 支援推測執行。我們對 Source 支援推測執行的目標是透過框架層的改動,不需要每個 Source 有額外的開發。在 Flink 裡 Source Task 和資料分片的分配關係,不是編譯期間就固定好的,而是在執行的時候,每個 Task 處理完一個分片後再去申請下一個分片。


引入推測執行以後,為了保證資料的正確性,新啟動的映象例項必須和之前的例項處理相同的分片集合,所以我們在快取裡記錄了 sub-task 和已經分配到的 splits 集合的對映關係。


如上圖所示,sub-task1 已經處理了 split1 和 split2,正在處理 Split3。但因為處理的比較慢,被判定為慢節點,在另外一個機器上啟動了一個映象例項,即 Source1’。在申請 split 的時候,JobMaster 會把快取記錄裡 sub-task 處理過的 splits 給 Source1’,只有當已經走完原來的 Source1 走過的足跡後,JobMaster 才會給 sub-task1 分配新的 split,並會把新的 split 記錄到快取裡。


流批一體架構在快手的實踐和思考

和 Source 不一樣,大部分 Sink Connector 需要額外的開發來避免寫入衝突或者提交衝突。目前我們內部在 File Sink 和 Hive Sink 這兩個 Sink 上支援了推測執行。這兩個 Sink 底層都用到了 FileSystemOutputFormat,同時我們還引入了一個新的介面叫 SpeculativeFinalizeOnMaster,這個介面和原來的 FinalizeOnMaster 的核心區別是 JobMaster 在回撥的時候,會額外傳入 sub-task 和最快結束例項之間的對映關係。


另外,我們還修改了 FileSystemOutputFormat 對臨時檔案的組織方式,在檔案組織目錄里加了例項資訊。在所有的併發完成後,每個 subtask 裡最快執行完的例項,所產生的臨時檔案才會被挪到正式目錄下,其他慢例項產生的檔案會被刪除。因為時間關係,Sink 支援推測執行沒來得及進入 1.16 版本,會在 1.17 版本釋出。到時不僅會支援 OutputFormat Sink,也會支援 FLIP-143 引入的 New Sink。


流批一體架構在快手的實踐和思考

第二個,TaskManager Shuffle 不穩定。Flink Batch 作業有兩種資料交換方式,一種是不落盤的 Pipeline Shuffle,一種是 Blocking shuffle。對於 Blocking Shuffle,上游 task 需要把 Shuffle 資料寫到離線檔案中,等下游 task 啟動以後,再來消費 Shuffle 的資料。Shuffle 資料可以落在本地的 TaskManager 裡面也可以落在遠端的服務裡。


Flink 社群版本提供了兩種 Blocking Shuffle 的實現。第一個是 TaskManager Shuffle,是把上游計算節點資料寫到本地盤,下游節點連線到上游 TaskManager 上讀取 Shuffle 檔案。所以 TaskManager 計算工作完成以後,不能立刻退出,要等下游消費完 Shuffle 檔案後才能釋放掉。這樣不僅造成了資源浪費,而且容錯代價大。


第二種是 Remote Shuffle Service,它透過單獨的叢集提供資料的 Shuffle 服務,可以避免 TaskManager Shuffle 的資源利用率低和容錯開銷大的問題,而且它是一種擁抱雲原生實現存算分離的方案。


流批一體架構在快手的實踐和思考

快手內部已經有了類似的 Remote Shuffle Service 實現,所以為了複用之前的技術降低成本,我們最終採用了內部的 Remote Shuffle Service 實現,這個架構主要分為五個角色。


  • 第一個角色是 Shuffle Master,它負責全域性的 Shuffle 資源排程,管理 Shuffle Worker,讓其吸引的 Worker 負載均衡。


  • 第二個角色是 APP Shuffle Manager,它和計算引擎的排程器互動來負責單個作業 Shuffle 的申請和釋放。


  • 第三個角色是 Shuffle Worker, 作為整個叢集的 slave 節點,負責將資料按照 Partition 維度聚合,排序,spill 到 dfs 上。


  • 第四個角色是 Shuffle Writer,他負責把 Shuffle 資料按照 Partition 維度發到對應的 Shuffle Worker 上。


  • 第五個角色是 Shuffle Reader,它負責從分散式檔案系統上把 Shuffle 檔案讀回來。


流批一體架構在快手的實踐和思考

可以看到內部的實現和社群的 Remote shuffle service 基本一致,只有一個核心區別。內部用的是 Reduce Partition,社群釋出的版本目前是 Map Partition。


Map Partition 裡的資料由一個上游任務產生,可能會被多個下游任務消費。而 Reduce Partition 的資料由多個上游計算任務輸出,只會被一個下游併發消費。


Reduce Partition 的好處是下游消費是順序讀,避免隨機小 I/O,同時減少磁碟壓力。但是為了避免資料不可用的情況下重新拉起所有上游 map,所以一般會做多副本,但這就會增加儲存的開銷。不過因為臨時的 Shuffle 資料儲存週期並不長,所以多副本的開銷也能接受。


流批一體架構在快手的實踐和思考

第三個,離線任務穩定性差。主要有兩個原因。


  • 第一個是中低優任務頻繁失敗重啟。離線叢集開啟資源搶佔,中低優任務的資源頻繁被搶佔,導致離線任務多次重啟,一旦超過閾值,這個作業就會認為是失敗;由產品側再重新拉起。如果搶佔發生地過於頻繁,超過產品側拉起次數的上限,這個任務就需要使用者手工介入處理。我們也不能透過使用者把 failover 的閾值設定的很大來規避這個問題。因為一旦發生業務問題引發的失敗,引擎在不斷的重試,導致使用者可能要很久才能感知到故障。


  • 第二個是失敗恢復開銷大。離線叢集資源緊張,任務可能只能申請 到部分資源,已經執行的任務處理太多 Splits,一旦發生異常,恢復代價大。


流批一體架構在快手的實踐和思考

對於第一個問題,引擎需要區分資源搶佔類導致的失敗和其他業務異常導致的失敗。對資源搶佔類的異常由框架自動重試,不計入失敗重啟次數。上圖反映了內部的實現細節需要做兩個改動,第一個是 YarnResourceManager 把 container 退出碼告訴 AM,第二個是 ExecutionFailureHandler 識別每次失敗的原因,主動跳過一些指定型別的異常,不計入失敗重啟次數。


流批一體架構在快手的實踐和思考

對於第二個失敗恢復開銷大的問題,上圖左側的反映了現在的處理方式。假設一個作業需要 4 個 Task,但因為資源緊張,只申請到了 2 個 TaskManager 資源,這個時候就會先啟動兩個 task 來處理,這兩個 task 會把所有的活都幹完後,把資源讓給 task3 和 task4,後面兩個 task 一看沒什麼要乾的,就退出了。這種 splits 分配傾斜問題,會導致 failover 的代價大。


為了不讓 failover 的代價過大,我們對單個 task 最多能處理的 splits 做了限制,一旦一個 task 處理 splits 的數目達到閾值,這個 task 就主動的退出,將剩下 splits 交給後面 task 來處理。


這裡的實現上有一些細節需要注意。比如任務失敗了,會把自己以前處理的 splits 還回去,這個時候也要把記錄的 splits counter 數扣掉相應的個數,以免出現 splits 洩露的問題。


流批一體架構在快手的實踐和思考

我們在易用性上的工作可以提煉成以下三點。


  • 第一個是在開發階段,基於社群的於 Adaptive Batch Scheduler 自動推導併發度,不需要使用者再手工的設定併發度。


  • 第二個是在執行階段,主動定期上報作業的進度資訊和異常資訊,讓使用者知道作業現在執行的狀態。


  • 第三個是在事後定位階段,批作業和流作業不同,因為很多使用者都是在作業失敗或者結果不符的時候,才會關注這個作業,但這個時候作業很可能已經執行完了,所以我們完善了 history server ,在使用者結束以後,也可以讓使用者在 UI 上檢視 JM 和 TM 的日誌。


流批一體架構在快手的實踐和思考

我們在支援流批一體業務的同時,也在思考如何大規模落地 Flink 批。很多業務其實也有流批一體的需求,但還在觀望,擔心 Flink Batch 的普適性是否適合其他 pattern,是否適合其他的業務場景,而我們自己也需要一個方法來論證和事先來評估。所以我們內部把 Flink Batch 接入離線的生產引擎,讓 Flink Batch 也可以承接線上的離線作業。


流批一體架構在快手的實踐和思考

上圖展示的是快手的離線生產引擎架構,我們所有的離線 SQL 生產作業,都採用 Hive,且以 HiveServer 作為統一的入口。從圖中可以看到有一個 BeaconServer 元件,它用來承載智慧路由的功能。根據一定的規則,把 SQL 作業路由到底層的引擎上。底層目前已經對接 spark、mr 和 presto 的引擎。


流批一體架構在快手的實踐和思考

我們把 Flink 也接到了這套系統。這個系統本身很靈活,所以很方便我們定製各種路由策略。比如白名單機制可以限制只路由固定 pattern 的 SQL 作業到 Flink 上;黑名單機制,可以禁止某些 pattern 的 SQL 作業路由到 Flink 上,比如 Flink 目前暫不支援的 SQL 語法;優先順序機制,初期只路由低優的作業到 Flink 上;灰度機制可以限制路由一定比例的少量作業到 Flink 引擎上等等。這些策略讓我們可以逐步擴大 Flink 在新增和存量離線作業中的佔比。


在 Flink 接入離線生產引擎裡,我們有三項關鍵工作。


  • 第一是增強 Flink 引擎的能力,包括提高 Hive 語法相容性和 Hive connector 的能力。


  • 第二是產品接入,除了剛才提到的接入公司的 HiveServer2 和 Beacon Server,還包括接入公司的鑑權體系等等。


  • 第三個是周邊建設,包括接入雙跑平臺來驗證結果的正確性、資源開銷等。另外,還建立自動化監控和報警流程,減少運維成本,及時發現問題解決問題。


流批一體架構在快手的實踐和思考

經過第一個階段的努力,我們的離線作業數目已經達到了 3000 個,覆蓋了機器學習、資料整合、離線生產等多個業務場景。


04

第二階段(業務視角的流批一體)的挑戰


流批一體架構在快手的實踐和思考

第一個挑戰是手動指定執行模式。比如指定作業執行模式、互動方式、是否開啟推測執行等等。這些都需要使用者知道很多底層的細節,才能判斷自己需要哪些引數,以及怎麼合理的來設定這些引數。


但我們認為對使用者更友好的使用方式是不需要使用者理解甚至感知這麼多底層的細節的,所以我們的解決方案有兩個方向。一個改進方向是儘量減少開箱需要調整的引數,另外一個方向是引入智慧化互動方式。使用者只需要提供業務邏輯和對結果的消費方式,其他都交給引擎來推導。


流批一體架構在快手的實踐和思考

第二個挑戰是批處理的效能。而 Flink 批要想有更廣闊的業務場景,要在效能上有更大的突破。Flink 社群在剛釋出的 1.16 版本也已經開啟了好幾個 FLIP,希望把 Flink Batch 的效能在往前走一步。


第一個方向是在排程層的最佳化。比如動態的自適應執行,社群已經初步具備了一些自適應能力。下一步社群會規劃更靈活的自適應能力,比如在執行的時候可以根據 join 兩邊輸入的實際資料量,動態調整 join 的策略。回顧之前的 Adaptive Batch Scheduler,它只是動態調整拓撲中邊的關係,而調整 join 策略就涉及到了動態調整拓撲的組織方式。從模組上來說,這個改動甚至要打破最佳化器和排程器的邊界。


第二個點是 Optimizer 上的改動,這裡舉了一個 Runtime Filter Pruning 的例子,基於執行時的資訊進一步進行條件過濾,社群在 1.16 版本已經在這個點上做了嘗試,Dynamic Partition Pruning,並且在 TPC-DS benchmark 測試中,這個最佳化已經帶來明顯收益。不過當前的 Dynamic Partition Pruning 只能在 Source 上做執行時的分割槽裁剪。但事實上,Source 其他的列上也可以做類似的執行時過濾,而且中間任何一級 join 都可以做類似的最佳化來實現效能的提升。


第三個最佳化點是 Shuffle 上的最佳化,之前 Flink 的批作業要麼選擇 Pipeline Shuffle,要麼選擇 Blocking Shuffle,Pipeline Shuffle 的好處是上下游之間的資料不落盤,效能好。缺點是資源開銷大、容錯代價大。而 Blocking Shuffle 的優點和缺點剛好和 Pipeline Shuffle 反過來。為了結合這兩個的優勢,1.16 版本推出了 Hybrid Shuffle。


第四個最佳化點是在 Vectorized Native Engine 層面的最佳化,一些比較新的查詢引擎都會使用向量化執行和以資料為中心的程式碼生成技術。而在 Flink 裡引入這些技術需要對框架層面做非常大的改造,所以需要在正式投入前做充分的 POC 驗證,評估方案的複雜性,以及給一些可量化的效能收益。


流批一體架構在快手的實踐和思考

下面分享流批混跑的業務場景下遇到的兩個挑戰。


第一個是有狀態作業的流批混跑。比如用批模式刷完歷史資料後流模式接著增量消費,目前只支援對一些無狀態的 SQL 作業的流批混跑,有狀態的 SQL 作業目前還不支援。因為批上的運算元不和狀態互動,批作業執行以後也不會生成狀態資料。所以等批作業結束後,流作業啟動時無法知道如何初始化各個運算元的狀態。


要實現這個目標,不管流模式還是批模式,運算元都要和狀態互動。考慮到一些效能問題,批模式的運算元可以在執行完成後,把資料 dump 到狀態,避免頻繁和狀態做互動。


在流批混跑場景下,另外一個困擾是如果批作業刷出來的狀態很大,流作業啟動的時候如何快速從快照中恢復。這個問題在現在純流的業務場景也有類似的困擾,比如狀態很大,一旦作業 failover 或者重啟,從狀態恢復要花很長的時間。因為 Flink 是 local state,恢復的時候需要把所有的狀態都提前拉到本地,然後 restore 到運算元裡。這也是一個有挑戰的問題。


流批一體架構在快手的實踐和思考

流批混跑第二個挑戰是儲存的割裂。雖然業務程式碼可以用統一的計算引擎來表達,但儲存還是不一樣。流模式一般用 Kafka 訊息佇列來做 Source 和 Sink,批作業一般用 Hive 或者 DFS 檔案做 Source 和 Sink。儲存的割裂造成的結果就是把複雜性留給了使用者和平臺。使用者需要抽象出邏輯表,指定邏輯表和底層物理表對映關係,平臺層根據執行模式把邏輯表路由到底層的物理表上,引入統一的流批一體儲存已經成為必不可少的一個環節。Flink 社群在去年提出了 Flink Table Store,它是一種流批一體的儲存設計,既能支援流寫流讀,也能支援批寫批讀,在儲存上遮蔽了流和批的差異。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024923/viewspace-2935737/,如需轉載,請註明出處,否則將追究法律責任。

相關文章