兼容性通過率99.9%,滴滴Flink-1.10的升級之路

一、背景

 

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

 

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

 

二、Flink-1.10新特性

 

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

 

1、原生DDL語法與Catalog支持  

 

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

 

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

 

2、FlinkSQL的增強  

 

  • 基於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支持,而且與我們內部的語法差別較大。現在擺在我們面前有幾條路可以選擇:

 

  • a.放棄內部語法的支持,修改全部任務至新語法。(違背了平滑遷移的初衷,而且對已有用戶學習成本高);

  • b.修改Flink內語法解析的模塊(sql-parser),支持對內部語法的解析。(實現較爲複雜,且不利於後續的版本升級);

  • c.在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(滴滴資深軟件開發工程師) 來源丨公衆號:滴滴技術(ID:didi_tech) dbaplus社羣歡迎廣大技術人員投稿,投稿郵箱: [email protected]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章