大數據

雙親委派模型與 Flink 的類加載策略

作者:LittleMagic

我們知道,在 JVM 中,一個類加載的過程大致分為加載、鏈接(驗證、準備、解析)、初始化5個階段。而我們通常提到類的加載,就是指利用類加載器(ClassLoader)通過類的全限定名來獲取定義此類的二進制字節碼流,進而構造出類的定義。

Flink 作為基於 JVM 的框架,在 flink-conf.yaml 中提供了控制類加載策略的參數 classloader.resolve-order,可選項有 child-first(默認)和 parent-first。本文來簡單分析一下這個參數背後的含義。

640-1.png

parent-first 類加載策略

ParentFirstClassLoader 和 ChildFirstClassLoader 類的父類均為 FlinkUserCodeClassLoader 抽象類,先來看看這個抽象類,代碼很短。

public abstract class FlinkUserCodeClassLoader extends URLClassLoader {
    public static final Consumer<Throwable> NOOP_EXCEPTION_HANDLER = classLoadingException -> {};

    private final Consumer<Throwable> classLoadingExceptionHandler;

    protected FlinkUserCodeClassLoader(URL[] urls, ClassLoader parent) {
        this(urls, parent, NOOP_EXCEPTION_HANDLER);
    }

    protected FlinkUserCodeClassLoader(
            URL[] urls,
            ClassLoader parent,
            Consumer<Throwable> classLoadingExceptionHandler) {
        super(urls, parent);
        this.classLoadingExceptionHandler = classLoadingExceptionHandler;
    }

    @Override
    protected final Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        try {
            return loadClassWithoutExceptionHandling(name, resolve);
        } catch (Throwable classLoadingException) {
            classLoadingExceptionHandler.accept(classLoadingException);
            throw classLoadingException;
        }
    }

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

FlinkUserCodeClassLoader 繼承自 URLClassLoader。因為 Flink App 的用戶代碼在運行期才能確定,所以通過 URL 在 JAR 包內尋找全限定名對應的類是比較合適的。而 ParentFirstClassLoader 僅僅是一個繼承 FlinkUserCodeClassLoader 的空類而已。

static class ParentFirstClassLoader extends FlinkUserCodeClassLoader {    ParentFirstClassLoader(URL[] urls, ClassLoader parent, Consumer<Throwable> classLoadingExceptionHandler) {
        super(urls, parent, classLoadingExceptionHandler);
    }
}

這樣就相當於 ParentFirstClassLoader 直接調用了父加載器的 loadClass() 方法。之前已經講過,JVM 中類加載器的層次關係和默認 loadClass() 方法的邏輯由雙親委派模型(parents delegation model)來體現,複習一下含義:

如果一個類加載器要加載一個類,它首先不會自己嘗試加載這個類,而是把加載的請求委託給父加載器完成,所有的類加載請求最終都應該傳遞給最頂層的啟動類加載器。只有當父加載器無法加載到這個類時,子加載器才會嘗試自己加載。

640-2.png

可見,Flink 的 parent-first 類加載策略就是照搬雙親委派模型的。也就是說,用戶代碼的類加載器是 Custom ClassLoader,Flink 框架本身的類加載器是 Application ClassLoader。用戶代碼中的類先由 Flink 框架的類加載器加載,再由用戶代碼的類加載器加載。但是,Flink 默認並不採用 parent-first 策略,而是採用下面的 child-first 策略,繼續看。

child-first 類加載策略

我們已經瞭解到,雙親委派模型的好處就是隨著類加載器的層次關係保證了被加載類的層次關係,從而保證了 Java 運行環境的安全性。但是在 Flink App 這種依賴紛繁複雜的環境中,雙親委派模型可能並不適用。例如,程序中引入的 Flink-Cassandra Connector 總是依賴於固定的 Cassandra 版本,用戶代碼中為了兼容實際使用的 Cassandra 版本,會引入一個更低或更高的依賴。而同一個組件不同版本的類定義有可能會不同(即使類的全限定名是相同的),如果仍然用雙親委派模型,就會因為 Flink 框架指定版本的類先加載,而出現莫名其妙的兼容性問題,如 NoSuchMethodError、IllegalAccessError 等。

鑑於此,Flink 實現了 ChildFirstClassLoader 類加載器並作為默認策略。它打破了雙親委派模型,使得用戶代碼的類先加載,官方文檔中將這個操作稱為"Inverted Class Loading"。代碼仍然不長,錄如下。

public final class ChildFirstClassLoader extends FlinkUserCodeClassLoader {
    private final String[] alwaysParentFirstPatterns;

    public ChildFirstClassLoader(
            URL[] urls,
            ClassLoader parent,
            String[] alwaysParentFirstPatterns,
            Consumer<Throwable> classLoadingExceptionHandler) {
        super(urls, parent, classLoadingExceptionHandler);
        this.alwaysParentFirstPatterns = alwaysParentFirstPatterns;
    }

    @Override
    protected synchronized Class<?> loadClassWithoutExceptionHandling(
            String name,
            boolean resolve) throws ClassNotFoundException {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);

        if (c == null) {
            // check whether the class should go parent-first
            for (String alwaysParentFirstPattern : alwaysParentFirstPatterns) {
                if (name.startsWith(alwaysParentFirstPattern)) {
                    return super.loadClassWithoutExceptionHandling(name, resolve);
                }
            }

            try {
                // check the URLs
                c = findClass(name);
            } catch (ClassNotFoundException e) {
                // let URLClassLoader do it, which will eventually call the parent
                c = super.loadClassWithoutExceptionHandling(name, resolve);
            }
        }

        if (resolve) {
            resolveClass(c);
        }
        return c;
    }

    @Override
    public URL getResource(String name) {
        // first, try and find it via the URLClassloader
        URL urlClassLoaderResource = findResource(name);
        if (urlClassLoaderResource != null) {
            return urlClassLoaderResource;
        }
        // delegate to super
        return super.getResource(name);
    }

    @Override
    public Enumeration<URL> getResources(String name) throws IOException {
        // first get resources from URLClassloader
        Enumeration<URL> urlClassLoaderResources = findResources(name);
        final List<URL> result = new ArrayList<>();

        while (urlClassLoaderResources.hasMoreElements()) {
            result.add(urlClassLoaderResources.nextElement());
        }

        // get parent urls
        Enumeration<URL> parentResources = getParent().getResources(name);
        while (parentResources.hasMoreElements()) {
            result.add(parentResources.nextElement());
        }

        return new Enumeration<URL>() {
            Iterator<URL> iter = result.iterator();

            public boolean hasMoreElements() {
                return iter.hasNext();
            }

            public URL nextElement() {
                return iter.next();
            }
        };
    }
}

核心邏輯位於 loadClassWithoutExceptionHandling() 方法中,簡述如下:

  1. 調用 findLoadedClass() 方法檢查全限定名 name 對應的類是否已經加載過,若沒有加載過,再繼續往下執行。
  2. 檢查要加載的類是否以 alwaysParentFirstPatterns 集合中的前綴開頭。如果是,則調用父類的對應方法,以 parent-first 的方式來加載它。
  3. 如果類不符合 alwaysParentFirstPatterns 集合的條件,就調用 findClass() 方法在用戶代碼中查找並獲取該類的定義(該方法在 URLClassLoader 中有默認實現)。如果找不到,再 fallback 到父加載器來加載。
  4. 最後,若 resolve 參數為 true,就調用 resolveClass() 方法鏈接該類,最後返回對應的 Class 對象。

可見,child-first 策略避開了“先把加載的請求委託給父加載器完成”這一步驟,只有特定的某些類一定要“遵循舊制”。alwaysParentFirstPatterns 集合中的這些類都是 Java、Flink 等組件的基礎,不能被用戶代碼沖掉。它由以下兩個參數來指定:

classloader.parent-first-patterns.default,不建議修改,固定為以下這些值:

java.;
scala.;
org.apache.flink.;
com.esotericsoftware.kryo;
org.apache.hadoop.;
javax.annotation.;
org.slf4j;
org.apache.log4j;
org.apache.logging;
org.apache.commons.logging;
ch.qos.logback;
org.xml;
javax.xml;
org.apache.xerces;
org.w3c
  • classloader.parent-first-patterns.additional:除了上一個參數指定的類之外,用戶如果有其他類以 child-first 模式會發生衝突,而希望以雙親委派模型來加載的話,可以額外指定(分號分隔)。

以上是關於 flink-conf.yaml 中提供的控制類加載策略的參數 classloader.resolve-order 含義的理解和分享,希望對大家有所啟發和幫助~

更多 Flink 相關問題可加入 Flink 社區釘釘大群交流~

最新釘群二維碼.jpeg

Leave a Reply

Your email address will not be published. Required fields are marked *