業餘草 thrift 內存溢出的分析解決過程分享

業餘草 今天爲你們分享一下我在使用 thrift 過程當中遇到的內存泄漏解放方法。java

 

背景:

有一個項目作一個系統,分客戶端和服務端,客戶端用c++寫的,用來收集信息而後傳給服務端(客戶端的數量仍是比較多的,正常的有幾千個),linux

服務端用Java寫的(帶管理頁面),屬於RPC模式,中間的通訊框架使用的是thrift。android

thrift不少優勢就很少說了,它是facebook的開源的rpc框架,主要是它可以跨語言,序列化速度快,可是他有個不討喜的地方就是它必須用本身IDL來定義接口c++

thrift版本:0.9.2.git

問題定位與分析

步驟一.初步分析

客戶端沒法鏈接服務端,查看服務器的端口開啓情況,服務端口並無開啓。因而啓動服務端,啓動幾秒後,服務端崩潰,重複啓動,服務端依舊在啓動幾秒後崩潰。程序員

步驟二.查看服務端日誌分析

分析得知是由於java.lang.OutOfMemoryError: Java heap space(堆內存溢出)致使的服務崩潰。redis

客戶端蒐集的主機信息,主機策略都是放在緩存中,多是由於緩存較大形成的,可是經過日誌能夠看出是由於Thrift服務拋出的堆內存溢出異常與緩存大小無關。spring

步驟三.再次分析服務端日誌

能夠發現每次拋出異常的時候都會伴隨着幾十個客戶端在向服務端發送日誌,每每在發送幾十條日誌以後,服務崩潰。能夠假設是否是堆內存設置的過小了?mongodb

查看啓動參數配置,最大堆內存爲256MB。修改啓動配置,啓動的時候分配更多的堆內存,改爲java -server -Xms512m -Xmx768m。小程序

結果是,能堅持多一點的時間,依舊會內存溢出服務崩潰。得出結論,一味的擴大內存是沒有用的。

**爲了證實結論是正確的,作了這樣的實驗:**
> 內存設置爲256MB,在公司服務器上部署了服務端,使用Java VisualVM遠程監控服務器堆內存。
>
> 模擬客戶現場,註冊3000個客戶端,使用300個線程同時發送日誌。
>
> 結果和想象的同樣,沒有出現內存溢出的狀況,以下圖:

 
> 上圖是Java VisualVM遠程監控,在壓力測試的狀況下,沒有出現內存溢出的狀況,256MB的內存確定夠用的。


步驟四.回到thrift源碼中,查找關鍵問題

服務端採用的是Thrift框架中TThreadedSelectorServer這個類,這是一個NIO的服務。下圖是thrift處理請求的模型:

 


**說明:**
>一個AcceptThread執行accept客戶端請求操做,將accept到的Transport交給SelectorThread線程, 
>
>AcceptThread中有個balance均衡器分配到SelectorThread;SelectorThread執行read,write操做,
>
>read到一個FrameBuffer(封裝了方法名,參數,參數類型等數據,和讀取寫入,調用方法的操做)交給WorkerProcess線程池執行方法調用。
>
>**內存溢出就是在read一個FrameBuffer產生的。**


步驟五.細緻一點描述thrift處理過程


>1.服務端服務啓動後,會listen()一直監聽客戶端的請求,當收到請求accept()後,交給線程池去處理這個請求
>
>2.處理的方式是:首先獲取客戶端的編碼協議getProtocol(),而後根據協議選取指定的工具進行反序列化,接着交給業務類處理process()
>
>3.process的順序是,**先申請臨時緩存讀取這個請求數據**,處理請求數據,執行業務代碼,寫響應數據,**最後清除臨時緩存**
>
> **總結:thrift服務端處理請求的時候,會先反序列化數據,接着申請臨時緩存讀取請求數據,而後執行業務並返回響應數據,最後請求臨時緩存。**
>
> 因此壓力測試的時候,thrift性能很高,並且內存佔用不高,是由於它有自負載調節,使用NIO模式緩存,並使用線程池處理業務,每次處理完請求以後及時清除緩存。


步驟六.研讀FrameBuffer的read方法代碼

能夠排除掉沒有及時清除緩存的可能,方向明確,極大的多是在申請NIO緩存的時候出現了問題,回到thrift框架,查看FrameBuffer的read方法代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public  boolean  read() {          // try to read the frame size completely 
             if  ( this .state_ == AbstractNonblockingServer.FrameBufferState.READING_FRAME_SIZE) {
                 if  (! this .internalRead()) {
                     return  false ;
                 }
          // if the frame size has been read completely, then prepare to read the actual time
                 if  ( this .buffer_.remaining() !=  0 ) {
                     return  true ;
                 }
 
                 int  frameSize =  this .buffer_.getInt( 0 );
                 if  (frameSize <=  0 ) {
                     this .LOGGER.error( "Read an invalid frame size of "  + frameSize +  ". Are you using TFramedTransport on the client side?" );
                     return  false ;
                 }
           // if this frame will always be too large for this server, log the error and close the connection. 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
         if  (( long )frameSize > AbstractNonblockingServer. this .MAX_READ_BUFFER_BYTES) {
             this .LOGGER.error( "Read a frame size of "  + frameSize +  ", which is bigger than the maximum allowable buffer size for ALL connections." );
             return  false ;
         }
 
         if  (AbstractNonblockingServer. this .readBufferBytesAllocated.get() + ( long )frameSize > AbstractNonblockingServer. this .MAX_READ_BUFFER_BYTES) {
             return  true ;
         }
 
         AbstractNonblockingServer. this .readBufferBytesAllocated.addAndGet(( long )(frameSize +  4 ));
         this .buffer_ = ByteBuffer.allocate(frameSize +  4 );
         this .buffer_.putInt(frameSize);
         this .state_ = AbstractNonblockingServer.FrameBufferState.READING_FRAME;
     }
 
     if  ( this .state_ == AbstractNonblockingServer.FrameBufferState.READING_FRAME) {
         if  (! this .internalRead()) {
             return  false ;
         else  {
             if  ( this .buffer_.remaining() ==  0 ) {
                 this .selectionKey_.interestOps( 0 );
                 this .state_ = AbstractNonblockingServer.FrameBufferState.READ_FRAME_COMPLETE;
             }
 
             return  true ;
         }
     else  {
         this .LOGGER.error( "Read was called but state is invalid ("  this .state_ +  ")" );
         return  false ;
     }
}


**說明:**
>MAX_READ_BUFFER_BYTES這個值即爲對讀取的包的長度限制,若是超過長度限制,就不會再讀了/
>
>這個MAX_READ_BUFFER_BYTES是多少呢,thrift代碼中給出了答案:

1
2
3
4
5
6
7
8
public  abstract  static  class  AbstractNonblockingServerArgs<T  extends  AbstractNonblockingServer.AbstractNonblockingServerArgs<T>>  extends  AbstractServerArgs<T> {<br>     
     public  long  maxReadBufferBytes = 9223372036854775807L;
 
     public  AbstractNonblockingServerArgs(TNonblockingServerTransport transport) {
         super (transport);
         this .transportFactory( new  Factory());
     }
}

 
>從上面源碼能夠看出,默認值竟然給到了long的最大值9223372036854775807L。

因此thrift的開發者是以爲使用thrift程序員不夠以爲內存不夠用嗎,這個換算下來就是1045576TB,這個太誇張了,這等於沒有限制啊,因此確定不能用默認值的。


步驟七.通訊數據抓包分析

須要可靠的證據證實一個客戶端通訊的數據包的大小。

 

這個是我抓到包最大的長度,最大一個包長度只有215B,因此須要限制一下讀取大小


步驟八:踏破鐵鞋無覓處

在論壇中,看到有人用http請求thrift服務端出現了內存溢出的狀況,因此我抱着試試看的心態,在瀏覽器中發起了http請求,

果不其然,出現了內存溢出的錯誤,和客戶現場出現的問題一摸同樣。這個讀取內存的時候數量過大,超過了256MB。
> 很明顯的一個問題,正常的一個HTTP請求不會有256MB的,考慮到thrift在處理請求的時候有反序列化這個操做。
>
> 能夠作出假設是否是反序列化的問題,不是thrift IDL定義的不能正常的反序列化?
>
> 驗證這個假設,我用Java socket寫了一個tcp客戶端,向thrift服務端發送請求,果不其然!java.lang.OutOfMemoryError: Java heap space。
> 這個假設是正確的,客戶端請求數據不是用thrift IDL定義的話,沒法正常序列化,序列化出來的數據會異常的大!大到超過1個G的都有。


步驟九. 找到緣由

某些客戶端沒有正常的序列化消息,致使服務端在處理請求的時候,序列化出來的數據特別大,讀取該數據的時候出現的內存溢出。

查看維護記錄,在別的客戶那裏也出現過內存溢出致使服務端崩潰的狀況,經過從新安裝客戶端,就再也不復現了。

因此能夠肯定,客戶端存在着沒法正常序列化消息的狀況。考慮到,客戶端量比較大,一個一個排除,再從新安裝比較困難,工做量很大,因此能夠從服務端的角度來解決問題,減小維護工做量。

最後能夠肯定解決方案了,真的是廢了很大的勁,不過也是很有收穫


問題解決方案

很是簡單

在構造TThreadedSelectorServer的時候,增長args.maxReadBufferBytes = 1*1024 * 1024L;也就是說修改maxReadBufferBytes的大小,設置爲1MB。

客戶端與服務端經過thrift通訊的數據包,最大十幾K,因此設置最大1MB,是足夠的。代碼部分修改完成,版本不作改變**
修改完畢後,此次進行了異常流測試,發送了http請求,使服務端沒法正常序列化。

服務端處理結果以下:

thrift會拋出錯誤日誌,並直接沒有讀這個消息,返回false,不處理這樣的請求,將其視爲錯誤請求。

3.國外有人對thrift一些server作了壓力測試,以下圖所示:

 

使用thrift中的TThreadedSelectorServer吞吐量達到18000以上
因爲高性能,申請內存和清除內存的操做都是很是快的,平均3ms就處理了一個請求。
因此是推薦使用TThreadedSelectorServer


4.修改啓動腳本,增大堆內存,分配單獨的直接內存。

修改成java -server -Xms512m -Xmx768m -XX:MaxPermSize=256m -XX:NewSize=256m -XX:MaxNewSize=512m -XX:MaxDirectMemorySize=128M。


設置持久代最大值 MaxPermSize:256m


設置年輕代大小 NewSize:256m


年輕代最大值 MaxNewSize:512M


最大堆外內存(直接內存)MaxDirectMemorySize:128M


5.綜合論壇中,StackOverflow一些同僚的意見,在使用TThreadedSelectorServer時,將讀取內存限制設置爲1MB,最爲合適,正常流和異常流的狀況下不會有內存溢出的風險。

 以前啓動腳本給服務端分配的堆內存太小,考慮到是NIO,因此在啓動服務端的時候,有必要單獨分配一個直接內存供NIO使用.修改啓動參數。

增長堆內存大小直接內存,防止由於服務端緩存太大,致使thrift服務沒有內存可申請,沒法處理請求。


總結:

真的是一次很是酸爽的過程,特此發個博客記錄一下,若是有說的不對的對方,歡迎批評斧正!若是以爲寫的不錯,歡迎給我點個推薦,您的一個推薦是我莫大的動力!

業餘草分享100套精選1000G架構師資料課程(超1T的IT學習資料免費送)。

超過1024G的IT學習資料免費領取,你值得擁有!

領取資源方式,關注「業餘草」公衆號,回覆對應的關鍵字

 

    • 0一、回覆」我要健康「,獲取程序員健康、內涵大禮包!
    • 0二、回覆」大數據「,獲取大數據相關資源。
    • 0三、回覆」linux「,獲取linux相關資料。
    • 0四、回覆」mongodb「,獲取mongodb學習資料。
    • 0五、回覆」redis「,獲取redis相關資料。
    • 0六、回覆」zookeeper「,獲取分佈式等相關資料。
    • 0七、回覆」dubbo「,獲取dubbo相關資料。
    • 0八、回覆」git「,獲取git資料
    • 0九、回覆」設計模式「,獲取設計模式資料
    • 十、回覆」小程序「,獲取小程序相關的資料
    • 十一、回覆」android「,獲取安卓相關的資料
    • 十二、回覆」java「,獲取java相關的資料
    • 1三、回覆」pyhton「,獲取Pyhton相關資料。
    • 1四、回覆」springboot「,獲取Spring Boot相關資料。
    • 1五、回覆」springcloud「,獲取Spring Cloud相關資料。
相關文章
相關標籤/搜索