关于大数据:开源直播课丨大数据集成框架ChunJun类加载器隔离方案探索及实践

4次阅读

共计 8555 个字符,预计需要花费 22 分钟才能阅读完成。

本期咱们带大家回顾一下无倦同学的直播分享《ChunJun 类加载器隔离》,ChunJun 类加载器隔离的计划是咱们近期摸索的一个新计划,这个计划目前还不是十分成熟,心愿能借由此次分享与大家一起探讨下这计划,如果大家有一些新的想法欢送大家在 github 上给我提 issue 或者 pr。

一、Java 类加载器解决类抵触根本思维

在学习计划之前,首先为大家介绍一下 Java 类加载器解决类抵触的根本思维。

01 什么是 Classpath?

Classpath 是 JVM 用到的一个环境变量,它用来批示 JVM 如何搜寻 Class。

因为 Java 是编译型语言,源码文件是.java,而编译后的.class 文件才是真正能够被 JVM 执行的字节码。因而,JVM 须要晓得,如果要加载一个 com.dtstack.HelloWorld 的类,应该去哪搜寻对应的 HelloWorld.class 文件。

所以,Classpath 就是一组目录的汇合,它设置的搜寻门路与操作系统相干,例如:

在 Windows 零碎上,用; 分隔,带空格的目录用 ”” 括起来,可能长这样:

C:\work\project1\bin;C:\shared;”D:\My Documents\project1\bin”

在 MacOS & Linux 零碎上,用: 分隔,可能长这样:

/usr/shared:/usr/local/bin:/home/wujuan/bin

启动 JVM 时设置 Classpath 变量, 实际上就是给 java 命令传入 -Classpath 或 -cp 参数.

java -Classpath .;/Users/lzq/Java/a;/Users/lzq/Java/b com.dtstack.HelloWorld

没有设置零碎环境变量,也没有传入 -cp 参数,那么 JVM 默认的 Classpath 为,即当前目录:

java com.dtstack.HelloWorld

02 Jar 包中的类什么时候被加载?

● Jar 包

Jar 包就是 zip 包,只不过后缀名字不同。用于治理扩散的 .class 类。

生成 jar 包能够用 zip 命令 zip -r ChunJun.zip ChunJun

java -cp ./ChunJun.zip com.dtstack.HelloWorld

● 加载

“加载”(Loading)阶段是整个“类加载”(Class Loading)过程中的一个阶段,心愿读者没有混同这两个看起来很类似的名词。在加载阶段,Java 虚 拟机须要实现以下三件事件:

1. 通过一个类的全限定名来获取定义此类的二进制字节流;

2. 将这个字节流所代表的动态存储构造转化为办法区的运行时数据结构;

3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为办法区这个类的各种数据的拜访入口。

● 解析

类或接口的解析

假如以后代码所处的类为 D,如果要把一个从未解析过的符号援用 N 解析为一个类或接口 C 的间接援用,那虚拟机实现整个解析的过程须要包含以下 3 个步骤:

1. 如果 C 不是一个数组类型,那虚拟机将会把代表 N 的全限定名传递给 D 的类加载器去加载这个类 C。

在加载过程中,因为元数据验证、字节码验证的须要,又可能触发其余相干类的加载动作,例如加载这个类的父类或实现的接口。一旦这个加载过程呈现了任何异样,解析过程就将宣告失败。

2. 如果 C 是一个数组类型,并且数组的元素类型为对象,也就是 N 的描述符会是类

似“[Ljava/lang/Integer 的模式,那将会依照第一点的规定加载数组元素类型。

如果 N 的描述符如后面所假如的模式,须要加载的元素类型就是“java.lang.Integer”,接着由虚拟机生成一个代表该数组维度和元素的数组对象。

3. 如果下面两步没有呈现任何异样,那么 C 在虚拟机中实际上曾经成为一个无效的类或接口了,但在解析实现前还要进行符号援用验证,确认 D 是否具备对 C 的拜访权限。如果发现不具备拜访权限,将抛出 java.lang,llegalAccessEror 异样。

03 哪些行为会触发类的加载?

对于在什么状况下须要开始类加载过程的第一个阶段“加载”,《Java 虚拟机标准》中并没有进行 强制束缚,这点能够交给虚拟机的具体实现来自在把握。然而对于初始化阶段,《Java 虚拟机标准》则是严格规定了有且只有六种状况必须立刻对类进行“初始化”(而加载、验证、筹备天然须要在此之 前开始):

● 场景一

遇到 new、getstatic、putstatic 或 invokestatic 这四条字节码指令时,如果类型没有进行过初始 化,则须要先触发其初始化阶段。可能生成这四条指令的典型 Java 代码场景有:

1. 应用 new 关键字实例化对象的时候。

2. 读取或设置一个类型的动态字段(被 final 润饰、已在编译期把后果放入常量池的动态字段除外)的时候。

3. 调用一个类型的静态方法的时候。

● 场景二

应用 java.lang.reflect 包的办法对类型进行反射调用的时候,如果类型没有进行过初始化,则需 要先触发其初始化。

● 场景三

当初始化类的时候,如果发现其父类还没有进行过初始化,则须要先触发其父类的初始化。

● 场景四

当虚拟机启动时,用户须要指定一个要执行的主类(蕴含 main()办法的那个类),虚构机会先 初始化这个主类。

● 场景五

当应用 JDK 7 新退出的动静语言反对时,如果一个 java.lang.invoke.MethodHandle 实例最初的解析后果为 REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial 四种类型的办法句柄,并且这个办法句柄对应的类没有进行过初始化,则须要先触发其初始化。

●场景六

当一个接口中定义了 JDK 8 新退出的默认办法(被 default 关键字润饰的接口办法)时,如果有这个接口的实现类产生了初始化,那该接口要在其之前被初始化。

对于以上这六种会触发类型进行初始化的场景,《Java 虚拟机标准》中应用了一个十分强烈的限定语 ——“有且只有”,这六种场景中的行为称为对一个类型进行被动援用。除此之外,所有援用类型的方 式都不会触发初始化,称为被动援用。

04 什么是双亲委派机制?

双亲委派机制,是依照加载器的层级关系,逐层进行委派,例如下图中的自定义类加载器想要加载类,它首先不会想要本人去加载,它会通过层级关系逐层进行委派,从自定义类加载器 -> App ClassLoader -> Ext ClassLoader -> BootStrap ClassLoader,如果在 BootStrap ClassLoader 中没有找到想要加载的类,又会逆循环加载。

05 如何突破双亲委派机制?

那么如何突破双亲委派机制呢? 其实能够通过重写 loadclass 办法来实现,具体过程大家可通过视频理解,这里就不过多赘述。

二、Flink 类加载隔离的计划

接下来咱们来介绍下 Flink 类加载隔离的计划,Flink 有两品种加载器 Parent-First 和 Child-First,他们的区别是:

1.Parent-First

相似 Java 中的双亲委派的类加载机制。Parent First ClassLoader 理论的逻辑就是一个 URL ClassLoader。

2.Child-First

先用 classloader.parent-first-patterns.default 和 classloader.parent-first-patterns.additional 拼接的 list 做匹配,如果类名前缀匹配了,先走双亲委派。否则就用 ChildFirstClassLoader 先加载。

Child-First 存在的问题

每次新 new 一个 ChildFirstClassLoader,如果运行工夫久的话,相似 Session 这种 TaskManager 始终不敞开的状况。工作运行屡次当前,会呈现元数据空间爆掉,导致工作失败。

Child-First 加载原理

01 Flink 是如何防止类泄露的?

大家能够参考 Flink 中的 jira,这外面蕴含一些 bug 和解决办法:

https://issues.apache.org/jir…

https://issues.apache.org/jir…

Flink 如何防止类泄露,次要是通过以下两种办法:

  1. 减少一层委派类加载器,将真正的 UserClassloader 包裹起来。
  1. 减少一个回调钩子,当工作完结的时候能够提供给用户一个接口,去开释未开释的资源。

KinesisProducer 应用了这个钩子

final RuntimeContext ctx = getRuntimeContext();

ctx.registerUserCodeClassLoaderReleaseHookIfAbsent(

KINESIS_PRODUCER_RELEASE_HOOK_NAME,

()-> this.runClassLoaderReleaseHook

(ctx.getUserCodeClassLoader()));

02 Flink 卸载用户代码中动静加载的类

卸载用户代码中动静加载的类,所有波及动静用户代码类加载(会话)的场景都依赖于再次 卸载 的类。

类卸载指垃圾回收器发现一个类的对象不再被援用,这时会对该类(相干代码、动态变量、元数据等)进行移除。

当 TaskManager 启动或重启工作时会加载指定工作的代码,除非这些类能够卸载,否则就有可能引起内存泄露,因为更新新版本的类可能会随着工夫一直的被加载积攒。这种景象常常会引起 OutOfMemoryError: Metaspace 这种典型异样。

类透露的常见起因和倡议的修复形式:

● Lingering Threads

确保利用代码的函数 /sources/sink 敞开了所有线程。提早敞开的线程不仅本身耗费资源,同时会因为占据对象援用,从而阻止垃圾回收和类的卸载。

● Interners

防止缓存超出 function/sources/sinks 生命周期的非凡构造中的对象。比方 Guava 的 Interner,或是 Avro 的序列化器中的类或对象。

● JDBC

JDBC 驱动会在用户类加载器之外透露援用。为了确保这些类只被加载一次,能够将驱动 JAR 包放在 Flink 的 lib/ 目录下,或者将驱动类通过 classloader-parent-first-patterns-additional 加到父级优先加载类的列表中。

开释用户代码类加载器的钩子(hook)能够帮忙卸载动静加载的类,这种钩子在类加载器卸载前执行,通常状况下最好把敞开和卸载资源作为失常函数生命周期操作的一部分(比方典型的 close() 办法)。有些状况下(比方动态字段)最好确定类加载器不再须要后就立刻卸载。

开释类加载器的钩子能够通过

RuntimeContext.registerUserCodeClassLoaderReleaseHookIfAbsent()办法进行注册。

03 Flink 卸载 Classloader 源码

BlobLibraryCacheManager$ResolvedClassLoader

private void runReleaseHooks() {

Set<map.entry> hooks = releaseHooks.entrySet();

if (!hooks.isEmpty()) {for (Map.EntryhookEntry : hooks) {

        try {LOG.debug("Running class loader shutdown hook: {}.", hookEntry.getKey());

            hookEntry.getValue().run();

        } catch (Throwable t) {

            LOG.warn("Failed to run release hook'{}'for user code class loader.",

                    hookEntry.getValue(),

                    t);

        }

    }

    releaseHooks.clear();}

}

三、ChunJun 如何实现类加载隔离

接下来为大家介绍下 ChunJun 如何实现类加载隔离。

01 Flink jar 的上传机会

首先咱们须要上传 Jar 包,整体流程如下图所示:

● Yarn Perjob

提交工作的时候上传 jar 包,会放到

hdfs://flink03:9000/user/root/.flink/application_1654762357754_0140。

● Yarn Session

启动 Session 的时候,Yarn 的 App 上传 Jar 包机制,往 Session 提交工作的时候,Flink 的 Blob Server 负责收。

02 Yarn 的分布式缓存

03 Yarn 的分布式缓存

分布式缓存机制是由各个 NM 实现的,次要性能是将应用程序所需的文件资源缓存到本地,以便后续工作的应用。资源缓存是用时触发的,也就是第一个用到该资源的工作触发,后续工作无需再进行缓存,间接应用即可。

依据资源类型和资源可见性,NM 可将资源分成不同类型:

资源可见性分类

● Public

节点上所有的用户都能够共享该资源,只有有一个用户的应用程序将着这些资源缓存到本地,其余所有用户的所有应用程序都能够应用。

● Private

节点上同一用户的所有应用程序共享该资源,只有该用户其中一个应用程序将资源缓存到本地,该用户的所有应用程序都能够应用。

● Application

节点上同一应用程序的所有 Container 共享该资源

资源类型分类

● Archive

归档文件,反对.jar、.zip、.tar.gz、.tgz、.tar 的 5 种归档文件。

● File

一般文件,NM 只是将这类文件下载到本地目录,不做任何解决

● Pattern

以上两种文件的混合体

YARN 是通过比拟 resource、type、timestamp 和 pattern 四个字段是否雷同来判断两个资源申请是否雷同的。如果一个曾经被缓存到各个节点上的文件被用户批改了,则下次应用时会主动触发一次缓存更新,以从新从 HDFS 上下载文件。

分布式缓存实现的次要性能是文件下载,波及大量的磁盘读写,因而整个过程采纳了异步并发模型放慢文件下载速度,以防止同步模型带来的性能开销。

04 Yarn 的分布式缓存

NodeManager 采纳轮询的调配策略将这三类资源寄存在 yarn.nodemanager.local-dirs 指定的目录列表中,在每个目录中,资源依照以下形式寄存:

● Public 资源

寄存在 ${yarn.nodemanager.local-dirs}/filecache/ 目录下,每个资源将独自寄存在以一个随机整数命名的目录中,且目录的拜访权限均为 0755。

● Private 资源

寄存在 ${yarn.nodemanager.local-dirs}/usercache/${user}/filecache/ 目录下,(其中 ${user}是应用程序提交者,默认状况下均为 NodeManager 启动者),每个资源将独自寄存在以一个随机整数命名的目录中,且目录的拜访权限均为 0710。

● Application 资源

寄存在 ${yarn.nodemanager.local-dirs}/usercache/${user}/${appcache}/${appid}/filecache/ 目录下(其中 ${appid}是应用程序 ID),每个资源将独自寄存在以一个随机整数命名的目录中,且目录的拜访权限均为 0710;

其中 Container 的工作目录位于 ${yarn.nodemanager.local-dirs}/usercache/${user}/${appcache}/${appid}/${containerid}目录下,其次要保留 jar 包文件、字典文件对应的软链接。

05 Flink BlobServer

06 如何疾速提交,缩小上传 jar 包

Flink libs 上面 jar 包、Flink Plugins 上面的 jar 包、Flink 工作的 jar 包(对于 ChunJun 来说就是所有 connector 和 core),Flink jar 用户自定义 jar 包。

● Perjob

如果能够提前上传到 HDFS:

  1. 提前把 Flink lib、Flink plugins、ChunJun jar 上传到 HDFS 下面。
  2. 提交工作的时候通过 yarn.provided.lib.dirs 指定 HDFS 下面的门路即可。

如果不能够提前上传到 HDFS:

  1. 工作提交上传到 HDFS 固定地位,提交的时候查看 HDFS 上如果有对应的 jar(有缓存策略),就把本地门路替换成近程门路。
  2. 利用回调钩子,分明异样工作完结的垃圾文件。

● Seeion

如果能够提前上传到 HDFS:

  1. 提前把 Flink lib、Flink plugins、ChunJun jar 上传到 HDFS 下面。
  2. 启动 session 的时候通过 yarn.provided.lib.dirs 指定 HDFS 下面的门路即可。
  3. 提交工作的时候不须要上传 core 包。

如果不能够提前上传到 HDFS:

  1. Session 启动的时候就上传所有 jar 到 HDFS 下面。通过 yarnship 指定。
  2. Flink 工作提交到 Session 的时候,不须要提交任何 jar 包。

07 类加载隔离遇到的问题剖析

● 思路剖析

  1. 首先要把不同插件(connector) 放到不同的 Classloader 外面。
  2. 而后应用 child-first 的加载策略。
  3. 确保不会产生 x not cast x 谬误。
  4. 元数据空间不会内存泄露,导致工作报错。
  5. 要缓存 connector jar 包。

● 遇到的问题

  1. Flink 一个 job 可能有多个算子,一个 connector 就是一个算子。Flink 原生是为 job 级别新生成的 Classloader,无奈把每个 connector 放在一个独立的 Classloader 外面。
  1. child-first 加载策略在 Session 模式下每次都新 new 一个 Classloader,导致元数据空间内存泄露。
  1. connecotor 之间用到私有的类会报错。
  1. 和问题 2 相似,次要是因为有些线程池,守护线程会拿着一些类对象,或者类 class 对象的援用。
  1. 如果用原生 -yarnship 去上传,会放到 App Classloader 外面。那么就会导致某些不冀望用 App Classloader 加载的类被加载。

08 Flink JobGraph Classpath 的应用

/* Set of JAR files required to run this job. /

private final ListuserJars = new ArrayList();

/* Set of custom files required to run this job. /

private final MapuserArtifacts = new HashMap<>();

/* List of Classpaths required to run this job. /

private ListClasspaths = Collections.emptyList();

  1. 客户端解决,JobGraph 解决 userJars、userArtifacts、Classpaths 这三个属性。
  2. Classpath 只留下 connector 的层级目录。
  3. 启动 Session 的时候上传 jar,jar 缓存在 Yarn 的所有的 NodeManager 节点。
  4. jobmanager 和 taskmanager 构建 Classloader 的时候去批改 Classpath 的门路,替换成以后节点 NodeManager 的缓存门路。
  5. 依据不同 connecotr 去构建 Flink Job 的 Classloader。
  6. 把构建进去的 classlaoder 进行缓存,下次工作还有雷同的 Classloader。防止内存泄露。
  7. 重写新的 ChildFirstCacheClassloader 外面的 loadclass 办法,依据不同的 connector url 去生成 独自的 Classloader。

四、遇到的问题和排查计划?

jar 包抵触常见的异样为找不到类(java.lang.ClassNotFoundException)、找不到具体方法(java.lang.NoSuchMethodError)、字段谬误(java.lang.NoSuchFieldError)或者类谬误(java.lang.LinkageError)。

● 常见的解决办法如下

1、首先做法是打出工程文件的依赖树,将依据 jar 包依赖状况断定是不是同一个 jar 包依赖了多个版本,如果确认问题所在,间接 exclusion 其中谬误的 jar 包即可。

2、如果通过看依赖树不能确定具体抵触的 jar 包,能够应用增加 jvm 参数的形式启动程序,将类加载的具体 jar 信息打印进去;-verbose:class。

3、通过上述步骤根本就能够解决 jar 包抵触问题,具体的问题要具体分析。

● 常用工具举荐

1.Maven-helper

次要排查类抵触的 IDEA 插件。

2.Jstack

死锁的一些问题能够通过这个工具查看 jstack 调用栈。

3.Arthas

排查一些性能问题和 Classloader 泄露问题。

4.VisualVM

排查一些对象内存泄露、dump 文件剖析等。

袋鼠云开源框架钉钉技术交换 qun(30537511),欢送对大数据开源我的项目有趣味的同学退出交换最新技术信息,开源我的项目库地址:https://github.com/DTStack/Taier

正文完
 0