调试类加载
Flink中的类加载概述
运行Flink应用程序时,JVM将随着时间的推移加载各种类。这些类可以分为两个域:
在Java类路径:这是Java类路径中常见的,它包括了JDK库,所有代码Flink的
/lib
文件夹(阿帕奇Flink及其核心依赖的类)。该动态用户代码:这些是包含在动态提交的作业,的JAR文件的所有类(通过REST,CLI,web用户界面)。它们按工作动态加载(和卸载)。
哪些类属于哪个域取决于运行Apache Flink的特定设置。作为一般规则,无论何时首先启动Flink进程和提交作业,都会动态加载作业的类。如果Flink进程与作业/应用程序一起启动,或者应用程序生成Flink组件(JobManager,TaskManager等),则所有类都在Java类路径中。
以下是有关不同部署模式的更多详细信息:
独立会话
将Flink集群作为独立会话启动时,JobManagers和TaskManagers将使用Java类路径中的Flink框架类启动。来自会话(通过REST / CLI)提交的所有作业/应用程序的类都是动态加载的。
Docker / Kubernetes Sessions
Docker / Kubernetes设置首先启动一组JobManagers / TaskManagers,然后通过REST或CLI提交作业/应用程序,就像独立会话一样:Flink的代码在Java类路径中,作业的代码是动态加载的。
YARN
YARN类加载在单个作业部署和会话之间有所不同:
当直接向YARN(via
bin/flink run -m yarn-cluster …
)提交Flink作业/应用程序时,将为该作业启动专用TaskManagers和JobManagers。这些JVM在Java类路径中同时具有Flink框架类和用户代码类。这意味着在这种情况下不涉及动态类加载。在启动YARN会话时,JobManagers和TaskManagers将使用类路径中的Flink框架类启动。针对会话提交的所有作业的类都是动态加载的。
Mesos
遵循此文档的 Mesos设置目前非常类似于YARN会话:TaskManager和JobManager进程使用Java类路径中的Flink框架类启动,作业类在提交作业时动态加载。
反向类加载和类加载器分辨率顺序
在涉及动态类加载(会话)的设置中,通常有两个ClassLoader的层次结构:(1)Java应用程序类加载器,它具有类路径中的所有类,以及(2)动态用户代码类加载器。用于从用户代码jar加载类。用户代码ClassLoader将应用程序类加载器作为其父代。案例。
默认情况下,Flink反转类加载顺序,这意味着它首先查看用户代码类加载器,并且只查看父类(应用程序类加载器),如果该类不是动态加载的用户代码的一部分。
反向类加载的好处是作业可以使用与Flink核心本身不同的库版本,这在库的不同版本不兼容时非常有用。该机制有助于避免常见的依赖冲突错误,如IllegalAccessError
或NoSuchMethodError
。代码的不同部分只是具有类的单独副本(Flink的核心或其中一个依赖项可以使用与用户代码不同的副本)。在大多数情况下,这种方法运行良好,无需用户进行其他配置。
但是,有些情况下反向类加载会导致问题(参见下文“X不能转换为X”)。您可以通过在Flink配置中通过classloader.resolve-order配置ClassLoader解析顺序parent-first
(从Flink的默认设置child-first
)恢复到Java默认模式。
请注意,某些类总是以父对象的方式解析(首先通过父ClassLoader),因为它们在Flink的核心和用户代码或面向API的用户代码之间共享。这些类的包通过classloader.parent-first-patterns-default和classloader.parent-first-patterns-additional配置。要添加要以父级优先加载的新软件包,请设置classloader.parent-first-patterns-additional
config选项。
避免动态类加载
所有组件(JobManger,TaskManager,Client,ApplicationMaster,…)在启动时记录其类路径设置。它们可以在日志开头的环境信息中找到。
当运行Flink JobManager和TaskManagers专用于某个特定作业的设置时,可以将JAR文件直接放入该/lib
文件夹中,以确保它们是类路径的一部分而不是动态加载。
它通常用于将作业的JAR文件放入/lib
目录中。JAR将成为类路径(AppClassLoader)和动态类加载器(FlinkUserCodeClassLoader)的一部分。因为AppClassLoader是FlinkUserCodeClassLoader的父级(并且默认情况下Java加载父级优先),所以这应该导致只加载一次类。
对于无法将作业的JAR文件放入/lib
文件夹的设置(例如,因为安装程序是多个作业使用的会话),可能仍然可以将公共库放入该/lib
文件夹,并避免为这些文件夹加载动态类。
在作业中手动加载类
在某些情况下,转换函数,源或接收器需要手动加载类(通过反射动态加载)。要做到这一点,它需要可以访问作业类的类加载器。
在这种情况下,函数(或源或接收器)可以成为RichFunction
(例如RichMapFunction
或RichWindowFunction
)并通过访问用户代码类加载器getRuntimeContext().getUserCodeClassLoader()
。
X不能转换为X异常
在使用动态类加载的设置中,您可能会在样式中看到异常com.foo.X cannot be cast to com.foo.X
。这意味着该类的多个版本com.foo.X
已由不同的类加载器加载,并且尝试将该类的类型彼此分配。
一个常见原因是库与Flink的反向类加载方法不兼容。您可以关闭反向类加载以验证这一点(classloader.resolve-order: parent-first
在Flink配置中设置)或从反向类加载中排除库(classloader.parent-first-patterns-additional
在Flink配置中设置)。
另一个原因可能是缓存的对象实例,如某些库(如Apache Avro)或实习对象(例如通过Guava的Interners)生成的。这里的解决方案是要么没有任何动态类加载的设置,要么确保相应的库完全是动态加载代码的一部分。后者意味着库不能添加到Flink的/lib
文件夹中,但必须是应用程序的fat-jar / uber-jar的一部分
卸载动态加载的类
涉及动态类加载(会话)的所有方案都依赖于再次卸载的类。类卸载意味着垃圾收集器发现类中没有对象存在且更多,因此删除了类(代码,静态变量,元数据等)。
每当TaskManager启动(或重新启动)任务时,它都会加载该特定任务的代码。除非可以卸载类,否则这将成为内存泄漏,因为加载了新版本的类,并且加载的类的总数随时间累积。这通常通过OutOfMemoryError:Metaspace表现出来。
类泄漏的常见原因和建议的修复:
延续线程:确保应用程序函数/ sources / sinks关闭所有线程。延迟线程本身会花费资源,另外通常还包含对(用户代码)对象的引用,从而防止垃圾收集和卸载类。
Interners:避免在超出函数/源/接收器生命周期的特殊结构中缓存对象。示例是Guava的interner,或序列化程序中Avro的类/对象缓存。
使用maven-shade-plugin解决与Flink的依赖冲突。
A到解决从应用程序开发者方面的依赖性冲突的方法是避免通过暴露的依赖阴影他们离开。
Apache Maven提供maven-shade-plugin,它允许在编译后更改类的包(因此您编写的代码不受着色影响)。例如,如果你com.amazonaws
的用户代码jar中有来自aws sdk的org.myorg.shaded.com.amazonaws
软件包,那么shade插件会将它们重定位到软件包中,这样你的代码就会调用你的aws sdk版本。
本文档页面介绍了使用shade插件重定位类。
请注意,大多数Flink的依赖,如guava
,netty
,jackson
,等被Flink的维护者阴影了,所以用户通常不必担心。