Flink的類載入器解析

概覽

在執行 Flink 應用程式時,JVM 會隨著時間的推移載入各種類。 這些類可以根據它們的來源分為三組:

Java Classpath:這是Java的通用類路徑,它包括JDK庫,以及Flink /lib資料夾中的所有程式碼(Apache Flink的類和一些依賴)。

Flink 外掛元件:外掛程式碼在 Flink 的 /plugins 資料夾下的資料夾中。 Flink 的外掛機制會在啟動時動態載入一次。

動態使用者程式碼:這些是動態提交的作業的 JAR 檔案中包含的所有類(透過 REST、CLI、Web UI)。 它們按作業動態載入(和解除安裝)。

作為一般規則,無論何時您先啟動 Flink 程序然後再提交作業,作業的類都會動態載入。 如果 Flink 程序與作業/應用程式一起啟動,或者如果應用程式產生 Flink 元件(JobManager、TaskManager 等),那麼所有作業的類都在 Java 類路徑中。

外掛元件中的程式碼由每個外掛的專用類載入器動態載入一次。

以下是有關不同部署模式的更多詳細資訊:

Standalone Session

當作為獨立會話啟動 Flink 叢集時,JobManagers 和 TaskManagers 使用 Java 類路徑中的 Flink 框架類啟動。 針對會話(透過 REST / CLI)提交的所有作業/應用程式中的類都是動態載入的。

Docker / Kubernetes Sessions

Docker / Kubernetes 設定首先啟動一組 JobManagers / TaskManagers,然後透過 REST 或 CLI 提交作業/應用程式,其行為類似於獨立會話:Flink 的程式碼位於 Java 類路徑中,外掛元件和作業程式碼在啟動時動態載入。

YARN

YARN 類載入在單個作業部署和會話之間有所不同:

當直接向 YARN 提交 Flink 作業/應用程式時(透過 bin/flink run -m yarn-cluster 。。。),將為該作業啟動專用的 TaskManager 和 JobManager。 這些 JVM 在 Java 類路徑中具有使用者程式碼類。 這意味著在這種情況下,作業不涉及動態類載入。

當啟動一個 YARN 會話時,JobManagers 和 TaskManagers 是用 classpath 中的 Flink 框架類啟動的。 針對會話提交的所有作業的類都是動態載入的。

反向類載入和類載入器解析順序

在涉及動態類載入的設定中(外掛元件、會話設定中的 Flink 作業),通常有兩個類載入器的層次結構:(1)Java 的應用程式類載入器,它包含類路徑中的所有類,以及(2)動態外掛/ 使用者程式碼類載入器。 用於從外掛或使用者程式碼 jar 載入類。 動態 ClassLoader 將應用程式類載入器作為其父級。

預設情況下,Flink 反轉類載入順序,這意味著它首先檢視動態類載入器,如果類不是動態載入程式碼的一部分,則僅檢視父類(應用程式類載入器)。

反向類載入的好處是外掛和作業可以使用與 Flink 核心本身不同的庫版本,這在不同版本的庫不相容時非常有用。 該機制有助於避免常見的依賴衝突錯誤,如 IllegalAccessError 或 NoSuchMethodError。 程式碼的不同部分只是具有單獨的類副本(Flink 的核心或其依賴項之一可以使用與使用者程式碼或外掛程式碼不同的副本)。 在大多數情況下,這執行良好,不需要使用者進行額外配置。

但是,在某些情況下,反向類載入會導致問題(請參閱下文,“X cannot be cast to X”)。 對於使用者程式碼類載入,您可以透過在 Flink 配置中透過 classloader。resolve-order 將 ClassLoader 解析順序配置為 parent-first(從 Flink 的預設 child-first)來恢復到 Java 的預設模式。

請注意,某些類總是以父級優先的方式解析(首先透過父類載入器),因為它們在 Flink 的核心和外掛/使用者程式碼或面向外掛/使用者程式碼的 API 之間共享。 這些類的包是透過 classloader。parent-first-patterns-default 和 classloader。parent-first-patterns-additional 配置的。 要新增父級優先載入的新包,請設定 classloader。parent-first-patterns-additional 配置選項。

避免使用者程式碼的動態類載入

所有元件(JobManger、TaskManager、Client、ApplicationMaster 等)在啟動時記錄它們的類路徑設定。 它們可以作為日誌開頭的環境資訊的一部分找到。

當執行 JobManager 和 TaskManagers 專用於一項特定作業的設定時,可以將使用者程式碼 JAR 檔案直接放入 /lib 資料夾中,以確保它們是類路徑的一部分而不是動態載入。

通常將作業的 JAR 檔案放入 /lib 目錄中。 JAR 將成為類路徑(AppClassLoader)和動態類載入器(FlinkUserCodeClassLoader)的一部分。 因為 AppClassLoader 是 FlinkUserCodeClassLoader 的父級(並且 Java 載入父級,預設情況下),這應該導致類只加載一次。

對於無法將作業的 JAR 檔案放入 /lib 資料夾的設定(例如因為安裝程式是由多個作業使用的會話),仍然可以將公共庫放入 /lib 資料夾,並避免動態為那些類進行載入。

使用者程式碼中的手動類載入

在某些情況下,轉換函式、源或接收器需要手動載入類(透過反射動態載入)。 為此,它需要能夠訪問作業類的類載入器。

在這種情況下,函式(或源或接收器)可以成為 RichFunction(例如 RichMapFunction 或 RichWindowFunction)並透過 getRuntimeContext()。getUserCodeClassLoader() 訪問使用者程式碼類載入器。

X cannot be cast to X exceptions

在使用動態類載入的設定中,您可能會看到 com。foo。X cannot be cast to com。foo。X 樣式中的異常。 這意味著 com。foo。X 類的多個版本已被不同的類載入器載入,並且該類的型別試圖相互分配。

一個常見的原因是庫與 Flink 的反向類載入方法不相容。 您可以關閉反向類載入來驗證這一點(在 Flink 配置中設定 classloader。resolve-order: parent-first)或從反向類載入中排除庫(在 Flink 配置中設定 classloader。parent-first-patterns-additional)。

另一個原因可能是快取物件例項,如 Apache Avro 之類的某些庫或透過註冊(例如透過 Guava 的 Interners)生成的物件例項。 這裡的解決方案是要麼在沒有任何動態類載入的情況下進行設定,要麼確保相應的庫完全是動態載入程式碼的一部分。 後者意味著該庫不能被新增到 Flink 的 /lib 資料夾中,而必須是應用程式的 fat-jar/uber-jar 的一部分

解除安裝使用者程式碼中動態載入的類

所有涉及動態使用者程式碼類載入(會話)的場景都依賴於再次解除安裝類。 類解除安裝意味著垃圾收集器發現類中不存在任何物件,因此刪除該類(程式碼、靜態變數、元資料等)。

每當 TaskManager 啟動(或重新啟動)一個任務時,它將載入該特定任務的程式碼。 除非可以解除安裝類,否則這將成為記憶體洩漏,因為載入了新版本的類,並且載入的類總數會隨著時間的推移而累積。 這通常透過 OutOfMemoryError: Metaspace 表現出來。

類洩漏的常見原因和建議的修復:

延遲執行緒:確保應用程式功能/源/接收器關閉所有執行緒。 延遲執行緒本身會消耗資源,並且通常還會持有對(使用者程式碼)物件的引用,從而防止垃圾收集和類解除安裝。

內部的:避免在超過函式/源/接收器生命週期的特殊結構中快取物件。 示例是 Guava 的 interners,或序列化程式中 Avro 的類/物件快取。

JDBC:JDBC 驅動程式在使用者程式碼類載入器之外洩漏引用。 為了確保這些類只加載一次,您應該將驅動程式 jar 新增到 Flink 的 lib/ 資料夾中,或者透過 classloader。parent-first-patterns-additional 將驅動程式類新增到父級優先載入的類列表中。

解除安裝動態載入類的一個有用工具是使用者程式碼類載入器釋放鉤子。 這些是在解除安裝類載入器之前執行的鉤子。 通常建議關閉和解除安裝資源作為常規函式生命週期的一部分(通常是 close() 方法)。 但在某些情況下(例如對於靜態欄位),最好在不再需要類載入器時解除安裝。

類載入器釋放鉤子可以透過 RuntimeContext。registerUserCodeClassLoaderReleaseHookIfAbsent() 方法註冊。

使用 maven-shade-plugin 解決與 Flink 的依賴衝突

從應用程式開發人員的角度解決依賴衝突的一種方法是透過隱藏它們來避免暴露依賴關係。

Apache Maven 提供了 maven-shade-plugin,它允許在編譯後更改類的包(因此您編寫的程式碼不受陰影影響)。 例如,如果您的使用者程式碼 jar 中有來自 aws sdk 的 com。amazonaws 包,則 shade 外掛會將它們重新定位到 org。myorg。shaded。com。amazonaws 包中,以便您的程式碼呼叫您的 aws sdk 版本。

注意 Flink 的大部分依賴,比如 guava、netty、jackson 等,都被 Flink 的維護者遮蔽掉了,所以使用者通常不用擔心。