双亲委派模型与 Flink 的类加载策略

本文涉及的产品
实时计算 Flink 版,1000CU*H 3个月
简介: Flink 作为基于 JVM 的框架,在 flink-conf.yaml 中提供了控制类加载策略的参数 classloader.resolve-order,可选项有 child-first(默认)和 parent-first。本文来简单分析一下这个参数背后的含义。

作者: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

相关实践学习
基于Hologres+Flink搭建GitHub实时数据大屏
通过使用Flink、Hologres构建实时数仓,并通过Hologres对接BI分析工具(以DataV为例),实现海量数据实时分析.
实时计算 Flink 实战课程
如何使用实时计算 Flink 搞定数据处理难题?实时计算 Flink 极客训练营产品、技术专家齐上阵,从开源 Flink功能介绍到实时计算 Flink 优势详解,现场实操,5天即可上手! 欢迎开通实时计算 Flink 版: https://cn.aliyun.com/product/bigdata/sc Flink Forward Asia 介绍: Flink Forward 是由 Apache 官方授权,Apache Flink Community China 支持的会议,通过参会不仅可以了解到 Flink 社区的最新动态和发展计划,还可以了解到国内外一线大厂围绕 Flink 生态的生产实践经验,是 Flink 开发者和使用者不可错过的盛会。 去年经过品牌升级后的 Flink Forward Asia 吸引了超过2000人线下参与,一举成为国内最大的 Apache 顶级项目会议。结合2020年的特殊情况,Flink Forward Asia 2020 将在12月26日以线上峰会的形式与大家见面。
相关文章
|
SQL Kubernetes 调度
Flink 流批一体在模型特征场景的使用
本文整理自B站资深开发工程师张杨老师在 Flink Forward Asia 2023 中 AI 特征工程专场中的分享。
77864 5
Flink 流批一体在模型特征场景的使用
|
流计算
在Flink中,Regular Join(包括Left Join)的结果顺序是由Flink的分区策略和数据的分布方式共同决定的
在Flink中,Regular Join(包括Left Join)的结果顺序是由Flink的分区策略和数据的分布方式共同决定的
109 1
|
11月前
|
存储 分布式计算 API
大数据-107 Flink 基本概述 适用场景 框架特点 核心组成 生态发展 处理模型 组件架构
大数据-107 Flink 基本概述 适用场景 框架特点 核心组成 生态发展 处理模型 组件架构
432 0
|
监控 关系型数据库 MySQL
实时计算 Flink版产品使用问题之在进行全量数据初始化时,连接器一般会采用什么策略
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
|
关系型数据库 MySQL 数据处理
实时计算 Flink版产品使用合集之怎样导数据使starrocks支持主键模型delete的配置
实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
159 1
|
存储 流计算
在Flink CDC中,Checkpoint的清理策略通常有两种设置方式
在Flink CDC中,Checkpoint的清理策略通常有两种设置方式
566 5
|
流计算
Flink CDC-sql怎样导数据使starrocks支持主键模型delete的配置吗?目前只能更新和插入,但是删除不行
Flink CDC-sql怎样导数据使starrocks支持主键模型delete的配置吗?目前只能更新和插入,但是删除不行
415 1
|
流计算
从Flink 重启策略机制能学习到什么?
最近在学习Flink ,在看到Flink的重启策略机制时感觉这个设计很好。
202 0
|
消息中间件 资源调度 Oracle
对Flink流处理模型的抽象
对Flink流处理模型的抽象
对Flink流处理模型的抽象
|
SQL 分布式计算 算法
Flink 的编程模型与其他框架比较
我们在讲解 Flink 程序的编程模型之前,先来了解一下 Flink 中的 Streams、State、Time 等核心概念和基础语义,以及 Flink 提供的不同层级的 API。
320 0
Flink 的编程模型与其他框架比较

相关产品

  • 实时计算 Flink版