背景
hive 用的 1.1.0版本(其实这个版本bug挺多,包括执行计划串列的等等问题吧,建议大家如果选1.x版本用1.2.2吧),一下提到的代码部分如无特殊说明都是hive-1.1.0版本。
前段时间写一个hive sql 预估资源的服务(根据sql返回其读取的行数及所读文件(表)的大小,在运行时给其指定合理资源的大小,目前我们把线上所有hql转到sparksql上执行,所以要指定其资源),其实之前一直利用hiveserver2(以下hiverserver2都用hs2替代)完成。
hs2比较轻量简单,但是hs2服务不是特别稳定,用过的人都知道(包括卡死,对hdfs敏感,对avro类型文件解析也时常有问题)。
经常出现接口服务超时问题。其实有这些问题也可以暂时忍一忍,但是前段时间出现有些sql可以导致整个接口不可用的情况,实在不可忍啦,就决定重写了。然后就掉进坑了啦!
上线问题排查,定位问题sql,排查卡死原因
首先根据后台日志找到了卡死服务的sql,然后就用这个sql debug呗,sql也较为复杂(大概600-700行左右)最后定位到应该是optimizer的问题。
这个优化器大概是做什么的呢,包括join 优化呀,谓词下推呀等等十来种吧(以后我会写一篇hive Driver 执行流程的文章,会详细介绍这部分内容。)
但是为什么他会影响其他sql呢,其实hive 在compile方法加了全局锁。这个锁是Driver 类的一个全局静态锁,所以相当于
private static Object mutex = new Object();
...
synchronized(mutex){
driver.compile(sql)
}
...
这个sql 卡在了 complie中的optimizer中了,死循环了,无法释放mutex锁对象了,然后就崩盘喽。
当时跟老大说了一下这个情况,我的第一反应是继承Driver 重写compile方法解决问题,老大的第一反应是用反射解决。(我后来详细看了下代码,用反射也可以解决,虽然我们要修改的变量不仅是private 还是final的,但是我们把他反射出来改变他的内容还是没有问题的)
然后我就开始尝试先用我的方案解决,继承Driver重写compile,然后发现比较困难,为什么呢,hive源码中用了相当多的私有方法及变量,方法层层调用也较多,如果重写compile就要重写茫茫多的方法及变量,如果升级hive版本也是麻烦事,可能这些个重写都要再写一遍,不是不能实现就是太恶心了。pass
然后我就想到了能不能用停止线程的方式来做,就是这个线程执行时间太长了,把他停止,让他把锁释放掉,让其他线程进来执行任务。
停止线程的方式可以参见我得另一篇文章 --------------------------------------------------------- 这里不多说了。
其实停止线程不是不行(是有间接方案的),但是你要明确知道你代码在哪里死循环出不来了,可以间接通过thread.interrupted()判断 抛异常或者return的方式跳出,但是由于代码不是我们自己写的, 你无法确定所有的sql是不是都在某个optimizer卡住。这要改起来就非常恶心了,到处是判断,而且修改完要重新编译。pass
然后我灵机一动偷偷看了hive1.2.x的实现,发现问题依旧,没有解决。又看了眼hive 2.0.0实现解决了,我凑 我凑 我凑 ,赶紧看看怎么解决的。这可以看我另一篇文章 ---------- 哈哈哈...
然后简单啊,就把pom中hive版本修改到2.0.0 ,在线下测试了3000个sql 没啥问题,上线,一晚相安无事,早上起来发现接口报警了,超时了,我凑,不是解决了吗,怎么回事,登陆服务器一看,full gc严重 old区 去都99%了,full gc 都无法让old对象减少,把堆栈dump出来,估计版本出现大坑了,重启了一下开始排查问题。
如果分析dump出来的堆栈信息 可以看我这篇 https://www.cnblogs.com/jiangxiaoxian/p/9559813.html 文章。
我说结果吧,发现ShutdownHookManager 类的全局变量的set<HookEntry> hookEntry其实是对runnable对象的封装。hive2.0.0实现与 hive1.x不一样,他为了做超时锁,引入了ShutdownHookManager 类,而且用全局的Set维护每个线程对象runnable,每个线程进来都会先remove掉上一个driver 的runnable,然后add添加当前的runnable对象(这里说的不是太准确,我为了简化问题描述吧),但是我的实现是每次new Driver() (driver类成员有runnable 对象), 导致remove不掉上一个driver的runnable对象,现在已经知道问题答案了,看起来挺简单,排查起来还是花了一些时间的。我当时是用反射确定Set <HookEntry> 无法remove的,他的size 一直在增长,具体怎么玩呢。
我是用dump分析定位问题出在哪里,用反射确定问题就在这里!
大家可能看不太懂以下代码,主要是hive 这部分源码变量大家不熟悉,有兴趣可以看下,这里还是比较简单的。
try {
Class<?> clazz = Class.forName("org.apache.hive.common.util.ShutdownHookManager");
// Class<?> clazz = Class.forName("org.apache.hadoop.hive.ql.Driver.class");
// Field hooks = clazz.getDeclaredField("hooks");
Field mgr = clazz.getDeclaredField("MGR");
// hooks.setAccessible(true);
mgr.setAccessible(true);
ShutdownHookManager shutdownHookManager = (ShutdownHookManager) mgr.get(ShutdownHookManager.class);
set = (Set) hooks.get(shutdownHookManager);
System.out.println(set.size)
} catch (Exception e) {
e.printStackTrace();
}
最后的解决方式是其实是用Driver 的destory方法,可以清理掉他自己runnable对象,虽然当时也看到这个方法了,但是是在driver.close()后调用的,没生效,必须在close()他之前调用。close()主要是关闭driver一些相关资源的,destory可以remove掉runnable对象。
文章写得有些啰嗦,主要是想表达我遇到问题整个思考的过程,及解决问题的方式。在此感谢老大则杰在问题排查过程中给的宝贵建议!this`s all