第二次提交Flink作业报No suitable driver found for jdbc

这个BUG是这样的,在flink集群测试(session mode)实时把pulsar的数据写入TDengine的时候,第一次提交任务可以运行成功,然后把任务cancel掉后,再次启动就会报No suitable driver found for jdbc:TAOS://hadoop001:6030?user=root&password=taosdata错误,导致任务提交失败,后续提交任务都会失败,报同样的错误。windows本地环境和linux本地环境都没有问题。

一开始思考问题的方向是:

  1. jar包没打进去
  2. Tdengine的客户端和服务端版本不一致
  3. jdbc的url路径写的不对
  4. driver驱动的版本不一致
  5. driver类没有加载进去
  6. flink环境问题
  7. 手动Cancel后没有调用Flink的Sink方法

在一个个都验证过之后发现都不行,好几天都毫无进展,直到我手动Class.forname的时候,出现了一个Native Library xxx is being loaded in another classloader的异常信息。

我开始换一个方向尝试解决这个Bug:

  1. 之前Driver没卸载,尝试手动卸载
  2. 看看有没有办法卸载之前的Driver对应的类加载器(我想太多,只能通过垃圾回收器同意下载)

一同尝试之后还是不行。

最后通过查阅资料,发现Flink为了解决版本的兼容性问题,没有使用双亲委派模型加载Class,用户代码中的类先由 Flink 框架的类加载器加载,再由用户代码的类加载器加载。

这是 FLink 的类加载器继承结构:
在这里插入图片描述

FlinkUserCodeClassLoader 继承自 URLClassLoader 类,其 loadClass() 方法实现如下:

@Override
public final Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
	try {
		synchronized (getClassLoadingLock(name)) {
			return loadClassWithoutExceptionHandling(name, resolve);
		}
	} catch (Throwable classLoadingException) {
		classLoadingExceptionHandler.accept(classLoadingException);
		throw classLoadingException;
	}
}

protected Class<?> loadClassWithoutExceptionHandling(String name, boolean resolve)
		throws ClassNotFoundException {
	// 本质上还是调用的 ClassLoader 的 loadClass() 方法
	// 即 FlinkUserCodeClassLoader 仍然遵循双亲委派模型
	return super.loadClass(name, resolve);
}

FlinkUserCodeClassLoader 有2个子类,分别为 ParentFirstClassLoader 和 ChildFirstClassLoader。

public static class ParentFirstClassLoader extends FlinkUserCodeClassLoader {

	ParentFirstClassLoader(
			URL[] urls, ClassLoader parent, Consumer<Throwable> classLoadingExceptionHandler) {
		// 直接复用父类 FlinkUserCodeClassLoader 的相关方法逻辑
		super(urls, parent, classLoadingExceptionHandler);
	}

	static {
		ClassLoader.registerAsParallelCapable();
	}
}

可以看到,Parent-First 类加载策略照搬双亲委派模型,也就是说,用户代码的类加载器是User ClassLoader,Flink 框架本身的类加载器是 Application ClassLoader,用户代码中的类先由 Flink 框架的类加载器加载,再由用户代码的类加载器加载。

双亲委派模型的好处是随着加载器的层次关系保证了被加载类的层次关系,从而保证了 Java 运行环境的安全性。但是在 Flink App 这种依赖纷繁复杂的环境中,双亲委派模型可能并不适用。例如,程序中引入的 Flink-Kafka Connector 总是依赖于固定的 Kafka 版本,用户代码中为了兼容实际使用的 Kafka 版本,会引入一个更低或更高的依赖。而同一个组件不同版本的类定义可能会不同(即使类的全限定名是相同的),如果仍然用双亲委派模型,就会因为 Flink 框架指定版本的类先加载,而出现莫名其妙的兼容性问题,如 NoSuchMethodError、IllegalAccessError等。

鉴于此,Flink 实现了 ChildFirstClassLoader 类加载器并作为默认策略,它打破了双亲委派模型,使得用户代码的类先加载,官方文档中将这个操作称为 “Inverted Class Loading”。

ChildFirstClassLoader

ChildFirstClassLoader 是通过覆写 FlinkUserCodeClassLoader 的 loadClassWithoutExceptionHandling() 方法来实现 Child-First 逻辑的。

@Override
protected Class<?> loadClassWithoutExceptionHandling(String name, boolean resolve)
		throws ClassNotFoundException {

	// 首先调用 findLoadedClass 方法检查全限定名 name 对应的类是否已加载过
	Class<?> c = findLoadedClass(name);

	if (c == null) {
		// 检查要加载的类是否以 alwaysParentFirstPatterns 集合中的前缀开头。如果是,则调用父类的 loadClassWithoutExceptionHandling 方法,以 Parent-First 的方式加载它
		for (String alwaysParentFirstPattern : alwaysParentFirstPatterns) {
			if (name.startsWith(alwaysParentFirstPattern)) {
				return super.loadClassWithoutExceptionHandling(name, resolve);
			}
		}

		try {
			// 若加载的类不以 alwaysParentFirstPatterns 集合中的前缀开头,则调用父类 URLClassLoader 的 findClass 方法进行类加载
			c = findClass(name);
		} catch (ClassNotFoundException e) {
			// 若调用 findClass() 方法失败
			// 最终再调用父类的 loadClassWithoutExceptionHandling 方法,以 Parent-First 的方式加载它
			c = super.loadClassWithoutExceptionHandling(name, resolve);
		}
	} else if (resolve) {
		resolveClass(c);
	}

	return c;
}

可见,用户如果仍然希望某些类"遵循祖制",采用双亲委派模型来加载,则需要借助 alwaysParentFirstPatterns 集合来实现。

而该集合主要由以下2个参数来指定:

  • classloader.parent-first-patterns.default,默认值如下(该值一般不推荐修改):
@Documentation.Section(Documentation.Sections.EXPERT_CLASS_LOADING)
public static final ConfigOption<List<String>> ALWAYS_PARENT_FIRST_LOADER_PATTERNS =
		ConfigOptions.key("classloader.parent-first-patterns.default")
				.stringType()
				.asList()
				.defaultValues(
						ArrayUtils.concat(
								new String[] {
									"java.",
									"scala.",
									"org.apache.flink.",
									"com.esotericsoftware.kryo",
									"org.apache.hadoop.",
									"javax.annotation.",
									"org.xml",
									"javax.xml",
									"org.apache.xerces",
									"org.w3c",
									"org.rocksdb."
								},
								PARENT_FIRST_LOGGING_PATTERNS))
				.withDeprecatedKeys("classloader.parent-first-patterns")
				.withDescription(
						"A (semicolon-separated) list of patterns that specifies which classes should always be"
								+ " resolved through the parent ClassLoader first. A pattern is a simple prefix that is checked against"
								+ " the fully qualified class name. This setting should generally not be modified. To add another pattern we"
								+ " recommend to use \"classloader.parent-first-patterns.additional\" instead.");
								
@Internal
public static final String[] PARENT_FIRST_LOGGING_PATTERNS =
		new String[] {
			"org.slf4j",
			"org.apache.log4j",
			"org.apache.logging",
			"org.apache.commons.logging",
			"ch.qos.logback"
		};

  • classloader.parent-first-patterns.additional

用户如果仍然希望某些类"遵循祖制",采用双亲委派模型来加载,则可以通过该参数额外指定,并以分号分隔。

@Documentation.Section(Documentation.Sections.EXPERT_CLASS_LOADING)
public static final ConfigOption<List<String>> ALWAYS_PARENT_FIRST_LOADER_PATTERNS_ADDITIONAL =
		ConfigOptions.key("classloader.parent-first-patterns.additional")
				.stringType()
				.asList()
				.defaultValues()
				.withDescription(
						"A (semicolon-separated) list of patterns that specifies which classes should always be"
								+ " resolved through the parent ClassLoader first. A pattern is a simple prefix that is checked against"
								+ " the fully qualified class name. These patterns are appended to \""
								+ ALWAYS_PARENT_FIRST_LOADER_PATTERNS.key()
								+ "\".");

若有需要,在 conf/flink-conf.yaml 中配置 classloader.parent-first-patterns.additional即可。

Flink 会将上述2种配置合并在一起,作为 alwaysParentFirstPatterns 集合:

public static String[] getParentFirstLoaderPatterns(Configuration config) {
	List<String> base = config.get(ALWAYS_PARENT_FIRST_LOADER_PATTERNS);
	List<String> append = config.get(ALWAYS_PARENT_FIRST_LOADER_PATTERNS_ADDITIONAL);
	return mergeListsToArray(base, append);
}

基于这种情况,想到了两种解决方案:

第一种:使用 单作业模式提交flink作业
第二种:修改flink配置文件,使得com.taosdata.jdbc.路径下的类加载的时候通过双亲委派机制加载,这样只有一个类加载器了。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
Flink 是一个流处理框架,支持在流处理任务中使用事务。而在事务处理中,最常用的是 2PC (Two-Phase Commit) 协议。Flink 在实现 2PC 时,通常需要和外部存储系统协作,比如 MySQL、Kafka 等。 对于 MySQL,Flink 提供了官方的 JDBC Connector,可以通过 JDBC 连接 MySQL 数据库。而在使用 JDBC Connector 时,需要使用 Flink 提供的实现了 2PC 协议的 SinkFunction,即 TwoPhaseCommitSinkFunction。 TwoPhaseCommitSinkFunction 的代码实现中,会在 preCommit() 方法中打开 MySQL 的事务,并在 invoke() 方法中执行数据插入操作。如果数据插入成功,会调用 preCommit() 方法的 prepareCommit() 方法来提交事务。如果数据插入失败,则会调用 preCommit() 方法的 abort() 方法来回滚事务。 需要注意的是,如果在 preCommit() 方法中打开了事务,但在 invoke() 方法中抛出了异常,则 TwoPhaseCommitSinkFunction 会自动回滚事务。也就是说,在 TwoPhaseCommitSinkFunction 中不需要显式地处理异常。 总体来说,使用 Flink 实现 2PC 协议提交 MySQL 数据库的过程,可以分为以下几个步骤: 1. 实现 TwoPhaseCommitSinkFunction 接口,并实现其中的 preCommit()、commit()、abort() 方法; 2. 在 preCommit() 方法中打开 MySQL 的事务; 3. 在 invoke() 方法中执行数据插入操作; 4. 如果数据插入成功,调用 preCommit() 方法的 prepareCommit() 方法来提交事务; 5. 如果数据插入失败,则会自动回滚事务。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值