生活资讯
flink类加载器原理与隔离(flink jar包冲突)
2025-01-01 16:32  浏览:99


本文是转载自袋鼠云公众号的文章
不知道大家有没有遇到过,flink发布任务遇到一些奇奇怪怪的报错,很奇怪的某个类就开始报错,一步一步点击去查看,发现不知道是哪个类包的那个类在报错,其实这种情况很有可能就是jar包版本冲突。
首先为大家介绍一下Java类加载器解决类冲突的基本思想。

Classpath是JVM用到的一个环境变量,它用来指示JVM如何搜索Class。
因为Java是编译型语言,源码文件是.java,而编译后的.class文件才是真正可以被JVM执行的字节码。因此,JVM需要知道,如果要加载一个com.dtstack.HelloWorld的类,应该去哪搜索对应的HelloWorld.class文件。

所以,Classpath就是一组目录的集合,它设置的搜索路径与操作系统相关,例如
在Windows系统上,用;分隔,带空格的目录用""括起来,可能长这样
C:****
在MacOS & Linux系统上,用:分隔,可能长这样
/usr*****

启动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

  • jar包准备
    Jar 包就是 zip 包,只不过后缀名字不同。用于管理分散的 .class 类。
    生成 jar 包可以用 zip 命令 zip -r ChunJun.zip ChunJun
    java -cp https://blog.csdn.net/Direction_Wind/article/details/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异常。
  • 场景二
    使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需 要先触发其初始化。
  • 场景三
    当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 场景四
    当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类,虚拟机会先 初始化这个主类
  • 场景五
    当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
  • 场景六
    当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
    对于以上这六种会触发类型进行初始化的场景,《Java虚拟机规范》中使用了一个非常强烈的限定语 ——“有且只有”,这六种场景中的行为称为对一个类型进行主动引用。除此之外,所有引用类型的方 式都不会触发初始化,称为被动引用。

接下来我们来介绍下Flink 类加载隔离的方案,Flink有两种类加载器Parent-First和Child-First,他们的区别是

  • Parent-First
    类似 Java 中的双亲委派的类加载机制。Parent First ClassLoader 实际的逻辑就是一个 URL ClassLoader。

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

  • Child-First 存在的问题
    每次新 new 一个 ChildFirstClassLoader,如果运行时间久的话,类似 Session 这种 TaskManager 一直不关闭的情况。任务运行多次以后,会出现元数据空间爆掉,导致任务失败。

大家可以参考Flink中的jira,这里面包含一些bug和处理方法
https://issues.a@pache.org/jira/br@owse/Flink-16245
https://issues.a@pache.org/jira/br@owse/Flink-11205

Flink如何避免类泄露,主要是通过以下两种方法

  1. 增加一层委派类加载器,将真正的 UserClassloader 包裹起来。

  2. 增加一个回调钩子,当任务结束的时候可以提供给用户一个接口,去释放未释放的资源。
    KinesisProducer 使用了这个钩子

 
 

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

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

当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)可以帮助卸载动态加载的类,这种钩子在类加载器卸载前执行,通常情况下最好把关闭和卸载资源作为正常函数生命周期操作的一部分(比如典型的方法)。有些情况下(比如静态字段)最好确定类加载器不再需要后就立即卸载。

释放类加载器的钩子可以通过
方法进行注册。

 
 
 

首先我们需要上传Jar包,整体流程如下图所示

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

根据资源类型和资源可见性,NM可将资源分成不同类型
● 资源可见性分类
● Public
节点上所有的用户都可以共享该资源,只要有一个用户的应用程序将着这些资源缓存到本地,其他所有用户的所有应用程序都可以使用。

● Private
节点上同一用户的所有应用程序共享该资源,只要该用户其中一个应用程序将资源缓存到本地,该用户的所有应用程序都可以使用。

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

● 资源类型分类
● Archive
归档文件,支持.jar、.zip、.tar.gz、.tgz、.tar的5种归档文件。
● File
普通文件,NM只是将这类文件下载到本地目录,不做任何处理

● Pattern
以上两种文件的混合体

YARN是通过比较resource、type、timestamp和pattern四个字段是否相同来判断两个资源请求是否相同的。如果一个已经被缓存到各个节点上的文件被用户修改了,则下次使用时会自动触发一次缓存更新,以重新从HDFS上下载文件。

分布式缓存完成的主要功能是文件下载,涉及大量的磁盘读写,因此整个过程采用了异步并发模型加快文件下载速度,以避免同步模型带来的性能开销。

NodeManager采用轮询的分配策略将这三类资源存放在yarn.nodemanager.local-dirs指定的目录列表中,在每个目录中,资源按照以下方式存放
● Public资源
存放在${yarn.nodemanager.local-dirs}/filecache/目录下,每个资源将单独存放在以一个随机整数命名的目录中,且目录的访问权限均为0755。

● Private资源
存放在{user}/filecache/目录下(其中${user}是应用程序提交者,默认情况下均为NodeManager启动者,每个资源将单独存放在以一个随机整数命名的目录中,且目录的访问权限均为0710。

● Application资源
存放在{user}/{appid}/filecache/目录下(其中${appid}是应用程序ID,每个资源将单独存放在以一个随机整数命名的目录中,且目录的访问权限均为0710

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 包。
  1. 首先要把不同插件(connector) 放到不同的 Classloader 里面。
  2. 然后使用 child-first 的加载策略。
  3. 确保不会发生 x not cast x 错误。
  4. 元数据空间不会内存泄露,导致任务报错。
  5. 要缓存 connector jar 包。

● 遇到的问题

  1. Flink 一个 job 可能有多个算子,一个 connector 就是一个算子。Flink 原生是为 job 级别新生成的 Classloader,无法把每个 connector 放在一个独立的 Classloader 里面。

  2. child-first 加载策略在 Session 模式下每次都新 new 一个 Classloader,导致元数据空间内存泄露。

  3. connecotor 之间用到公有的类会报错。

  4. 和问题2类似,主要是因为有些线程池,守护线程会拿着一些类对象,或者类 class 对象的引用。

 
  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包冲突问题,具体的问题要具体分析。

2.Jstack
死锁的一些问题可以通过这个工具查看 jstack 调用栈。

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

    以上就是本篇文章【flink类加载器原理与隔离(flink jar包冲突)】的全部内容了,欢迎阅览 ! 文章地址:http://ww.kub2b.com/tnews/4179.html
     栏目首页      相关文章      动态      同类文章      热门文章      网站地图      返回首页 企库往资讯移动站 http://ww.kub2b.com/mobile/ , 查看更多   
最新文章
耐水弹力海棉
产品属性用途区域产品包装、耐水 防潮密度0.02-0.18g/cm3原产地中国,江苏,常州品牌D-Foam形状可根据客户提供图纸生产颜色可根
耐水高强度海棉
产品属性用途区域产品包装、耐水 防潮密度0.02-0.18g/cm3原产地中国,江苏,常州品牌D-Foam形状可根据客户提供图纸生产颜色可根
防潮耐水EVA材料
产品属性用途区域产品包装、耐水 防潮密度0.02-0.18g/cm3原产地中国,江苏,常州品牌D-Foam形状可根据客户提供图纸生产颜色可根
耐油耐水海绵
产品属性用途区域产品包装、耐水 防潮密度0.02-0.18g/cm3原产地中国,江苏,常州品牌D-Foam形状可根据客户提供图纸生产颜色可根
供应耐水海绵
产品属性用途区域产品包装、耐水 防潮密度0.02-0.18g/cm3原产地中国,江苏,常州品牌D-Foam形状可根据客户提供图纸生产颜色可根
看了OPPO、vivo的新旗舰手机样张后,决定还是继续用微单吧
最近,OPPO、vivo都给出了自家旗舰手机的样张,大战一触即发。记得手机圈上一次这么火爆,还是小米15 Ultra的时候。具体来说,当
微信借钱不求人,6个步骤轻松搞定...手机微信怎么借钱「微信借钱不求人,6个步骤轻松搞定...」
微信,作为中国人日常生活中不可或缺的社交软件,不仅满足了人们的沟通需求,还悄然融入了金融服务,其中就包括微信借钱功能。无
小米8系列手机,有它才叫防摔保护手机爆屏「小米8系列手机,有它才叫防摔保护」
手机已成为日常生活必备品,而且小米8陶瓷后盖摔不得,维修的费用都赶上半个手机的钱了,选什么手机壳呢,贼难拆的磨砂硬壳?一
米其林指南开启江苏篇章,“江苏味”如何与世界“双向奔赴”
米其林指南作为餐饮界的“奥斯卡”,关注度高。2024年7月,米其林指南重调评价体系,转为省份榜单评选,并官宣江苏省、福建省成
重磅发布!5.4%!
4月16日,国家统计局发布的数据显示,一季度,在以习近平同志为核心的党中央坚强领导下,各地区各部门认真贯彻落实党中央、国务