Flink中的类加载机制

Flink中的类加载机制

Flink中的类加载配置项

Flink中关于类加载有以下配置选项

配置项默认值说明
classloader.resolve-order“child-first”类加载顺序。child-first优先从Flink任务(jar包)中加载类,parent-first优先从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”优先从Flink集群加载的类,以分号分隔的类前缀匹配
classloader.parent-first-patterns.additional(none)额外需要优先从Flink集群加载的类
classloader.fail-on-metaspace-oom-errortrue如果尝试加载用户代码类时抛出“OutOfMemoryError:Metaspace”,则Flink JVM进程失败。

类图关系

在这里插入图片描述

FlinkUserCodeClassLoader是一个继承了URLClassLoader的抽象类,主要是约束了其子类加载器在进行类加载的一些行为。

/**
 * This class loader accepts a custom handler if an exception occurs in {@link #loadClass(String, boolean)}.
 */
public abstract class FlinkUserCodeClassLoader extends URLClassLoader {
	
  // 加载类出现异常时的handler,默认不作处理
  public static final Consumer<Throwable> NOOP_EXCEPTION_HANDLER = classLoadingException -> {};

  // 指定一个类加载异常处理handler
	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;
	}

  /**
   * 实现FlinkUserCodeClassLoader的子类不需要重写loadClass方法,只需要重写loadClassWithoutExceptionHandling方法。
   * loadClassWithoutExceptionHandling只负责类加载的具体实现,
   * 加载类时出现的异常会被捕获并交由类加载异常处理handler进行处理,并再次向上抛
   *
   */
	@Override
	protected final Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
		try {
			return loadClassWithoutExceptionHandling(name, resolve);
		} catch (Throwable classLoadingException) {
			classLoadingExceptionHandler.accept(classLoadingException);
			throw classLoadingException;
		}
	}

	/**
	 * Same as {@link #loadClass(String, boolean)} but without exception handling.
	 *
	 * <p>Extending concrete class loaders should implement this instead of {@link #loadClass(String, boolean)}.
	 */
	protected Class<?> loadClassWithoutExceptionHandling(String name, boolean resolve) throws ClassNotFoundException {
		return super.loadClass(name, resolve);
	}
}

parent-first类加载

parent-first的含义就是优先从Flink集群加载类,如果没有该类就从用户的jar包中加载类。

ParentFirstClassLoader类加载器的实现特别简,说白了就是一个有FlinkUserCodeClassLoader特性的URLClassLoader,通过指定的URL加载类文件查找对应的类。

/**
 * Regular URLClassLoader that first loads from the parent and only after that from the URLs.
 */
static class ParentFirstClassLoader extends FlinkUserCodeClassLoader {

   ParentFirstClassLoader(URL[] urls, ClassLoader parent, Consumer<Throwable> classLoadingExceptionHandler) {
      super(urls, parent, classLoadingExceptionHandler);
   }
}

那么,ParentFirstClassLoader是如何保证同一个Flink的类,优先加载集群的而非用户jar包中的?其实这里是利用Java的类加载机制——双亲委派模型。

在这里插入图片描述

我们知道用户提交的Flink任务运行于TaskManager进程内,TaskManager运行时的类加载器是Application ClassLoader(用户程序运行时的默认类加载器),当我们使用自定义类加载器加载需要的类时,会自底向上逐一查找,如果ParentFirstClassLoader中有这个类就再次从Application ClassLoader中查找这个类,如果Application ClassLoader中也有这个类,就会继续从Extension ClassLoader中查找,直到父加载器加载不到这个类时,才会使用当前类加载器加载这个类。这样就巧妙的利用双亲委派模型实现了parent-first的类加载机制。

child-first类加载

接着我们来看child-first是如何打破双亲委派机制的。

不少人应该都用过Tomcat,Tomcat中可以部署多个Java程序,每个Java程序都包含了各自不同版本的jar包,但是他们却能各自正常的工作。同样地,同一个Flink集群也需要运行各种各样的Flink任务,而且如果用户使用的某个jar和Flink集群的jar版本不一致,需要优先加载用户的jar怎么办?Flink的ChildFirstClassLoader类加载提供了优先从用户jar包加载类的机制。

/**
 * A variant of the URLClassLoader that first loads from the URLs and only after that from the parent.
 *
 * <p>{@link #getResourceAsStream(String)} uses {@link #getResource(String)} internally so we
 * don't override that.
 */
public final class ChildFirstClassLoader extends FlinkUserCodeClassLoader {

	/**
	 * 总是从父加载器加载的类,如以org.apache.flink开头的类等,这样所有任务都使用同一版本的link基础库
	 * The classes that should always go through the parent ClassLoader. This is relevant
	 * for Flink classes, for example, to avoid loading Flink classes that cross the
	 * user-code/system-code barrier in the user-code ClassLoader.
	 */
	private final String[] 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) {
      // 总是从父加载器加载的类交由父加载器加载,ChildFirstClassLoader跳过加载这些类
			// 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
        // 如果当前类加载其中找不到这个类那只能交由父类加载器从Flink集群环境中查找了
				c = super.loadClassWithoutExceptionHandling(name, resolve);
			}
		}

		if (resolve) {
			resolveClass(c);
		}

		return c;
	}

  /** 下面两个方法都是优先从子类中获取资源文件 **/
	@Override
	public URL getResource(String name) {
		...
	}

	@Override
	public Enumeration<URL> getResources(String name) throws IOException {
		...
	}
}

这里alwaysParentFirstPatterns就是classloader.parent-first-patterns.defaultclassloader.parent-first-patterns.additional配置的类前缀集合,即这两个配置项中的类就算使用child-first类加载方式,也会从父加载器中加载,而非用户jar包中加载。这也是为什么Flink官网的例子中,flink相关的jar在pom引入的时候scope都采用了provided,既减少了用户jar包的大小,又能在集群上正常运行,最主要的是如果集群更新了版本,程序可以直接享受更新后的功能,而不用重新打包。

类冲突处理

上面介绍了Flink中的类加载机制,实际开发中可能会因为类加载出现ClassNotFound或者NoSuchMethodError和NoSuchFieldError等异常。这种情况可能是因为你的jar包中的版本和集群的版本不一致,优先加载了集群的class。在不改动集群默认配置的情况下,同时也防止对其他任务的影响,可以考虑对项目中引入的冲突类作shade。如com.typesafe的类发生冲突,maven项目加入如下配置:

<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-shade-plugin</artifactId>
				<executions>
					<execution>
						<phase>package</phase>
						<goals>
							<goal>shade</goal>
						</goals>
						<configuration>
							
							<relocations>
								<relocation>
									<pattern>com.typesafe</pattern>
									<shadedPattern>com.mycompany.com.typesafe</shadedPattern>
								</relocation>
							</relocations>
							
						</configuration>
					</execution>
				</executions>
			</plugin>

项目打包后,就会自动把引入和源码中相关的com.typesafe开头的类修改为com.mycompany.com.typesafe,由于类的路径不同,类加载器在加载用户jar中的类时就不会与集群中的其他版本冲突。

  • 6
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值