滴滴 Flink-1.10 升級之路 一、 背景 二、 Flink-1.10 新特性 三、挑戰與應對 四、引擎增強 五、總結

導讀:滴滴實時計算引擎從 Flink-1.4 無縫升級到 Flink-1.10 版本,做到了完全對用戶透明。並且在新版本的指標、調度、SQL 引擎等進行了一些優化,在性能和易用性上相較舊版本都有很大提升。

這篇文章介紹了我們升級過程中遇到的困難和思考,希望能給大家帶來啓發。

一、 背景

在本次升級之前,我們使用的主要版本爲 Flink-1.4.2,並且在社區版本上進行了一些增強,提供了 StreamSQL 和低階 API 兩種服務形式。現有集羣規模達到了 1500 臺物理機,運行任務數超過 12000 ,日均處理數據 3 萬億條左右。

不過隨着社區的發展,尤其是 Blink 合入 master 後有很多功能和架構上的升級,我們希望能通過版本升級提供更好的流計算服務。今年 2 月份,里程碑版本 Flink-1.10 發佈,我們開始在新版上上進行開發工作,踏上了充滿挑戰的升級之路。

二、 Flink-1.10 新特性

作爲 Flink 社區至今爲止的最大的一次版本升級,加入的新特性解決了之前遇到很多的痛點。

1. 原生 DDL 語法與 Catalog 支持

Flink SQL 原生支持了 DDL 語法,比如 CREATE TABLE/CREATE FUNCTION,可以使用 SQL 進行元數據的註冊,而不需要使用代碼的方式。

也提供了 Catalog 的支持,默認使用 InMemoryCatalog 將信息臨時保存在內存中,同時也提供了 HiveCatalog 可以與 HiveMetastore 進行集成。也可以通過自己拓展 Catalog 接口實現自定義的元數據管理。

2.Flink SQL 的增強

  • 基於 ROW_NUMBER 實現的 TopN 和去重語法,拓展了 StreamSQL 的使用場景。
  • 實現了 BinaryRow 類型作爲內部數據交互,將數據直接以二進制的方式構建而不是對象數組,比如使用一條數據中的某個字段時,可以只反序列其中部分數據,減少了不必要的序列化開銷。
  • 新增了大量內置函數,例如字符串處理、FIRST/LAST_VALUE 等等,由於不需要轉換爲外部類型,相較於自定義函數效率更高。
  • 增加了 MiniBatch 優化,通過微批的處理方式提升任務的吞吐

3.內存配置優化

之前對 Flink 內存的管理一直是一個比較頭疼的問題,尤其是在使用 RocksDB 時,因爲一個 TaskManager 中可能存在多個 RocksDB 實例,不好估算內存使用量,就導致經常發生內存超過限制被殺。

在新版上增加了一些內存配置,例如 state.backend.rocksdb.memory.fixed-per-slot 可以輕鬆限制每個 slot的RocksDB 內存的使用上限,避免了 OOM 的風險。

三、挑戰與應對

本次升級最大的挑戰是,如何保證 StreamSQL 的兼容性。StreamSQL 的目的就是爲了對用戶屏蔽底層細節,能夠更加專注業務邏輯,而我們可以通過版本升級甚至更換引擎來提供更好的服務。保證任務的平滑升級是最基本的要求。

1. 內部 patch 如何兼容

由於跨越多個版本架構差距巨大,內部 patch 基本無法直接合入,需要在新版本上重新實現。我們首先整理了所有的歷史 commit,篩選出那些必要的修改並且在新版上進行重新實現,目的是能覆蓋已有的所有功能,確保新版本能支持現有的所有任務需求。

例如:

  • 新增或修改 Connectors 以支持公司內部需要,例如 DDMQ(滴滴開源消息隊列產品),權限認證功能等。
  • 新增 Formats 實現,例如 binlog,內部日誌採集格式的解析等。
  • 增加 ADD JAR 語法,可以在 SQL 任務中引用外部依賴,比如 UDF JAR,自定義 Source/Sink。
  • 增加 SET 語法,可以在 SQL 中設置 TableConfig,指導執行計劃的生成

2. StreamSQL 語法兼容

社區在 1.4 版本時,FlinkSQL還處於比較初始的階段,也沒有原生的 DDL 語法支持,我們使用 Antlr 實現了一套自定義的 DDL 語法。但是在 Flink1.10 版本上,社區已經提供了原生的 DDL 支持,而且與我們內部的語法差別較大。現在擺在我們面前有幾條路可以選擇:

  • 放棄內部語法的支持,修改全部任務至新語法。(違背了平滑遷移的初衷,而且對已有用戶學習成本高)
  • 修改 Flink 內語法解析的模塊(sql-parser),支持對內部語法的解析。(實現較爲複雜,且不利於後續的版本升級)
  • 在 sql-parser 之上封裝一層語法轉換層,將原本的 SQL 解析提取有效信息後,通過字符串拼接的方式組織成社區語法再運行。

最終我們選用了第三種方案,這樣可以最大限度的減少和引擎的耦合,作爲插件運行,未來再有引擎升級完全可以複用現有的邏輯,能夠降低很多的開發成本。

例如:我們在舊版本上使用 "json-path" 的庫實現了 json 解析,通過在建表語句裏定義類似 $.status 的表達式表示如何提取此字段。

新版本上原生的 json 類型解析可以使用 ROW 類型來表示嵌套結構,在轉換爲新語法的過程中,將原本的表達是解析爲樹並構建出新的字段類型,再使用計算列的方式提取出原始表中的字段,確保表結構與之前一致。類型名稱、配置屬性也通過映射轉換爲社區語法。

3. 兼容性測試

最後是測試階段,需要進行完善的測試確保所有任務都能做到平滑升級。我們原本的計劃是準備進行迴歸測試,對已有的所有任務替換配置後進行回放,但是在實際操作中有很多問題:

  • 測試流程過長,一次運行可能需要數個小時。
  • 出現問題時不好定位,可能發生在任務的整個生命週期的任何階段。
  • 無法驗證計算結果,即新舊版本語義是否一致

所以我們按任務的提交流程分成多個階段進行測試,只有在當前階段能夠全部測試通過後後進入下一個階段測試,提前發現問題,將問題定位範圍縮小到當前階段,提高測試效率。

  • 轉換測試:對所有任務進行轉換,測試結果符合預期,抽象典型場景爲單元測試。
  • 編譯測試:確保所有任務可以通過 TablePlanner 生成執行計劃,再編譯成 JobGraph,真正提交運行前結束。
  • 迴歸測試:在測試環境對任務替換配置後進行回放,確認任務可以提交運行
  • 對照測試:對採樣數據以文件的形式提交至新舊兩個版本中運行,對比結果是否完全一致(因爲部分任務結果不具有確定性,所以使用舊版本連續運行 2 次,篩選出確定性任務,作爲測試用例)

四、引擎增強

除了對舊版本的兼容,我們也結合了新版本的特性,對引擎進行了增強。

1. Task-Load 指標

我們一直希望能精確衡量任務的負載狀況,使用反壓指標指標只能粗略的判斷任務的資源夠或者不夠。

結合新版的 Mailbox 線程模型,所有互斥操作全部運行在 TaskThread 中,只需統計出線程的佔用時間,就可以精確計算任務負載的百分比。

未來可以使用指標進行任務的資源推薦,讓任務負載維持在一個比較健康的水平。

2. SubTask 均衡調度

在 FLIP-6 後,Flink 修改了資源調度模型,移除了--container 參數,slot 按需申請確保不會有閒置資源。但是這也導致了一個問題,Source 的併發數常常是小於最大併發數的,而 SubTask 調度是按 DAG 的拓撲順序調度,這樣 SourceTask 就會集中在某些 TaskManager 中導致熱點。

我們加入了"最小 slot 數"的配置,保證在 Flink session 啓動後立即申請相應數量的 slot,且閒置時也不主動退出,搭配 cluster.evenly-spread-out-slots 參數可以保證在 slot 數充足的情況下,SubTask 會均勻分佈在所有的 TaskManager 上。

3. 窗口函數增強

以滾動窗口爲例 TUMBLE(time_attr, INTERVAL '1' DAY),窗口爲一天時開始和結束時間固定爲每天 0 點 -24 點,無法做到生產每天 12 點-次日 12 點的窗口。

對於代碼可以通過指定偏移量實現,但是 SQL 目前還未實現,通過增加參數 TUMBLE(time_attr, INTERVAL '1' DAY, TIME '12:00:00') 表示偏移時間爲 12 小時。

還有另外一種場景,比如統計一天的 UV,同時希望展示當前時刻的計算結果,例如每分鐘觸發窗口計算。對於代碼開發的方式可以通過自定義 Trigger 的方式決定窗口的觸發邏輯,而且 Flink 也內置了一些 Tigger 實現,比如 ContinuousTimeTrigger 就很適合這種場景。所以我們又在窗口函數裏增加了一種可選參數,代表窗口的觸發週期,TUMBLE(time_attr, INTERVAL '1' DAY, INTERVAL '1' MINUTES) 。

通過增加 offset 和 tiggger 週期參數(TUMBLE(time_attr, size[,offset_time][,trigger_interval])),拓展了 SQL 中窗口的使用場景,類似上面的場景可以直接使用 SQL 開發而不需要使用代碼的方式。

4. RexCall 結果複用

在很多 SQL 的使用場景裏,會多次使用上一個計算結果,比如將 JSON 解析成 Map 並提取多個字段 。

雖然通過子查詢,看起來 json 解析只調用一次,但是經過引擎的優化後,通過結果表的投影 (Projection) 生成函數調用鏈 (RexCall),結果類似:

這樣會導致 json 解析的計算重複運行了3次,即使使用視圖分割成兩步操作,經過 Planner 的優化一樣會變成上邊的樣子。

對於確定性 (isDeterministic=true) 的函數來說,相同的輸入一定代表相同的結果,重複執行 3 次 json 解析其實是沒有意義的,如何優化才能實現對函數結果的複用呢?

在代碼生成時,將 RexCall 生成的唯一標識(Digest)和變量符號的映射保存在 CodeGenContext 中,如果遇到 Digest 相同的函數調用,則可以複用已經存在的結果變量,這樣解析 JSON 只需要執行第一次,之後就可以複用第一次的結果。

五、總結

通過幾個月的努力,新版本已經上線運行,並且作爲 StreamSQL 的默認引擎,任務重啓後直接使用新版本運行。兼容性測試的通過率達到 99.9%,可以基本做到對用戶的透明升級。對於新接觸 StreamSQL 用戶可以使用社區 SQL 語法進行開發,已有任務也可以修改 DML 部分語句來使用新特性。現在新版本已經支持了公司內許多業務場景,例如公司實時數據倉庫團隊依託於新版本更強的表達能力和性能,承接了多種多樣的數據需求做到穩定運行且與離線口徑保持一致。

版本升級不是我們的終點,隨着實時計算的發展,公司內也有越來越多的團隊需要使用 Flink 引擎, 也向我們提出了更多的挑戰,例如與 Hive 的整合做到將結果直接寫入 Hive 或直接使用 Flink 作爲批處理引擎,這些也是我們探索和發展的方向,通過不斷的迭代向用戶提供更加簡單好用的流計算服務。

作者|Alan

原文鏈接

本文爲阿里雲原創內容,未經允許不得轉載。

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