1. 背景
由于业务需求驱动,财务系统需要定时从库存事物数据库,及订单接口,价格接口等数据库实例或接口系统获取数据并按设定的动态逻辑规则生成账单及明细.面对复杂,海量数据来源以及灵活的多数据库分库实例,在众多的数据处理框架中,我们决定使用Spark框架来处理海量账单数据.
库存事物相关数据库表以及账单相关表是按各自分库维度存放在各自上百个 MYSQL 数据分库实例上. 其中读取库存事物分库是通过Spark自管理的JDBC方式读取,而写入账单分库是我们手工通过JDBC写入(使用DRUID连接池).
2. 应用版本
- spark : 2.1.0
- mysql : 5.7
- jdk : 1.8
- hadoop: 2.5.3
3. 问题
在业务正式上线后一段时间后,发现每两三个星期会出现一次Spark任务Hold住的情况,于是打开Spark UI跟踪分析这个问题,结果在Executor的Thread dump中发现了BLOCKED的线程,且久久不能释放.
spark_ui_lock.jpg
图1
4. 问题分析
4.1 Spark线程锁的分析
从 图1 上可以看出, Thread ID : 125 , Thread ID:139 的线程在DriverRegistry$.register方法的45行BLOCKED住,而 Thread ID:127 线程一直HOLD 在RUNNABLE状态.
而从栈信息上看很可能是在 DriverRegistry$.register方法46行处等待资源中.
于是打开Spark DriverRegistry类源代码,其register代码如下所示
结合Spark源代码分析,初步可以推断出:
线程 Thread ID:127 在静态方法 register(className:String) 的 val wrapper = new DriverWrapper(cls.newInstance().asInstanceOf[Driver]) 可能处于等待状态,
而线程 Thread ID:125 , Thread ID:139 要执行 register(className:String) 方法的45行时,因为 Thread ID:127 持有的 synchronized代码块 的锁没有释放,所以一直处于BLOCKED状态.
4.2 Spark 与 Druid 互锁分析
接下来的疑问是: 为什么 Thread ID:127 会处于等待状态呢?
顺着 图1 继续往下看,发现 Thread ID:128 在 com.alibaba.druid.proxy.DruidDriver.registerDriver(DruidDriver.java:92) 处也是处于 RUNNABLE 状态.
其源代码如下:
public class DruidDriver implements Driver, DruidDriverMBean {
static {
AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object More ...run() {
registerDriver(instance);
return null;
}
});
}
public static boolean registerDriver(Driver driver) {
...
DriverManager.registerDriver(driver); //92行
...
}
}
4.3 DriverManager代码死锁分析
从前面已知的信息来看,解决我们疑问的只能在com.mysql.jdbc.Driver, DriverManager 类中找,接下来我们看一下这两个类的源代码
driver_code.jpg因此我们目前的线程栈信息上看,Spark 的 Thread ID:127 获取加载了mysql驱动并初始化DriverManager类,而DriverManager类初始化时会执行loadInitialDrivers(),并且这个方法会加载当前classPath下/META-INF/services/java.sql.Driver文件中的类(这个文件中包含有com.mysql.jdbc.Driver及其他数据库驱动).
这时候 Thread ID 128 通过Druid连接池获取连接时,刚持有DriverManager类型的锁,
要等待Spark Thread ID:127 加载完DriverManager类,而 Thread ID:127 类又需要使用DriverManager类来加载driver class时,要等待 Thread ID 128 释放DriverManager类型锁,这样两个线程就互锁了。
5. 问题解决方法
解决的方法是让驱动类的加载过程变为单线程加载方式.在我们代码中调用Druid连接池获取Connection之前,串行执行
DriverRegistry.register("com.mysql.jdbc.Driver");
6. 参考资料
具体JDBC驱动类初始化死锁问题
可以参考 你假笨 的 JDK的sql设计不合理导致的驱动类初始化死锁问题
网友评论