HDP-Hive在Ambari打開Nagios情況下OutOfMemory的問題

簡述: 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地址字符串.



 
把遠端客戶端ip的整數表達轉換成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,而不發送任何字符串消息.

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章