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
关于在什么情况下需要开始类加载过程的第一个阶段“加载”,《Java虚拟机规范》中并没有进行 强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,《Java虚拟机规范》 则是严格规定了有且只有六种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之 前开始)
- 场景一
遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始 化,则需要先触发其初始化阶段。能够生成这四条指令的典型Java代码场景有:
1.使用new关键字实例化对象的时候。
2.读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外) 的时候。
3.调用一个类型的静态方法的时候。
双亲委派机制,是按照加载器的层级关系,逐层进行委派,例如下图中的自定义类加载器想要加载类,它首先不会想要自己去加载,它会通过层级关系逐层进行委派,从自定义类加载器 -> App ClassLoader -> Ext ClassLoader -> BootStrap ClassLoader,如果在BootStrap ClassLoader中没有找到想要加载的类,又会逆循环加载。
那么如何打破双亲委派机制呢?其实可以通过重写 loadclass 方法来实现,具体过程大家可通过更多学习了解,这里就不过多赘述。
接下来我们来介绍下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如何避免类泄露,主要是通过以下两种方法:
增加一层委派类加载器,将真正的 UserClassloader 包裹起来。
增加一个回调钩子,当任务结束的时候可以提供给用户一个接口,去释放未释放的资源。
KinesisProducer 使用了这个钩子
final RuntimeContext ctx = getRuntimeContext();
ctx.registerUserCodeClassLoaderReleaseHookIfAbsent(
KINESIS_PRODUCER_RELEASE_HOOK_NAME,
()-> this.runClassLoaderReleaseHook
(ctx.getUserCodeClassLoader()));
卸载用户代码中动态加载的类,所有涉及动态用户代码类加载(会话)的场景都依赖于再次卸载的类。
类卸载指垃圾回收器发现一个类的对象不再被引用,这时会对该类(相关代码、静态变量、元数据等)进行移除。
当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()
方法进行注册。
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();
}
}
首先我们需要上传Jar包,整体流程如下图所示:
● Yarn Perjob
提交任务的时候上传 jar 包,会放到
hd@fs://flink03:9000/@user/root/.flink/@application_1654762357754_0140。
● Yarn Session
启动 Session 的时候,Yarn 的 App 上传 Jar 包机制,往 Session 提交任务的时候,Flink 的 Blob Server 负责收。
分布式缓存机制是由各个NM实现的,主要功能是将应用程序所需的文件资源缓存到本地,以便后续任务的使用。资源缓存是用时触发的,也就是第一个用到该资源的任务触发,后续任务无需再进行缓存,直接使用即可。
根据资源类型和资源可见性,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资源
存放在 y a r n . n o d e m a n a g e r . l o c a l − d i r s / u s e r c a c h e / {yarn.nodemanager.local-dirs}/usercache/ yarn.nodemanager.local−dirs/usercache/{user}/filecache/目录下,(其中${user}是应用程序提交者,默认情况下均为NodeManager启动者),每个资源将单独存放在以一个随机整数命名的目录中,且目录的访问权限均为0710。
● Application资源
存放在 y a r n . n o d e m a n a g e r . l o c a l − d i r s / u s e r c a c h e / {yarn.nodemanager.local-dirs}/usercache/ yarn.nodemanager.local−dirs/usercache/{user}/ a p p c a c h e / {appcache}/ appcache/{appid}/filecache/目录下(其中${appid}是应用程序ID),每个资源将单独存放在以一个随机整数命名的目录中,且目录的访问权限均为0710;
其中Container的工作目录位于 y a r n . n o d e m a n a g e r . l o c a l − d i r s / u s e r c a c h e / {yarn.nodemanager.local-dirs}/usercache/ yarn.nodemanager.local−dirs/usercache/{user}/ a p p c a c h e / {appcache}/ appcache/{appid}/${containerid}目录下,其主要保存jar包文件、字典文件对应的软链接。
Flink libs 下面 jar包、Flink Plugins 下面的 jar 包、Flink 任务的 jar 包(对于 ChunJun 来说就是所有 connector 和 core), Flink jar 用户自定义 jar 包。
● Perjob
如果可以提前上传到 HDFS:
如果不可以提前上传到 HDFS:
● Seeion
如果可以提前上传到 HDFS:
如果不可以提前上传到 HDFS:
● 思路分析
● 遇到的问题
Flink 一个 job 可能有多个算子,一个 connector 就是一个算子。Flink 原生是为 job 级别新生成的 Classloader,无法把每个 connector 放在一个独立的 Classloader 里面。
child-first 加载策略在 Session 模式下每次都新 new 一个 Classloader,导致元数据空间内存泄露。
connecotor 之间用到公有的类会报错。
和问题2类似,主要是因为有些线程池,守护线程会拿着一些类对象,或者类 class 对象的引用。
如果用原生 -yarnship 去上传,会放到 App Classloader 里面。那么就会导致某些不期望用 App Classloader 加载的类被加载。
/** 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();
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 插件。
亲测,真的嘎嘎好用,我几乎99%的jar包冲突都能解决,
2.Jstack
死锁的一些问题可以通过这个工具查看 jstack 调用栈。
3.Arthas
排查一些性能问题和 Classloader 泄露问题。
4.VisualVM
排查一些对象内存泄露、dump 文件分析等。