螞蟻實時計算團隊的AntFlink提交攻堅之路

帶你聊技術發表於2023-01-12


背景說明

Blink提交採用程式模型(包裝flink info/run命令)進行作業執行計劃的生成和作業的提交,這個基本是大資料計算引擎jstorm/spark/flink的共識,採用該方式的優點在於:

    • 簡單:

    • 使用者只需在自己的jar包中進行邏輯處理

    • 引擎client負責以方法呼叫形式呼叫使用者main方法,然後編譯、提交
    • 乾淨

    • 程式模型使用者包用完銷燬,引擎版本包透過目錄隔離,不用考慮多版本問題。

但這也帶來了缺點,每次都得走一遍大量class 載入、校驗等jvm啟動全流程。同時,大多數作業的的執行計劃生成耗時是在20秒以內,也就是說此時瓶頸不在編譯階段,此時jvm啟動開銷就成為了瓶頸。尤其當這些操作極其高頻時,帶來的開銷不容小視。
下圖是blink作業模式下的plan生成和提交的耗時情況。
螞蟻實時計算團隊的AntFlink提交攻堅之路
螞蟻實時計算團隊的AntFlink提交攻堅之路

技術演進

JVM 程式冷啟動層面最佳化

下圖展示的是一個典型的Java應用各模組執行時間的分佈情況。從JVM啟動到應用程式開始執行需要經歷:VM載入,位元組碼檔案載入,以及JIT(just in time)編譯技術對解釋執行的位元組碼進行最佳化,生成本地執行程式碼的過程,還需加上JVM內部垃圾回收所耗費的時間。典型的Java應用載入時間通常是秒級起步,如果遇到比較大的應用初始花費幾分鐘都是正常的。
螞蟻實時計算團隊的AntFlink提交攻堅之路

CDS

CDS全稱是 Class-Data Sharing,其可以讓類可以被預處理放到一個歸檔檔案中,後續 Java 程式啟動時將該歸檔檔案對映到記憶體中,類載入器複用上一次程式啟動時曾經載入、驗證過的類資訊,以節約應用啟動的時間。換句話就是避免每次程式冷啟動。

缺點:

透過CDS方案改進提交,雖然有些效果,但是總體上差強人意,核心缺點卻不容忽視:

僅能做到作業級別class複用:

    • 原因:由於不同使用者作業很大可能依賴不同版本的包,做class快取時就會存在衝突。

    • 間接帶來的問題:

    • 每個作業需要快取大量class,對brs服務磁碟帶來巨大壓力(單個作業快取資料100MB以上)。

    • 引入CDS清理機制:由於blink作業操作並不符合近期操作原則,此時後續作業操作cds命中不了

AOT

AOT提前編譯也不是一個新鮮的概念,在Java之前、很多其他的語言已經提出了AOT。DK9 引入了AOT(Ahead of Time) 編譯技術,核心思想是在程式執行之前將class編譯成native的code,程式執行階段從原先的解釋執行變成執行native code,從而減少冷啟動問題。
螞蟻實時計算團隊的AntFlink提交攻堅之路

缺點:

1、有較多限制,同時業內並沒有大規模使用:

    • 暫不支援自定義ClassLoader
    • 不支援CMS和ZGC

2、在我們場景測試下來,效果並不好

常駐服務生成

1、常駐服務生成方案:

    • 可以在blink rest server機器上,部署額外服務,負責生成plan/jobgraph(該服務和blink rest server同屬一個程式)

2、面臨核心痛點問題:

    • 引擎版本間的相容性沒有保證,如何支援引擎多版本?

    • 方案1:多程式方案(每個版本額外啟動一個常駐程式)

    • 為了更快速地支援業務需求、快速迭代,我們團隊基本保持著半個月內發版的頻率。
    • 為了儘快fix使用者問題,也會發布臨時版本
    • 基於此,該方案不可行,否則會有大量進場。
    • 方案2:多classloader方案

    • 每個版本一個classloader,透過classloader做jar包隔離。

引擎多版本classloader方案實現思路

先做下簡要背景說明,作業包可分為下面4種情況

    • flink/lib依賴包

    • launcher包:包涵和引擎互動的包。如plan/jobgraph的生成、資源plan apply到jobgraph中、熱更新等

    • user jar使用者jar包:作業級別

    • connnector/backend外掛包

    • 引擎當前為了支援平臺包優先順序高於引擎端而設計
    • 可以當做使用者jar包來看


引入version classloader

可以簡單使用該classloader層級關係做隔離

    • 由於每個作業的user jar包不同,則version classloader沒法複用

    • version classloader用完及釋放,此時和程式模型相比也就沒有太大區別,即效能會不好。

螞蟻實時計算團隊的AntFlink提交攻堅之路

進一步引入reuqest level classloader

思路:

    • 由於version級別的classloader,很少或者不變動,可複用。

    • request級別的classloader每次用完立即釋放

    • 由於每個作業的使用者jar不同,沒法複用

launcher包的功能如何暴露給spring boot server(即blink rest server)使用呢?

    • spring boot server透過反射呼叫launcher包中的方法即可;

    • 但是遵循以下規則即可:
    • 由於該spring boot server和flink打交道透過launcher包,暴露的方法引數務必注意只能是jdk的類。
    • 假如暴露的引數使用的是開源庫的類,哪怕version classloader和spring boot的app都有該jar包,但是此時類是不同的classloader載入了,會導致LinkageError問題。

優點:

    1. version classloader和spring boot的app classloader沒有繼承關係,做到了乾淨隔離,因此該spring boot可以隨便依賴flink、甚至blink或者其他依賴,並不影響該服務;
    2. 將version classloader cache起來,複用率非常高;
    • 當同版本更新發布或者測試環境希望僅僅更新某變更jar包做驗證時,透過監聽版本包目錄jar包變更,讓classloader快取失效,重新構建即可。

螞蟻實時計算團隊的AntFlink提交攻堅之路

思考

為啥hive/spark/flink計算引擎都是透過自定義classloader方案,不採用類似上面的方案,如下圖1所示呢?

    • 自定義classloader本質上是想解決使用者jar和引擎包衝突的問題 ;
    • 但是使用者包和引擎包的互動:
    • 1) 不可能像上面方案互動是單向的
    • 上面方案:spring boot server僅單向訪問引擎launcher提供的介面;
    • 而對於計算引擎來說,user code訪問引擎程式碼,引擎程式碼依賴user code的返回值是不可避免的。
    • 2) 不可能像上面方案約束暴露的方法引數必須為jdk的類
    • 否則使用者用起來一定很不爽;
    • 基於上面兩點,計算引擎自然不可能使用下面圖1方案,而是圖2方案。

螞蟻實時計算團隊的AntFlink提交攻堅之路
圖1:flink classloader方案改造
螞蟻實時計算團隊的AntFlink提交攻堅之路
圖2:flink 自定義classloader 引擎當前方案

那麼計算引擎使用圖2的方案存在什麼問題呢?

1、LinkageError無法避免

  • 由於相互互動,同一個類被不同的classloader載入然後相互引用,細節見筆者分析的文件連結[1];
  • 雖然flink對此做了很多改進,但是該問題無法根本解決;
  • 比如引擎已經約束好哪些包是必須交給app classloader載入,防止被user classloader載入,那麼相互引用就不會有問題;
  • 但不可能放進去很多,否則不同版本三方包衝突問題不就隨之而來。所以又暴露了使用者級別配置,使用者作業執行時報LinkageError問題,使用者把對應的包路徑塞入配置即可。但如果兩個classloader比如需要且引用,則沒有辦法解;

2、ClassNotFoundExceotion報錯詭異,讓人困惑

  • 一般地,使用者外掛包該錯,很簡單,user jar打上依賴即可
  • 但是有些情況,就比較繞。
    • 先鋪墊下基礎知識, classloader類載入機制3原則:

    1. 全盤負責:所謂全盤負責,就是當一個類載入器負責載入某個Class時,該Class所依賴和引用其他Class也將由該類載入器負責載入,除非顯示使用另外一個類載入器來載入,如class.forName(, classloader)。

    2. 雙親委派:所謂的雙親委派,則是先讓父類載入器試圖載入該Class,只有在父類載入器無法載入該類時才嘗試從自己的類路徑中載入該類。通俗的講,就是某個特定的類載入器在接到載入類的請求時,首先將載入任務委託給父載入器,依次遞迴,如果父載入器可以完成類載入任務,就成功返回;只有父載入器無法完成此載入任務時,才自己去載入。

    3. 快取機制:快取機制將會保證所有載入過的Class都會被快取,當程式中需要使用某個Class時,類載入器先從快取區中搜尋該Class,只有當快取區中不存在該Class物件時,系統才會讀取該類對應的二進位制資料,並將其轉換成Class物件,存入緩衝區中。這就是為很麼修改了Class後,必須重新啟動JVM,程式所做的修改才會生效的原因。

    舉例:A, B, C三個類依賴關係如下圖,但是類B對應的jar在兩個classloader中都有。

    • 此時B在程式啟動時,已經被父classloader載入。然後呼叫user code時,呼叫了A -> B -> C。由於B已經被父classloader載入,根據全盤負責原則此時C將交給父classloader載入,而父classloader沒有該C的jar包,則報ClassNotFoundExceotion。

    • 但是使用者就很困惑,呼叫鏈明明是我的程式碼,而且我的包中已經有該class,為什麼會報這個錯呢?

    • 解決辦法:

    • 將B從父classpath去除。不可行,這樣父classloader在程式啟動前,就報ClassNotFoundExceotion了;

    • 對user code中B 做shade改包名,一般該解法可行。但是比較trick的是使用者程式碼依賴的B不是依賴形式使用,而是以hard code編碼方式。如果讓使用者改動依賴程式碼,就很麻煩。

    • 最終臨時是將該依賴打入到父classpath。但是對於引擎來說,就會有較大改動。如果是廣泛使用的包,又會很容易和其他使用者作業衝突。

螞蟻實時計算團隊的AntFlink提交攻堅之路

效果

透過多版本classloader方案最佳化後,經測試簡單作業plan耗時從10秒降低到1秒以內,有數量級級別的提升。

    • 同時,從背景說明章節的圖中可看到絕大多數作業都為簡單作業;

螞蟻實時計算團隊的AntFlink提交攻堅之路

作業提交和jobgraph生成解耦

blink/flink核心在於jobgraph,而session/perjob/application模式核心僅僅在於生成job graph的位置不同、是否支援多作業而已。具體細節見筆者之前寫的文件連結[2]。

blink 採用single job的session模式,提交作業時先拉起JobManager,然後同步方式等pod拉起之後(拉起需要申請pod比較耗時),之後在編譯作業生成jobgraph。如果發現不相容再退出JM作業,則前面耗時的工作白做了。

    • 基於此,我們實現flink支援k8s per job模式,解耦作業提交和jobgraph生成。在客戶端提前生成jobgraph,如果不相容直接報錯了,無需拉起JobManager。

    • 解耦後,可以做很多最佳化。運維態不變更作業。可以直接複用已經生成的jobgraph,無需再重複生成等。

    • 同時,為了統一程式碼棧,降低開發成本,也擴充套件datastream作業支援per job模式提交。

螞蟻實時計算團隊的AntFlink提交攻堅之路

結語

提交主要分為兩個階段client段和服務端,本文主要從客戶端最佳化出發,對於服務端提交最佳化,螞蟻實時計算團隊還做了其他大量工作,如映象加速(映象拉取的最佳化)、叢集模式(申請TaskManager資源時,不走sigma映象方式,直接起程式方式)、熱更新(對使用者作業不修改情況下,不走整個提交流程,複用k8s的flink叢集)。

參考文件連結:

[1]%20《包衝突常見解法》

[2]%20《Flink%20JobGraph核心資訊》


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

相關文章