Flink 自定義類加載器和子優先類加載策略

子類優先加載

Flink 默認采用了子優先(Child-First)的類加載策略來加載用戶代碼,以解決潛在的依賴沖突問題。

我們可以通過源碼來證明這一點。

ChildFirstClassLoader?的實現

Flink 中負責實現“子優先”加載邏輯的核心類是?ChildFirstClassLoader。其關鍵的?loadClassWithoutExceptionHandling?方法定義了類加載的順序。

// ... existing code ...
public final class ChildFirstClassLoader extends FlinkUserCodeClassLoader {/*** The classes that should always go through the parent ClassLoader. This is relevant for Flink* classes, for example, to avoid loading Flink classes that cross the user-code/system-code* barrier in the user-code ClassLoader.*/private final String[] alwaysParentFirstPatterns;public ChildFirstClassLoader(URL[] urls,ClassLoader parent,String[] alwaysParentFirstPatterns,Consumer<Throwable> classLoadingExceptionHandler) {super(urls, parent, classLoadingExceptionHandler);this.alwaysParentFirstPatterns = alwaysParentFirstPatterns;}@Overrideprotected Class<?> loadClassWithoutExceptionHandling(String name, boolean resolve)throws ClassNotFoundException {// First, check if the class has already been loadedClass<?> c = findLoadedClass(name);if (c == null) {// 1. 檢查是否應該“父優先”加載for (String alwaysParentFirstPattern : alwaysParentFirstPatterns) {if (name.startsWith(alwaysParentFirstPattern)) {return super.loadClassWithoutExceptionHandling(name, resolve);}}try {// 2. 在子加載器(用戶JAR包)中查找類c = findClass(name);} catch (ClassNotFoundException e) {// 3. 如果找不到,再委托給父加載器c = super.loadClassWithoutExceptionHandling(name, resolve);}} else if (resolve) {resolveClass(c);}return c;}
// ... existing code ...

從上面的代碼中我們可以清晰地看到:

  1. 檢查父優先例外:首先,代碼會檢查要加載的類是否匹配?alwaysParentFirstPatterns?列表中的模式(例如?java.org.apache.flink.?等)。如果匹配,則直接委托給父加載器,確保 Flink 核心類和 JDK 類不會被用戶的版本覆蓋。
  2. 子加載器優先:如果不屬于父優先的例外情況,它會先嘗試調用?findClass(name),在自己的 URL 路徑(即用戶的 JAR 包)中查找類。這正是“Child-First”的核心體現。
  3. 回退到父加載器:只有當?findClass(name)?拋出?ClassNotFoundException,即在用戶 JAR 包中找不到該類時,它才會調用?super.loadClassWithoutExceptionHandling(name, resolve),將加載任務委托給父加載器。

類加載器的創建

FlinkUserCodeClassLoaders?這個工具類是創建用戶代碼類加載器的工廠。它根據配置來決定是創建?ChildFirstClassLoader?還是?ParentFirstClassLoader

// ... existing code ...public static MutableURLClassLoader create(ResolveOrder resolveOrder,URL[] urls,ClassLoader parent,String[] alwaysParentFirstPatterns,Consumer<Throwable> classLoadingExceptionHandler,boolean checkClassLoaderLeak) {switch (resolveOrder) {case CHILD_FIRST:return childFirst(urls,parent,alwaysParentFirstPatterns,classLoadingExceptionHandler,checkClassLoaderLeak);case PARENT_FIRST:return parentFirst(urls, parent, classLoadingExceptionHandler, checkClassLoaderLeak);default:throw new IllegalArgumentException("Unknown class resolution order: " + resolveOrder);}}/** Class resolution order for Flink URL {@link ClassLoader}. */public enum ResolveOrder {CHILD_FIRST,PARENT_FIRST;public static ResolveOrder fromString(String resolveOrder) {if (resolveOrder.equalsIgnoreCase("parent-first")) {return PARENT_FIRST;} else if (resolveOrder.equalsIgnoreCase("child-first")) {return CHILD_FIRST;} else {throw new IllegalArgumentException("Unknown resolve order: " + resolveOrder);}}}
// ... existing code ...

Flink 的配置項?classloader.resolve-order?的默認值是?child-first,因此在大多數情況下,Flink 會創建并使用?ChildFirstClassLoader?來加載用戶代碼。

總結

源碼清楚地表明,Flink 通過?ChildFirstClassLoader?實現了子優先的類加載機制:優先在用戶 JAR 包中查找類,找不到時才委托給父加載器,同時通過?alwaysParentFirstPatterns?保證了 Flink 自身和核心依賴的穩定性。

MutableURLClassLoader

MutableURLClassLoader?被創建出來主要是為了解決標準?java.net.URLClassLoader?的一些限制,并為 Flink 的類加載機制提供更強的靈活性。它主要擴展了以下兩個核心能力:

在 Java 原生的?URLClassLoader?中,addURL(URL url)?方法是?protected?的,這意味著只有其子類或者同一個包下的類才能調用它。這在使用上造成了不便,因為我們無法從外部動態地向一個已存在的?URLClassLoader?實例中添加新的類路徑(比如新的 JAR 包)。

MutableURLClassLoader?通過重寫?addURL?方法并將其訪問權限修改為?public,解決了這個問題。

// ... existing code ...
@Internal
public abstract class MutableURLClassLoader extends URLClassLoader {// ... existing code ...public MutableURLClassLoader(URL[] urls, ClassLoader parent) {super(urls, parent);}@Overridepublic void addURL(URL url) {super.addURL(url);}/**
// ... existing code ...*/public abstract MutableURLClassLoader copy();
}

這樣做之后,Flink 框架的任何部分都可以在運行時向這個 ClassLoader 實例中動態添加 JAR 文件的 URL,從而加載新的類。這對于需要動態加載用戶代碼和依賴的場景(例如 SQL Client 提交一個帶 UDF 的 JAR 包)至關重要。

例如,在?flink-table-runtime?模塊的?SqlDriver?中,就利用了這個特性來動態加載執行器相關的 JAR:

// ... existing code ...private static ClassLoader getClassLoader() throws Exception {MutableURLClassLoader sqlGatewayClassloader =(MutableURLClassLoader) Thread.currentThread().getContextClassLoader();try {sqlGatewayClassloader.loadClass(RUNNER_CLASS_NAME);LOG.info("Load {} from the classpath.", RUNNER_CLASS_NAME);} catch (ClassNotFoundException e) {LOG.info("{} is not in the classpath. Finding...", RUNNER_CLASS_NAME);sqlGatewayClassloader.addURL(findExecutor().toUri().toURL());}return sqlGatewayClassloader;}
// ... existing code ...

定義?copy?抽象方法

MutableURLClassLoader?還定義了一個抽象方法?copy()

// ... existing code .../*** Copy the classloader for each job and these jobs can add their jar files to the classloader* independently.** @return the copied classloader*/public abstract MutableURLClassLoader copy();
}

該方法的 Javadoc 注釋解釋了其用途:“為每個作業復制類加載器,這些作業可以獨立地將其 JAR 文件添加到類加載器中。”

在 Flink 中,不同的作業(Job)可能需要不同的依賴庫。為了實現作業間的類路徑隔離,避免依賴沖突,一個常見的做法是為每個作業創建一個獨立的 ClassLoader。copy()?方法就是這個機制的體現,它強制所有子類實現一個復制自身的能力,從而可以方便地為新作業創建一個擁有相同初始類路徑但又相互獨立的 ClassLoader 實例。

例如,ChildFirstClassLoader?就實現了這個?copy?方法:

// ... existing code ...@Overridepublic MutableURLClassLoader copy() {return new ChildFirstClassLoader(getURLs(), getParent(), alwaysParentFirstPatterns, classLoadingExceptionHandler);}
}

總結

總而言之,MutableURLClassLoader?是 Flink 自定義類加載體系的一個重要基類。它通過公開?addURL?方法提供了在運行時動態修改類路徑的靈活性,并通過強制實現?copy?方法來支持為不同作業創建隔離的類加載環境,從而更好地滿足了 Flink 作為分布式計算框架對類加載的復雜需求。

FlinkUserCodeClassLoader

FlinkUserCodeClassLoader?是 Flink 中用于加載用戶代碼(例如,作業的 JAR 包、UDF 等)的核心抽象基類。它繼承自我們之前討論過的?MutableURLClassLoader,因此它具備了動態添加 URL 和被復制的能力。在此基礎上,它增加了更精細的控制和擴展能力。

FlinkUserCodeClassLoader?最核心的增強是引入了一套可定制的異常處理機制

  • 它定義了一個?Consumer<Throwable> classLoadingExceptionHandler?字段。
  • 在構造函數中,可以傳入一個具體的異常處理器。如果未提供,則使用默認的?NOOP_EXCEPTION_HANDLER(即什么也不做)。

這個機制是通過重寫?loadClass?方法實現的,這也是這個類的設計精髓所在:

// ... existing code ...@Overridepublic final Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {try {synchronized (getClassLoadingLock(name)) {return loadClassWithoutExceptionHandling(name, resolve);}} catch (Throwable classLoadingException) {classLoadingExceptionHandler.accept(classLoadingException);throw classLoadingException;}}/*** Same as {@link #loadClass(String, boolean)} but without exception handling.** <p>Extending concrete class loaders should implement this instead of {@link* #loadClass(String, boolean)}.*/protected Class<?> loadClassWithoutExceptionHandling(String name, boolean resolve)throws ClassNotFoundException {return super.loadClass(name, resolve);}
// ... existing code ...

可以看到:

  1. loadClass?方法被聲明為?final,這意味著子類不能重寫它。這保證了所有 Flink 用戶代碼類加載器的行為一致性。
  2. 它內部使用?try-catch?塊包裹了實際的類加載邏輯?loadClassWithoutExceptionHandling
  3. 當捕獲到任何?Throwable(包括?ClassNotFoundException?等)時,它會首先調用?classLoadingExceptionHandler?來處理這個異常,然后再將異常拋出。

這提供了一個強大的鉤子(Hook),使得 Flink 可以在類加載失敗時執行一些自定義邏輯,例如記錄詳細日志、發送告警等,而無需修改每個具體的類加載器實現。

設計模式:模板方法(Template Method)

FlinkUserCodeClassLoader?完美地運用了模板方法設計模式

  • 模板方法:?public final Class<?> loadClass(...)?定義了類加載的整體骨架和算法流程(加鎖 -> 加載 -> 異常處理)。這個流程是固定不變的。
  • 抽象/鉤子方法:?protected Class<?> loadClassWithoutExceptionHandling(...)?是留給子類去實現的具體步驟。子類通過重寫這個方法來定義自己獨特的加載策略(例如,是父類優先還是子類優先)。

例如,ChildFirstClassLoader?會重寫?loadClassWithoutExceptionHandling?來實現先從自己的 URL 中查找類,找不到再去父加載器中查找的邏輯。而?ParentFirstClassLoader?則可以直接使用默認實現,即調用?super.loadClass,這本身就是父類優先的邏輯。

這種設計將通用的、不變的邏輯(如線程同步、異常處理)與易變的、具體的邏輯(加載順序)解耦,使得整個類加載體系更加清晰和易于擴展。

在 Flink 生態中的角色

  • 用戶代碼隔離的基石:正如其名,它是加載所有用戶代碼的專用 ClassLoader。Flink 通過為不同的作業或任務創建不同的?FlinkUserCodeClassLoader?實例,實現了依賴隔離。
  • 加載策略的統一入口:無論是?parent-first?還是?child-first?策略,都是通過?FlinkUserCodeClassLoaders?這個工廠類創建出不同的?FlinkUserCodeClassLoader?子類實例來實現的。
  • 安全網機制:如?SafetyNetWrapperClassLoader?所示,它可以包裹一個?FlinkUserCodeClassLoader?實例,以防止 ClassLoader 泄漏。這也體現了其作為 Flink 類加載核心組件的地位。

總結

FlinkUserCodeClassLoader?在?MutableURLClassLoader?的基礎上,通過模板方法模式增加了兩個關鍵特性:

  1. 統一的、可定制的異常處理機制,增強了系統的可觀測性和健壯性。
  2. 規范化的類加載流程,強制所有子類遵循固定的加載骨架(如線程安全),同時又允許它們靈活定義具體的加載順序。

它是 Flink 實現復雜、健壯且靈活的類加載機制的核心基類。

ChildFirstClassLoader

ChildFirstClassLoader?是 Flink 中實現**子優先(Child-First)**類加載策略的具體實現,也是 Flink 默認的用戶代碼加載器。它繼承自?FlinkUserCodeClassLoader

"子優先"(也稱為“反向類加載”)意味著在加載一個類時,會優先在子加載器(即用戶代碼的 JAR 包)中進行搜索,只有在找不到的情況下,才會委托給父加載器(通常是 Flink 的應用類加載器)進行搜索。

這種機制是 Flink 實現用戶代碼與 Flink 框架本身依賴隔離的關鍵。它允許用戶在自己的作業 JAR 包中打包任意版本的依賴庫,即使這些庫與 Flink 內部使用的版本沖突,也能正常運行。例如,用戶作業可以使用 Guava 29.0,而 Flink 框架可能使用 Guava 18.0,兩者不會互相干擾。

下面我們結合代碼,分模塊來詳細解析?ChildFirstClassLoader?的實現。

// ... existing code ...
public final class ChildFirstClassLoader extends FlinkUserCodeClassLoader {/*** The classes that should always go through the parent ClassLoader. This is relevant for Flink* classes, for example, to avoid loading Flink classes that cross the user-code/system-code* barrier in the user-code ClassLoader.*/private final String[] alwaysParentFirstPatterns;public ChildFirstClassLoader(URL[] urls,ClassLoader parent,String[] alwaysParentFirstPatterns,Consumer<Throwable> classLoadingExceptionHandler) {super(urls, parent, classLoadingExceptionHandler);this.alwaysParentFirstPatterns = alwaysParentFirstPatterns;}
// ... existing code ...
  • alwaysParentFirstPatterns: 這是?ChildFirstClassLoader?的一個核心屬性。它是一個字符串數組,定義了一組類名前綴模式。凡是匹配這些模式的類,都不會遵循“子優先”的策略,而是會被強制**總是使用父優先(Parent-First)**的策略加載。
  • 為什么需要這個例外??因為有些類是 Flink 框架和用戶代碼之間交互的“接口”或“橋梁”。如果這些接口類被用戶代碼的 ClassLoader 和 Flink 框架的 ClassLoader 分別加載一次,就會產生兩個完全不同(盡管類名相同)的?Class?對象。這會導致?ClassCastException?(類型轉換異常,例如 "X cannot be cast to X")。因此,必須確保這些核心接口類在整個 JVM 中只被加載一次,并且是由父加載器加載。典型的例子包括 Flink 的核心 API (org.apache.flink.*)、Java 的核心庫 (java.*,?javax.*) 等。這些模式可以通過 Flink 的配置文件?flink-conf.yaml?中的?classloader.parent-first-patterns?來配置。

核心邏輯:loadClassWithoutExceptionHandling

這是實現“子優先”加載策略的核心所在。它重寫了父類?FlinkUserCodeClassLoader?的模板方法。

// ... existing code ...@Overrideprotected Class<?> loadClassWithoutExceptionHandling(String name, boolean resolve)throws ClassNotFoundException {// 步驟 1: 檢查類是否已經被加載過Class<?> c = findLoadedClass(name);if (c == null) {// 步驟 2: 檢查是否匹配“父優先”模式for (String alwaysParentFirstPattern : alwaysParentFirstPatterns) {if (name.startsWith(alwaysParentFirstPattern)) {// 如果匹配,直接委托給父類加載(即標準的父優先邏輯)return super.loadClassWithoutExceptionHandling(name, resolve);}}try {// 步驟 3: 嘗試在子加載器(自己的URL)中查找類c = findClass(name);} catch (ClassNotFoundException e) {// 步驟 4: 如果在子加載器中找不到,再委托給父類加載c = super.loadClassWithoutExceptionHandling(name, resolve);}} else if (resolve) {resolveClass(c);}return c;}
// ... existing code ...

加載流程可以分解為以下幾個步驟:

  1. 檢查緩存: 首先調用?findLoadedClass(name),檢查這個類是否已經被當前的 ClassLoader 加載過了。這是 JVM 類加載的標準第一步,避免重復加載。
  2. 檢查“父優先”例外: 如果類尚未加載,則遍歷?alwaysParentFirstPatterns?列表。如果類名?name?匹配了其中任何一個模式,就立即中斷“子優先”邏輯,直接調用?super.loadClassWithoutExceptionHandling(name, resolve)。這會觸發標準的雙親委派模型,即從父加載器開始向上查找。
  3. 子加載器查找: 如果類名不匹配任何“父優先”模式,則執行“子優先”的核心邏輯:調用?findClass(name)。這個方法會只在當前 ClassLoader 的 URL 列表(即用戶 JAR 包)中查找類。
  4. 委托父加載器: 如果?findClass(name)?拋出?ClassNotFoundException(意味著在用戶 JAR 包中沒找到這個類),則在?catch?塊中,最后再調用?super.loadClassWithoutExceptionHandling(name, resolve),將加載任務委托給父加載器。

這個邏輯清晰地實現了“子優先,但有例外”的加載策略。

資源加載:getResource?和?getResources

加載資源(如配置文件)的邏輯與加載類相似,也需要遵循“子優先”的原則。

getResource(String name)

// ... existing code ...@Overridepublic URL getResource(String name) {// 優先在子加載器(自己的URL)中查找資源URL urlClassLoaderResource = findResource(name);if (urlClassLoaderResource != null) {return urlClassLoaderResource;}// 如果找不到,再委托給父加載器return super.getResource(name);}
// ... existing code ...

這個實現非常直接:先調用?findResource(name)?在自己的 URL 中查找,如果找到了就立刻返回;否則,調用?super.getResource(name)?委托給父加載器。

getResources(String name)

這個方法用于查找所有同名的資源,并返回一個?Enumeration

// ... existing code ...@Overridepublic Enumeration<URL> getResources(String name) throws IOException {// 1. 先獲取所有子加載器中的資源Enumeration<URL> urlClassLoaderResources = findResources(name);final List<URL> result = new ArrayList<>();while (urlClassLoaderResources.hasMoreElements()) {result.add(urlClassLoaderResources.nextElement());}// 2. 再獲取所有父加載器中的資源Enumeration<URL> parentResources = getParent().getResources(name);while (parentResources.hasMoreElements()) {result.add(parentResources.nextElement());}// 3. 將兩者合并,并返回一個新的 Enumerationreturn new Enumeration<URL>() {Iterator<URL> iter = result.iterator();public boolean hasMoreElements() {return iter.hasNext();}public URL nextElement() {return iter.next();}};}
// ... existing code ...

這里的實現保證了返回的資源列表中,來自子加載器的資源總是排在來自父加載器的資源前面。

總結

ChildFirstClassLoader?是 Flink 實現依賴隔離和靈活部署的關鍵組件。它通過重寫類和資源的加載方法,顛覆了 Java 默認的雙親委派模型,實現了**子優先(Child-First)**的加載順序。同時,通過?alwaysParentFirstPatterns?配置項保留了一個“后門”,允許特定的、必須在框架和用戶代碼間共享的類庫繼續使用父優先加載,從而在靈活性和穩定性之間取得了平衡。

ParentFirstClassLoader?

ParentFirstClassLoader?是?FlinkUserCodeClassLoaders?的一個靜態內部類,它實現了父優先(Parent-First)的類加載策略。這種策略是 Java 標準的雙親委派模型的直接體現。

// ... existing code .../*** Regular URLClassLoader that first loads from the parent and only after that from the URLs.*/@Internalpublic static class ParentFirstClassLoader extends FlinkUserCodeClassLoader {ParentFirstClassLoader(URL[] urls, ClassLoader parent, Consumer<Throwable> classLoadingExceptionHandler) {super(urls, parent, classLoadingExceptionHandler);}static {ClassLoader.registerAsParallelCapable();}@Overridepublic MutableURLClassLoader copy() {return new ParentFirstClassLoader(getURLs(), getParent(), classLoadingExceptionHandler);}}
// ... existing code ...
  • 繼承關系: 它繼承自?FlinkUserCodeClassLoader。這意味著它擁有了?FlinkUserCodeClassLoader?的所有特性,包括可定制的異常處理機制和作為?MutableURLClassLoader?的動態添加 URL 及?copy?的能力。
  • 構造函數: 它的構造函數非常簡單,只是直接調用了父類?FlinkUserCodeClassLoader?的構造函數,傳遞了必要的參數。
  • copy()?方法: 它實現了?copy()?方法,用于創建一個具有相同 URL、父加載器和異常處理器的新實例。

核心加載邏輯(缺失的?loadClassWithoutExceptionHandling

您可能會注意到,ParentFirstClassLoader?沒有重寫?loadClassWithoutExceptionHandling?方法。這正是它實現“父優先”策略的關鍵所在。

我們回顧一下它的父類?FlinkUserCodeClassLoader?中的這個方法:

// ... existing code ...protected Class<?> loadClassWithoutExceptionHandling(String name, boolean resolve)throws ClassNotFoundException {return super.loadClass(name, resolve);}
// ... existing code ...

由于?ParentFirstClassLoader?沒有重寫它,那么當調用?loadClass?時,它會執行?FlinkUserCodeClassLoader?中的默認實現,即?return super.loadClass(name, resolve)。這里的?super?指向的是?URLClassLoader

URLClassLoader?的?loadClass?方法遵循標準的雙親委派模型:

  1. 檢查類是否已加載。
  2. 如果未加載,將加載請求委托給父加載器
  3. 只有當父加載器(以及父加載器的父加載器,一直到 Bootstrap ClassLoader)都無法加載該類時,才會嘗試在自己的 URL 路徑中查找(即調用?findClass)。

因此,ParentFirstClassLoader?通過不重寫核心加載方法,天然地繼承并實現了標準的“父優先”加載邏輯。

為什么還需要“父優先”?到底用哪個?

這是一個非常好的問題,觸及了 Flink 類加載設計的核心權衡。

雖然 Flink 默認并推薦使用?ChildFirstClassLoader?來解決依賴沖突,但在某些特定場景下,“父優先”仍然是必要或更優的選擇:

  • 簡單場景和測試: 在一些簡單的作業或測試環境中,用戶代碼的依賴非常清晰,與 Flink 框架沒有沖突。在這種情況下,使用標準的“父優先”模型更簡單、更符合 Java 的傳統習慣,也更容易理解和調試。
  • 避免行為不一致: 當用戶代碼嚴重依賴于 Flink 框架提供的某個特定版本的庫時,如果使用“子優先”并意外地打包了一個不兼容的版本,可能會導致難以預料的運行時錯誤。此時,明確指定“父優先”可以確保用戶代碼和 Flink 框架使用完全相同的依賴庫,保證行為的一致性。
  • 向后兼容和用戶選擇: Flink 作為一個成熟的框架,需要提供選項以適應不同的用戶需求和歷史遺留問題。在早期版本或某些特定部署模式下,用戶可能已經習慣了“父優先”的行為。提供這個選項可以平滑過渡,并給予用戶控制權。如?flink-1.10?的發布說明中就提到了這一點,客戶端開始遵循類加載策略,對于希望保持舊行為的用戶,可以顯式配置為?parent-first

到底用哪個?

這取決于你的具體需求和場景,選擇的原則是:

  • 默認和推薦使用?child-first:

    • 當你開發復雜的 Flink 作業,引入了大量第三方依賴(如 Hadoop、HBase、各種 Connectors 的庫)。
    • 當你不確定你的依賴是否會與 Flink 的內部依賴沖突時。
    • 絕大多數生產場景下,都應該使用?child-first。這是 Flink 解決“依賴地獄”(Dependency Hell)問題的標準方案。
  • 在特定情況下選擇?parent-first:

    • 當你的作業非常簡單,幾乎沒有外部依賴,或者你明確知道所有依賴都與 Flink 兼容。
    • 當你遇到由“子優先”引起的、難以解決的?ClassCastException?或?LinkageError,并且你希望強制作業使用 Flink 環境提供的依賴版本時。
    • 在進行某些單元測試或集成測試時,為了簡化環境,可能會選擇“父優先”。

如何控制使用哪個?

可以從多個角度證明?child-first?是 Flink 的默認類加載行為。

在軟件工程中,測試代碼是驗證默認行為最可靠的證據之一。在 Flink 的客戶端測試中,有一個測試用例明確地斷言了默認配置不是?parent-first

在?DefaultPackagedProgramRetrieverITCase.java?文件中,有如下測試代碼:

// ... existing code ...@Testvoid testConfigurationIsConsidered() throws FlinkException {final String parentFirstConfigValue = "parent-first";// we want to make sure that parent-first is not set as a defaultassertThat(CoreOptions.CLASSLOADER_RESOLVE_ORDER.defaultValue()).isNotEqualTo(parentFirstConfigValue);final Configuration configuration = new Configuration();
// ... existing code ...

這段代碼的核心是?assertThat(CoreOptions.CLASSLOADER_RESOLVE_ORDER.defaultValue()).isNotEqualTo(parentFirstConfigValue);。它斷言了?CLASSLOADER_RESOLVE_ORDER?這個配置項的默認值不等于 "parent-first"

我們知道這個配置項只有兩個可選值:child-first?和?parent-first。既然測試證明了默認值不是?parent-first,那么它必然是?child-first

官方文檔?debugging_classloading.md?也明確指出了這一點。

// ... existing code ...
對于用戶代碼的類加載,可以通過調整Flink的[`classloader.resolve-order`]({{< ref "docs/deployment/config" >}}#classloader-resolve-order)配置將ClassLoader解析順序還原至Java的默認模式(從Flink默認的`child-first`調整為`parent-first`)。
// ... existing code ...

括號里的內容“(從Flink默認的child-first調整為parent-first)”直接說明了 Flink 的默認設置是?child-first

查看 Flink 1.10 的發布說明,我們可以了解到這個默認值變更的歷史背景。

flink-1.10.md

// ... existing code ...
# Release Notes - Flink 1.10
### Clusters & Deployment
#### Flink Client respects Classloading Policy 
##### [FLINK-13749](https://issues.apache.org/jira/browse/FLINK-13749)The Flink client now also respects the configured classloading policy, i.e.,
`parent-first` or `child-first` classloading. Previously, only cluster
components such as the job manager or task manager supported this setting.
This does mean that users might get different behaviour in their programs, in
which case they should configure the classloading policy explicitly to use
`parent-first` classloading, which was the previous (hard-coded) behaviour.
// ... existing code ...

這段說明指出,從 Flink 1.10 開始,客戶端也開始遵循可配置的類加載策略。它建議,如果用戶發現行為有變,可以顯式地配置回?parent-first,因為那是之前硬編碼的行為。這暗示了從這個版本開始,默認的行為已經不再是?parent-first,而是可配置的?child-first

綜合以上來自測試代碼、官方文檔和版本歷史的三個方面的證據,我們可以非常確定地得出結論:child-first?是當前 Flink 版本中默認的類加載策略。這是 Flink 為了更好地解決用戶作業與框架之間的依賴沖突而做出的重要設計決策。

FlinkUserCodeClassLoaders?這個工廠類就是根據配置項來創建對應 ClassLoader 實例的:

// ... existing code ...public static MutableURLClassLoader create(ResolveOrder resolveOrder,
// ... existing code ...switch (resolveOrder) {case CHILD_FIRST:return childFirst(
// ... existing code ...case PARENT_FIRST:return parentFirst(
// ... existing-code ...}}
// ... existing code ...

總結

ParentFirstClassLoader?是 Flink 對標準 Java 雙親委派模型的一個直接實現,它通過不重寫關鍵的加載方法來達成“父優先”的目的。

它與?ChildFirstClassLoader?共同構成了 Flink 靈活的類加載體系,為用戶提供了兩種選擇:

  • child-first?(默認): 解決依賴沖突,適用于復雜的生產環境。
  • parent-first: 遵循 Java 標準,適用于簡單場景或需要強制依賴統一的特殊情況。

用戶可以根據自己的作業復雜度和依賴情況,通過?classloader.resolve-order?配置來選擇最適合的加載策略。

SafetyNetWrapperClassLoader

SafetyNetWrapperClassLoader?是?FlinkUserCodeClassLoaders?的一個靜態內部類。從名字就可以看出,它是一個“安全網”包裝器。它的核心目標是解決一個在長時間運行的 JVM 應用中非常棘手的問題:類加載器泄漏,以及由此導致的?OutOfMemoryError: Metaspace

在 Flink 的 Session 集群模式下,用戶的作業代碼(JARs)是動態加載的。每個作業都有自己的?FlinkUserCodeClassLoader。當作業結束后,這個 ClassLoader 以及它加載的所有類(包括用戶代碼、依賴庫等)都應該能被垃圾回收器(GC)回收,從而釋放 Metaspace 內存。

問題在于,有些用戶代碼或其依賴的第三方庫可能會在靜態字段(static field)中持有對當前線程上下文類加載器(Thread.contextClassLoader)的引用。當 Flink 執行用戶代碼時,會把線程上下文類加載器設置為該作業的?FlinkUserCodeClassLoader。如果這個引用在作業結束后沒有被清理,就會導致一個從靜態變量出發的強引用鏈,使得?FlinkUserCodeClassLoader?無法被 GC 回收。隨著作業的不斷提交和結束,泄漏的 ClassLoader 會越來越多,最終耗盡 Metaspace。

SafetyNetWrapperClassLoader?就是為了打破這個潛在的引用鏈,充當一個“保險絲”或“安全網”。

?類的定義與核心結構

// ... existing code .../*** Ensures that holding a reference on the context class loader outliving the scope of user code* does not prevent the user classloader to be garbage collected (FLINK-16245).** <p>This classloader delegates to the actual user classloader. Upon {@link #close()}, the* delegate is nulled and can be garbage collected. Additional class resolution will be resolved* solely through the bootstrap classloader and most likely result in ClassNotFound exceptions.*/@Internalpublic static class SafetyNetWrapperClassLoader extends MutableURLClassLoader {private static final Logger LOG =LoggerFactory.getLogger(SafetyNetWrapperClassLoader.class);protected volatile FlinkUserCodeClassLoader inner;protected SafetyNetWrapperClassLoader(FlinkUserCodeClassLoader inner, ClassLoader parent) {super(new URL[0], parent);this.inner = inner;}
// ... existing code ...
  • 繼承關系: 它繼承自?MutableURLClassLoader,所以它本身也是一個功能完備的 ClassLoader。
  • 包裝器模式 (Wrapper/Decorator Pattern): 它的核心是持有一個?inner?字段,這個字段指向真正的用戶代碼類加載器(比如?ChildFirstClassLoader?或?ParentFirstClassLoader)。
  • 構造函數: 構造函數接收真正的?inner?ClassLoader,并將其保存起來。注意?super(new URL[0], parent),這個包裝器自身并不管理任何 URL,它只是一個空殼。

委托與關閉

SafetyNetWrapperClassLoader?的所有類加載相關的方法,都只是簡單地委托給內部的?inner?加載器來完成。

// ... existing code ...@Overridepublic Class<?> loadClass(String name) throws ClassNotFoundException {return ensureInner().loadClass(name);}@Overrideprotected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {// called for dynamic class loadingreturn ensureInner().loadClass(name, resolve);}@Overridepublic void addURL(URL url) {ensureInner().addURL(url);}@Overridepublic URL getResource(String name) {return ensureInner().getResource(name);}
// ... (其他委托方法) ...

這里的關鍵在于?ensureInner()?和?close()?方法。

ensureInner()?方法:

// ... existing code ...private FlinkUserCodeClassLoader ensureInner() {if (inner == null) {throw new IllegalStateException("Trying to access closed classloader. Please check if you store "+ "classloaders directly or indirectly in static fields. If the stacktrace suggests that the leak "+ "occurs in a third party library and cannot be fixed immediately, you can disable this check "+ "with the configuration '"+ CoreOptions.CHECK_LEAKED_CLASSLOADER.key()+ "'.");}return inner;}

在每次委托之前,它都會檢查?inner?是否為?null。如果不為?null,就返回?inner?實例;如果為?null,則拋出異常,并給出非常詳細和有用的提示信息,指導用戶如何排查類加載器泄漏問題。

close()?方法:

// ... existing code ...@Overridepublic void close() {final FlinkUserCodeClassLoader inner = this.inner;if (inner != null) {try {inner.close();} catch (IOException e) {LOG.warn("Could not close user classloader", e);}}this.inner = null;}
// ... existing code ...

這是“安全網”機制的核心。當 Flink 認為一個用戶代碼類加載器生命周期結束時(例如,作業完成),它會調用?close()?方法。這個方法做了兩件事:

  1. 調用?inner?ClassLoader 自己的?close()?方法(如果需要釋放資源)。
  2. 將?this.inner?設置為?null。這是最關鍵的一步!

"安全網"如何工作

  1. 包裝: Flink 在創建用戶代碼類加載器時,如果開啟了泄漏檢查,并不會直接使用?ChildFirstClassLoader,而是用?SafetyNetWrapperClassLoader?將其包裝起來。
    // ... existing code ...
    private static MutableURLClassLoader wrapWithSafetyNet(FlinkUserCodeClassLoader classLoader, boolean check) {return check? new SafetyNetWrapperClassLoader(classLoader, classLoader.getParent()): classLoader;
    }
    // ... existing code ...
    
  2. 設置上下文: Flink 將這個?SafetyNetWrapperClassLoader?實例設置為?Thread.contextClassLoader
  3. 泄漏發生: 假設有問題的代碼持有了對?SafetyNetWrapperClassLoader?實例的靜態引用。
  4. 作業結束: Flink 調用?SafetyNetWrapperClassLoader?的?close()?方法。
  5. 引用斷開:?close()?方法將?inner?字段置為?null
  6. GC 生效: 現在,即使那個靜態引用依然存在,它也僅僅指向一個輕量級的、幾乎為空的?SafetyNetWrapperClassLoader?對象。而這個包裝器對象內部已經不再引用重量級的、包含了所有用戶代碼和依賴的?ChildFirstClassLoader。因此,ChildFirstClassLoader?及其加載的所有類都沒有了強引用,可以被 GC 正常回收。

通過這種方式,SafetyNetWrapperClassLoader?像一個“熔斷器”,主動斷開了泄漏的引用鏈,從而保護了系統的 Metaspace 不被耗盡。

總結

SafetyNetWrapperClassLoader?是 Flink 框架健壯性的一個重要體現。它通過一個巧妙的包裝器/委托模式,解決了由第三方庫或用戶代碼行為不當引起的類加載器泄漏問題。

  • 功能:作為實際用戶代碼類加載器的代理,轉發所有請求。
  • 核心機制:通過?close()?方法將內部對實際類加載器的引用置為?null
  • 目的:打破因靜態字段引用導致的類加載器泄漏鏈,確保用戶代碼類加載器在作業結束后能被垃圾回收,防止?Metaspace?內存溢出。
  • 用戶體驗:當泄漏的 ClassLoader 被再次使用時,它會拋出帶有明確指導信息的異常,幫助用戶定位和修復問題。這個檢查可以通過配置項?classloader.check-leaked-classloader?來開啟或關閉。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/90366.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/90366.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/90366.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Nginx 安全加固:如何阻止 IP 直接訪問,只允許域名訪問

在部署網站或 Web 應用時,我們通常會通過域名來訪問服務。然而,有時用戶可能會嘗試直接使用服務器的 IP 地址來訪問,這不僅可能繞過我們的域名特定配置(如 SSL 證書、重定向規則等),還可能導致不必要的安全風險或管理混亂。本文將介紹如何配置 Nginx,使其在通過 IP 地址…

服務端處于 TIME_WAIT 狀態的 TCP 連接,收到相同四元組的 SYN 后會發生什么?詳解

文章目錄一、先判斷 SYN 是否合法1、開啟「時間戳」機制1.1、合法 SYN1.2、非法 SYN2、關閉「時間戳」機制1.1、合法 SYN1.2、非法 SYN二、收到合法 SYN三、收到非法 SYN一、先判斷 SYN 是否合法 1、開啟「時間戳」機制 1.1、合法 SYN 客戶端的 SYN「序列號」比服務端「期望…

數字化轉型:一文讀懂從單系統到智能架構(業務、應用、數據、技術架構)的跨越

在數字化浪潮席卷全球的今天&#xff0c;企業正經歷從 “單系統孤島” 到 “智能架構協同” 的范式革命。智能架構以業務敏捷化、應用服務化、數據價值化、技術云原生化為核心特征&#xff0c;通過四個維度的架構升級&#xff0c;破解傳統 IT 系統的效率瓶頸&#xff0c;支撐企…

AUTOSAR進階圖解==>AUTOSAR_SRS_Transformer

AUTOSAR Transformer 詳解 基于AUTOSAR 4.4.0標準的Transformer模塊分析與說明目錄 1. Transformer概述 1.1 Transformer的作用1.2 Transformer的基本特性 2. Transformer架構 2.1 整體架構2.2 類層次結構 3. Transformer類型 3.1 SOME/IP Transformer3.2 COM Based Transform…

【算法專題訓練】05、最大單詞長度乘積

1、題目信息 https://leetcode.cn/problems/aseY1I/description/ 給定一個字符串數組 words&#xff0c;請計算當兩個字符串 words[i] 和 words[j] 不包含相同字符時&#xff0c;它們長度的乘積的最大值。假設字符串中只包含英語的小寫字母。如果沒有不包含相同字符的一對字符串…

Tenable 利用 AI 升級漏洞評級系統,提升風險優先級排序能力

網絡安全公司 Tenable Holdings Inc. 今日宣布對其漏洞優先級評級系統&#xff08;Vulnerability Priority Rating&#xff0c;VPR&#xff09;進行人工智能驅動的升級&#xff0c;旨在幫助機構更準確地識別和應對最具威脅性的漏洞。從60%到1.6%的精準聚焦Tenable VPR 系統于20…

安全插座項目規劃書

安全插座項目規劃書 一、項目概述 本項目旨在設計并開發一款安全插座&#xff0c;通過集成多種安全保護功能&#xff0c;有效預防因電氣故障引發的安全問題&#xff0c;如過載、短路、漏電等&#xff0c;為用戶提供更加可靠的用電環境。 二、技術架構 &#xff08;一&#xff0…

Logcat日志分析

1. AndroidRuntime關鍵字&#xff08;跟整個系統代碼相關&#xff09; 一、AndroidRuntime的核心作用 AndroidRuntime是Android系統負責啟動和運行應用程序的核心組件&#xff0c;當應用因未處理的異常&#xff08;如空指針、數組越界等&#xff09;導致崩潰時&#xff0c;Andr…

Apache Ranger 權限管理

編譯 mvn install package -DskipTests -Dfast -Drat.skiptrue -Dmaven.test.skiptrue -Dcheckstyle.skiptrue -Denforcer.skiptrueinstall.properties PYTHON_COMMAND_INVOKERpython#DB_FLAVORMYSQL|ORACLE|POSTGRES|MSSQL|SQLA DB_FLAVORMYSQL ## # Location of DB client l…

tailscale+GitLab

1. 查看當前 LFS 的遠程地址 bash 復制 git lfs env | grep Endpoint 你會看到類似&#xff1a; Endpointhttp://192.168.3.36/makeup/classicparking.git/info/lfs (authbasic) 2. 修改 LFS 的遠程地址 使用以下命令將 LFS 的地址改為 http://100.125.163.56&#xff1…

微信通話自動錄音器

—————【下 載 地 址】——————— 【?本章下載一】&#xff1a;https://pan.xunlei.com/s/VOVvLpQuRxYadClkxTGwO2OnA1?pwdvind# 【?本章下載二】&#xff1a;https://pan.xunlei.com/s/VOVvLpQuRxYadClkxTGwO2OnA1?pwdvind# 【百款黑科技】&#xff1a;https://uc…

05.原型模式:從影分身術到細胞分裂的編程藝術

目錄序幕&#xff1a;當復制對象成為戰略需求一、原型工廠的核心裝備庫1.1 Java原生的淺克隆術二、深度克隆的煉金法則2.1 手工克隆大法&#xff08;硬核派&#xff09;2.2 序列化克隆術&#xff08;魔法派&#xff09;三、原型模式的工業級裝配3.1 原型注冊管理局3.2 Spring框…

[NLP]如何在 Synopsys VCS 仿真腳本中處理多個 UPF 文件的加載

如何在 Synopsys VCS 仿真腳本中處理多個 UPF 文件的加載 摘要:我將詳細解釋在 Synopsys VCS(VCS)模擬腳本中如何處理多個 UPF 文件的加載,包括原理、命令選項、示例腳本以及注意事項。這基于 VCS 的 native low power verification 支持(IEEE 1801 UPF 標準)。如…

DNF: Decouple and Feedback Network for Seeing in the Dark

DNF&#xff1a;用于暗光視覺的解耦與反饋網絡 摘要 RAW 數據的獨特屬性在低光照圖像增強方面展現出巨大潛力。然而&#xff0c;現有架構在單階段和多階段方法中的固有局限性限制了其性能。跨兩個不同域&#xff08;噪聲到干凈和 RAW 到 sRGB&#xff09;的混合映射&#xff0c…

論文精讀《Frequency domain watermarking: An overview》

1. 數字水印技術基礎概念與發展背景 數字水印技術作為信息隱藏領域的核心分支,其發展歷程可以追溯到20世紀90年代中期計算機網絡和信息技術的快速發展時期。隨著大量版權作品以數字文件形式存在,電子出版逐漸普及,傳統的版權保護方法面臨前所未有的挑戰。數字水印技術應運而…

北斗短報文兜底、5G-A增強:AORO P1100三防平板構建應急通信網絡

公網中斷的災區現場&#xff0c;泥石流阻斷了最后一條光纜。一支救援隊卻在廢墟間有序穿行&#xff0c;隊長手中的三防平板正閃爍著北斗衛星信號&#xff0c;定位坐標與傷亡信息化作一行行短報文&#xff0c;穿透通信孤島直達指揮中心。這是AORO P1100三防平板搭載的北斗短報文…

Java排序算法之<冒泡排序>

目錄 1、冒泡排序介紹 2、算法步驟 3、Java 實現&#xff08;帶優化&#xff09; 4、算法復雜度分析 5、優點與缺點 前言 排序算法的“進化路線”&#xff1a; 冒泡排序 → 選擇排序 → 插入排序 → 希爾排序 → 快速排序 → 歸并排序 → 堆排序↓Java 內置排序&#xff…

生活毫無頭緒就毫無頭緒吧(7.24)

最近好長一段時間沒有記錄了明顯感覺自己陷入了混亂中作息規律&#xff0c;專注力&#xff0c;心流&#xff0c;營養的飯菜如今下筆也沒有什么頭緒&#xff0c;前些日子本有感想但是又疲于記錄&#xff0c;忘了許許多多最近在寫論文&#xff0c;但嘗試了游泳——蛙泳感覺太神奇…

vulhub-master 靶場Apache(httpd)漏洞

apache_parsing_vulnerability 漏洞原理在Apache1.x/2.x中Apache 解析?件的規則是從右到左開始判斷解析,如果后綴名為不可識別?件解析,就再往左判斷。如 1.php.xxxxx&#xff0c;Apache會試圖識別你的代碼&#xff0c;從右往左一個一個試。漏洞攻略參加一個1.php.jpg文件&…

Python 數據分析(一):NumPy 基礎知識

目錄 1. 簡介2. 使用 2.1 ndarray2.2 數據類型2.3 索引與切片2.4 副本與視圖2.5 軸的概念2.6 基本運算2.7 常用操作 1. 簡介 NumPy&#xff08;Numerical Python&#xff09;是一個開源的 Python 科學計算擴展庫&#xff0c;主要用來處理任意維度數組與矩陣&#xff0c;通常…