场景描述
我们在工作中有时候需要使用JDBC操作Hive,但最近经常出现每隔一段时间JDBC就超时没反应的情况。(这个问题和MetaStore内存溢出时的表现一模一样,关于MetaStore长时间没有响应的解决方案可以看我的另一篇文章中的第三个问题)
问题追溯
从以前的历史教训中,潜意识里第一个操作就是dump HiveServer2的内存模型,然后MAT去分析。但是因为初次发现此问题的时候为了生产环境尽快恢复,让运维去直接重启了,重新启动后的JVM去做dump没什么意义。而生产环境没有配置OOM时dump内存,所以就想了个笨方法:每天dump一次HiveServer2,直到发现OOM。
前天又发生一次,下图是MAT分析的当天的dump结果中的leak report
从这个图为线索去看HiveServer2的源码就可以找到问题的根源了。总体的类图如下
每个JDBC请求打开连接时,都是下面这个路径:把会话存储在SessionManager的全进程唯一实例里,而SessionManager与OperationManager又互相引用(这个对象也是全进程唯一的),每个会话里的每次操作都是通过OperationManager来存储的,Driver对象就在OperationManager内部的HashMap里。当JDBC客户端close的时候,把会话与会话的操作从HashMap里删除掉。
如果客户端的机器遇到极端情况(比如直接断电,然后重启服务)会不会出现内存溢出呢?答案是理论上不会,因为HiveServer2在启动的时候开启了一个线程,以固定的频率(默认是6小时)去检查超过固定时间无交互的会话(默认是7天),如果存在就直接关闭了。代码如下:
为什么说理论上不会呢?我们考虑这个场景,HiveServer2内存假设1个G,每个会话需要内存100M,客户端需要长期维护5个会话,那么粗略计算,当前HiveServer2最多允许10个会话,当出现客户端断电又重连的情况,前一次运行中的会话未关闭,且未被定时线程清理(因为要等6小时触发一次,即使判断为需要清理也还要再等7天),再加上第二次启动的5个会话,内存就已经到OOM的边缘了,此时JVM一定在疯狂FullGC,客户端表现就是啥都干不了,最后超时了。
触发场景
原文
结合上面的分析,最容易想到引发这个问题的是什么场景呢?对,连接池。因为HiveServer2开发出来就是为了对外提供JDBC标准服务的,那么应用上使用连接池无可厚非啊。但很奇怪的是,我们自己的工程,因为使用场景的原因,对Hive的操作基本是不使用连接池的。所以我去找运维同事帮忙,把所有对HiveServer2的连接都抓出来,看看是哪些服务在使用。果然,是兄弟团队的应用程序,部署了11个实例,且使用了连接池配置MaxActive为30。
对方使用的连接池是Druid,粗略的看了下,这个连接池内部维护的队列,在连接被回收的时候,也是有一大堆逻辑去保证这个连接最大可能的recycle(这个是代码里的方法名字),放到一个队列里去循环利用,而不是真的去close了。所以,当连接不够用的时候,就申请,然后连接太多的时候也维护了30个连接,11个实例加一起,就会出现最初的问题了。其他的连接池是否会造成这个问题我没有时间去分析,就不再展开了。
2020年3月27日更新
我们找对方修改了代码配置,去掉了连接池。内存回收的情况稍有改善,但是仍然没有解决问题,一段时间后,HiveServer2再次内存溢出。再次看了一遍hive的odbc包与server包所有连接与关闭的代码,结合dump确认一定是有连接未关闭造成的。
索性netstat监控一直看,到底哪些IP的那些端口在长时间连接。抓了几小时后,发现确实还有长连接在连,找到对应的IP,去看对方端口属于哪个进程,最终确定是HUE。
找到运维说明情况,运维给搭建了一个新的HiveServer2实例,专供HUE查询使用,挂了就拉起来。准备再继续观察一段时间。
我用jmap 5分钟看一次 HiveServer2老年代内存占用,发现没有一直走高的趋势,都在周期性的占用释放,看来这个问题应该是解决了。(如果仍然没有解决,按照以前的经验3月31日前必会再次OOM,所以3月31日没有更新此文章,说明这个方法确定有效)
解决方案
- JDBC操作Hive尽量不使用连接池,当然不要忘记close
- 在使用JDBC的场景里,必须提前预估好HiveServer2能提供的最大会话数量,毕竟会话与操作信息都是内存中的HashMap来存储的
- 缩短Session空闲超时判断的时间,对应的参数是hive.server2.idle.session.timeout 我们修改的值是一天的毫秒数
延伸问题
分享给同事的时候,有人问了一个问题,他有一个main函数启动了Java进程,没有Spring环境,自己创建了Driud的连接池然后获取DataSource,问不关闭DataSource就退出的话是不是会同样有问题。我认为是的,因为只要不关都要等7天才会被HiveServer强制退出。
DataSource接口描述了获取连接的行为,但关闭行为没有在这个接口里描述,我们在使用Druid这个连接池的时候获取到的是DruidDataSource类,它的关闭行为是从Closable接口中声明的。所以还是要记得强制转换对象类型为Closable,然后调用执行一下。