天天看點

記一次記憶體溢出的分析經曆——使用thrift

背景:

有一個項目做一個系統,分用戶端和服務端,用戶端用c++寫的,用來收集資訊然後傳給服務端(用戶端的數量還是比較多的,正常的有幾千個),

服務端用Java寫的(帶管理頁面),屬于RPC模式,中間的通信架構使用的是thrift。

thrift很多優點就不多說了,它是facebook的開源的rpc架構,主要是它能夠跨語言,序列化速度快,但是他有個不讨喜的地方就是它必須用自己IDL來定義接口

thrift版本:0.9.2.

問題定位與分析

步驟一.初步分析

用戶端無法連接配接服務端,檢視伺服器的端口開啟狀況,服務端口并沒有開啟。于是啟動服務端,啟動幾秒後,服務端崩潰,重複啟動,服務端依舊在啟動幾秒後崩潰。

步驟二.檢視服務端日志分析

分析得知是因為java.lang.OutOfMemoryError: Java heap space(堆記憶體溢出)導緻的服務崩潰。

用戶端搜集的主機資訊,主機政策都是放在緩存中,可能是因為緩存較大造成的,但是通過日志可以看出是因為Thrift服務抛出的堆記憶體溢出異常與緩存大小無關。

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

可以發現每次抛出異常的時候都會伴随着幾十個用戶端在向服務端發送日志,往往在發送幾十條日志之後,服務崩潰。可以假設是不是堆記憶體設定的太小了?

檢視啟動參數配置,最大堆記憶體為256MB。修改啟動配置,啟動的時候配置設定更多的堆記憶體,改成java -server -Xms512m -Xmx768m。

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

**為了證明結論是正确的,做了這樣的實驗:**

> 記憶體設定為256MB,在公司伺服器上部署了服務端,使用Java VisualVM遠端監控伺服器堆記憶體。

>

> 模拟客戶現場,注冊3000個用戶端,使用300個線程同時發送日志。

> 結果和想象的一樣,沒有出現記憶體溢出的情況,如下圖:

記一次記憶體溢出的分析經曆——使用thrift

> 上圖是Java VisualVM遠端監控,在壓力測試的情況下,沒有出現記憶體溢出的情況,256MB的記憶體肯定夠用的。

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

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

記一次記憶體溢出的分析經曆——使用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方法代碼:

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() != 

) {

return

true

;

}

int

frameSize = 

this

.buffer_.getInt(

);

if

(frameSize <= 

) {

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. 

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() == 

) {

this

.selectionKey_.interestOps(

);

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代碼中給出了答案:

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,這個太誇張了,這等于沒有限制啊,是以肯定不能用預設值的。

步驟七.通信資料抓包分析

需要可靠的證據證明一個用戶端通信的資料包的大小。

記一次記憶體溢出的分析經曆——使用thrift

這個是我抓到包最大的長度,最大一個包長度隻有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

thrift會抛出錯誤日志,并直接沒有讀這個消息,傳回false,不處理這樣的請求,将其視為錯誤請求。

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

記一次記憶體溢出的分析經曆——使用thrift

使用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服務沒有記憶體可申請,無法處理請求。

總結:

真的是一次非常酸爽的過程,特此發個部落格記錄一下,如果有說的不對的對方,歡迎批評斧正!如果覺得寫的不錯,歡迎給我點個推薦,您的一個推薦是我莫大的動力!