简述: Ambari的Nagios会不断发信息到hiveserver2的10000端口,以做健康监测,但hiveserver2的thrift会将其信息错误解读而导致OOM
问题发现: 在hiveserver2所在的节点,查看/var/log/hive/hive-server2.log 发现hiveserver2会莫名其妙不断OutOfMemory: Java heap space
问题排查:
加入日志和OOM heap dump
打开gc日志: 在/usr/lib/hive/bin/ext/hiveserver2.sh第18行加入: export HADOOP_CLIENT_OPTS="${HADOOP_CLIENT_OPTS} -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/var/log/hive/gc.hiveserver2.log-`date +'%Y%m%d%H%M%S'` -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/hive -XX:+DisableExplicitGC -XX:+UseCompressedOops"
在OOM时会进行heap dump
用jvisualvm分析heap dump文件, 并结合hive-server2.log中的OOM错误堆栈进行排查:
日志中的OOM堆栈:
Exception in thread "pool-5-thread-5" java.lang.OutOfMemoryError: Java heap space
at org.apache.thrift.transport.TSaslTransport.receiveSaslMessage(TSaslTransport.java:181)
at org.apache.thrift.transport.TSaslServerTransport.handleSaslStartMessage(TSaslServerTransport.java:125)
at org.apache.thrift.transport.TSaslTransport.open(TSaslTransport.java:253)
at org.apache.thrift.transport.TSaslServerTransport.open(TSaslServerTransport.java:41)
at org.apache.thrift.transport.TSaslServerTransport$Factory.getTransport(TSaslServerTransport.java:216)
at org.apache.thrift.server.TThreadPoolServer$WorkerProcess.run(TThreadPoolServer.java:189)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:744)
heap dump中的OOM堆栈(通过jvisualvm查看):
"pool-5-thread-1" prio=5 tid=18 RUNNABLE
at java.lang.OutOfMemoryError.<init>(OutOfMemoryError.java:48)
at org.apache.thrift.transport.TSaslTransport.receiveSaslMessage(TSaslTransport.java:181)
at org.apache.thrift.transport.TSaslServerTransport.handleSaslStartMessage(TSaslServerTransport.java:125)
at org.apache.thrift.transport.TSaslTransport.open(TSaslTransport.java:253)
at org.apache.thrift.transport.TSaslServerTransport.open(TSaslServerTransport.java:41)
Local Variable: org.apache.thrift.transport.TSaslServerTransport#1
at org.apache.thrift.transport.TSaslServerTransport$Factory.getTransport(TSaslServerTransport.java:216)
Local Variable: java.lang.ref.WeakReference#200
Local Variable: org.apache.thrift.transport.TSocket#2
at org.apache.thrift.server.TThreadPoolServer$WorkerProcess.run(TThreadPoolServer.java:189)
Local Variable: org.apache.hive.service.auth.TSetIpAddressProcessor#1
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
Local Variable: java.util.concurrent.ThreadPoolExecutor#4
Local Variable: org.apache.thrift.server.TThreadPoolServer$WorkerProcess#1
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
Local Variable: java.util.concurrent.ThreadPoolExecutor$Worker#1
at java.lang.Thread.run(Thread.java:744)
由于栈底是一个Thread,结合抛出异常的类名和方法名,猜测是一个与通信相关的监听线程所抛出的OOM.
下载相应版本的hive源码(0.13.0)与thrift源码, 在hive源码中查找上述jvisualvm查看的OOM异常栈中与thrift相关的类是在hive的那个地方调用了
从底查找OOM栈中并非是java原生类的thrift类,因为从OOM栈可以发现这基本是thrift的类之间的调用,也就是可以猜测是hive建了一个Thread去调用在该栈从底往上锁遇到的第一个thrift类的实例(此处实例用Local Variable标注,说明相应的类是被实例化来使用了的). 此处从底往上所遇到的第一个thrift类是org.apache.thrift.server.TThreadPoolServer.
发现在hive的源代码中的HiveMetaStore, HiveServer, ThriftBinaryCLIService中都用了TThreadPoolServer, 而由于HiveMetaStore和HiveServer都是在其main方法内实例化了TThreadPoolServer, 因此可排除这两个类. 而ThriftBinaryCLIService是一个Runnable, 在其run()方法内实例化了TThreadPoolServer, 因此若TThreadPoolServer内部的调用中发生了OOM则很有可能出现与该次OOM栈相似的异常栈形式, 所以我们着手深入了解一下ThriftBinaryCLIService.run()方法内对TThreadPoolServer进行调用,以及TThreadPoolServer相关的内部调用代码.
由于OOM从"TSaslTransport.receiveSaslMessage(TSaslTransport.java:181)"抛出,从离OOM抛出点最近的类实例信息"Local Variable: org.apache.thrift.transport.TSaslServerTransport#1"并结合代码可知,TSaslServerTransport是TSaslTransport的子类,在调用TSaslServerTransport.open()中间接调用了其父类的TSaslTransport.receiveSaslMessage()方法,而OOM就在TSaslTransport.receiveSaslMessage()方法中被触发.
通过查看源码发现"TSaslTransport.receiveSaslMessage(TSaslTransport.java:181)"处的代码是"byte[] payload = new byte[EncodingUtils.decodeBigEndian(messageHeader, STATUS_BYTES)];", 初步猜测是EncodingUtils.decodeBigEndian()方法解析出来的数字太大,导致创建了一个超大的byte[]而导致OOM.
在jvisualvm的heap dump分析器中对类实例按占用内存大小排序,可看到byte[]的确占了很多内存.
在jvisualvm的heap dump分析器中查看org.apache.thrift.transport.TSaslServerTransport对应的实例在OOM时的状态.
由于代码"byte[] payload = new byte[EncodingUtils.decodeBigEndian(messageHeader, STATUS_BYTES)];"是通过对messageHeader进行解析而得到一个整数,因此可以看看OOM时messageHeader中的内容是什么.
拿到messageHeader当时的内容后,可以写一个main方法简单看看"new byte[EncodingUtils.decodeBigEndian(messageHeader, STATUS_BYTES)]"所得到的数组有多大,最后发现数组有近1G多那么大. 而按照当时hiveserver2的内存配置,堆也就约1G的内存,突然申请那么大的byte[],不OOM才怪呢.
那么是什么导致"EncodingUtils.decodeBigEndian(messageHeader, STATUS_BYTES)"得到的整数那么大呢,想必应该是发过来的信息不符合thrift协议规范,而又如何找到对方发了什么信息过来呢? 以及对方是谁? 为什么要发这些信息呢?
我们从OOM堆栈中得知问题是发生在调用链 TThreadPoolServer->TSaslServerTransport->TSaslServerTransport的super父类TSaslTransport上的, 通过源代码得知,TSaslTransport.receiveSaslMessage()是用于读取远端连接过来的client端的信息的,具体的读取通过TSaslTransport.underlyingTransport去读取. 而TSaslTransport.underlyingTransport则代表了一个客户端的连接,在TThreadPoolServer.serve()中由代表服务端的TThreadPoolServer.serverTransport_通过监听获得.因此,我们可以从TSaslTransport.underlyingTransport入手,获得连接到服务端的远端IP和端口.
首先,找到TSaslTransport.underlyingTransport, 并查看它内部的信息.
查看TSaslTransport.underlyingTransport实例的socket_.impl.localport,其值为10000即本地ThriftBinaryCLIService所对外暴露的端口,而TSaslTransport.underlyingTransport实例的socket_.impl.port为远端访问这个10000的远端端口,值为58235. TSaslTransport.underlyingTransport实例的socket_.impl.address.holder.address为"-1062694615",是访问本地10000端口的远端客户端的IP的整数表达形式,可参考Inet4Address.getAddress()把这个整数转化成我们所熟悉的IP地址字符串.
public static void convertInt2IPString() { int address = -1062694615; //copy from the instance of InetAddress.holder.address byte[] ipByteArr = getAddress(address); String ipStr = numericToTextFormat(ipByteArr); System.out.println(ipStr);//整数ip为-1062694615,转换成常规ip为192.168.145.41 } //copy from Inet4Address.getAddress() public static byte[] getAddress(int address) { final int INADDRSZ = 4; byte[] addr = new byte[INADDRSZ]; addr[0] = (byte) ((address >>> 24) & 0xFF); addr[1] = (byte) ((address >>> 16) & 0xFF); addr[2] = (byte) ((address >>> 8) & 0xFF); addr[3] = (byte) (address & 0xFF); return addr; } //copy from Inet4Address.numericToTextFormat() static String numericToTextFormat(byte[] src) { return (src[0] & 0xff) + "." + (src[1] & 0xff) + "." + (src[2] & 0xff) + "." + (src[3] & 0xff); }
得到"-1062694615"转换成的ip为:"192.168.145.41".
用命令"ss -anpe | grep 10000"在本地运行,结果如下:
LISTEN 0 50 *:10000 *:* users:(("java",20804,373)) uid:1006 ino:1760863 sk:ffff88003ed12080
CLOSE-WAIT 1 0 192.168.145.42:10000 192.168.145.41:36315 users:(("java",20804,364)) uid:1006 ino:2266968 sk:ffff88003ec93480
其中20804的确是hiveserver2的进程, 访问本地10000端口的远端客户端ip的确也是192.168.145.41,但客户端的远端端口却不是58235.
在客户端192.168.145.41运行"ss -anpe | grep 10000",发现每运行一次该命令,访问192.168.145.42:10000的端口都会变化,且状态是TIME_WAIT,这说明与192.168.145.42:10000的连接已经被192.168.145.42关闭,因此猜测由于192.168.145.42的OOM导致192.168.145.41的某个程序无法连接到10000端口,而连接10000的那个程序就不得不变化端口,以求连接到192.168.145.42:10000上.
由于客户端发送信息的端口一直在变,而且用ss命令查看相关端口都只能看到等待状态而无法看到使用该端口的线程,我们就无法获得通过这些端口发送消息的进程的信息. 因此我们打算看看发送到192.168.145.42服务端10000端口的消息到底是怎样的.
使用"tcpdump -i eth4 -s 0 -nnA 'port 10000' -w /mnt/shareDisk/local10000.cap" 抓取访问10000端口的数据包,并保存到local10000.cap中. 在windows中用Wireshark打开cap文件,查看通信报文内容.发现所有来自192.168.145.41的报文体都包含字符串"A001". 但这条线索依然无法帮助我们定位到底是什么程序发了字符串"A001"到192.168.145.42的10000端口.
由于猜测要访问192.168.145.42的hiveserver2的10000端口的应该就是大数据集群内部的东西,因此我们通过Ambari的页面逐一关闭大数据集群的service,再通过192.168.145.42的tcpdump命令观察192.168.145.41是否停止发送"A001"消息. 通过这种方法,我们发现关闭了Nagios之后就不会有数据包发送到192.168.145.42的10000端口. 从而缩小了问题范围.
我们进而拿到ambari相应版本的代码,搜索"A001"字符串,发现在"/var/lib/ambari-server/resources/stacks/HDP/2.0.6/services/NAGIOS/package/templates/hadoop-services.cfg.j2"中有一段代码"check_command check_tcp_wrapper_sasl!{{ hive_server_port }}!-w 1 -c 1!A001 AUTHENTICATE ANONYMOUS" 而hive_server_port就是hiveserver2的thrift端口10000. 从 "/var/lib/ambari-server/resources/stacks/HDP/2.0.6/services/NAGIOS/package/templates/hadoop-commands.cfg.j2"得知,nagios用于发送信息检验端口的check_tcp_wrapper_sasl命令最终是通过调用nagios的check_tcp插件完成的, 而check_tcp_wrapper_sasl与check_tcp_wrapper两个命令的不同之处仅在于前者通过check_tcp发送了字符串"A001 AUTHENTICATE ANONYMOUS", 而后者没有发送字符串.
至此,我们可以做出结论,Ambari的Nagios由于监控需要,会定期向hiveserver2发送"A001 AUTHENTICATE ANONYMOUS",但这与hive监听10000的ThriftBinaryCLIService服务的协议不一致(可能是Ambari的Nagios的问题),导致ThriftBinaryCLIService在解析消息时,截取消息第2到第5位以解析出消息长度时解析出了一个超大的整数,用该整数初始化byte数组,从而导致了OOM.
解决方案如下:
1, 在hive-site.xml上配置hive.server2.authentication=NOSASL, 取消hive的SASL认证
2, 将"/var/lib/ambari-server/resources/stacks/HDP/2.0.6/services/NAGIOS/package/templates/hadoop-services.cfg.j2"中的"check_command check_tcp_wrapper_sasl!{{ hive_server_port }}!-w 1 -c 1!A001 AUTHENTICATE ANONYMOUS" 改为 "check_command check_tcp_wrapper!{{ hive_server_port }}!-w 1 -c 1" 即更改Ambari的Nagios的监控脚本模板文件,以通过普通的tcp访问去监控hive,而不发送任何字符串消息.