中文字幕在线观看,亚洲а∨天堂久久精品9966,亚洲成a人片在线观看你懂的,亚洲av成人片无码网站,亚洲国产精品无码久久久五月天

記一次內(nèi)存溢出的分析經(jīng)歷 — thrift帶給我的痛

2018-07-02    來源:importnew

容器云強勢上線!快速搭建集群,上萬Linux鏡像隨意使用

說在前面的話

朋友,你經(jīng)歷過部署好的服務(wù)突然內(nèi)存溢出嗎?

你經(jīng)歷過沒有看過Java虛擬機,來解決內(nèi)存溢出的痛苦嗎?

你經(jīng)歷過一個BUG,百思不得其解,頭發(fā)一根一根脫落的煩惱嗎?

我知道,你有過!

但是我還是要來說說我的故事………………


背景:

有一個項目做一個系統(tǒng),分客戶端和服務(wù)端,客戶端用c++寫的,用來收集信息然后傳給服務(wù)端(客戶端的數(shù)量還是比較多的,正常的有幾千個),

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

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

thrift版本:0.9.2.

問題定位與分析

步驟一.初步分析

客戶端無法連接服務(wù)端,查看服務(wù)器的端口開啟狀況,服務(wù)端口并沒有開啟。于是啟動服務(wù)端,啟動幾秒后,服務(wù)端崩潰,重復(fù)啟動,服務(wù)端依舊在啟動幾秒后崩潰。

步驟二.查看服務(wù)端日志分析

分析得知是因為java.lang.OutOfMemoryError: Java heap space(堆內(nèi)存溢出)導(dǎo)致的服務(wù)崩潰。

客戶端搜集的主機信息,主機策略都是放在緩存中,可能是因為緩存較大造成的,但是通過日志可以看出是因為Thrift服務(wù)拋出的堆內(nèi)存溢出異常與緩存大小無關(guān)。

步驟三.再次分析服務(wù)端日志

可以發(fā)現(xiàn)每次拋出異常的時候都會伴隨著幾十個客戶端在向服務(wù)端發(fā)送日志,往往在發(fā)送幾十條日志之后,服務(wù)崩潰?梢约僭O(shè)是不是堆內(nèi)存設(shè)置的太小了?

查看啟動參數(shù)配置,最大堆內(nèi)存為256MB。修改啟動配置,啟動的時候分配更多的堆內(nèi)存,改成java -server -Xms512m -Xmx768m。

結(jié)果是,能堅持多一點的時間,依舊會內(nèi)存溢出服務(wù)崩潰。得出結(jié)論,一味的擴大內(nèi)存是沒有用的。

**為了證明結(jié)論是正確的,做了這樣的實驗:**

> 內(nèi)存設(shè)置為256MB,在公司服務(wù)器上部署了服務(wù)端,使用Java VisualVM遠程監(jiān)控服務(wù)器堆內(nèi)存。
> 模擬客戶現(xiàn)場,注冊3000個客戶端,使用300個線程同時發(fā)送日志。
> 結(jié)果和想象的一樣,沒有出現(xiàn)內(nèi)存溢出的情況,如下圖:


> 上圖是Java VisualVM遠程監(jiān)控,在壓力測試的情況下,沒有出現(xiàn)內(nèi)存溢出的情況,256MB的內(nèi)存肯定夠用的。

步驟四.回到thrift源碼中,查找關(guān)鍵問題

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

 

**說明:**
>一個AcceptThread執(zhí)行accept客戶端請求操作,將accept到的Transport交給SelectorThread線程,

>AcceptThread中有個balance均衡器分配到SelectorThread;SelectorThread執(zhí)行read,write操作,

>read到一個FrameBuffer(封裝了方法名,參數(shù),參數(shù)類型等數(shù)據(jù),和讀取寫入,調(diào)用方法的操作)交給WorkerProcess線程池執(zhí)行方法調(diào)用。

>**內(nèi)存溢出就是在read一個FrameBuffer產(chǎn)生的。**

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

>1.服務(wù)端服務(wù)啟動后,會listen()一直監(jiān)聽客戶端的請求,當收到請求accept()后,交給線程池去處理這個請求

>2.處理的方式是:首先獲取客戶端的編碼協(xié)議getProtocol(),然后根據(jù)協(xié)議選取指定的工具進行反序列化,接著交給業(yè)務(wù)類處理process()

>3.process的順序是,**先申請臨時緩存讀取這個請求數(shù)據(jù)**,處理請求數(shù)據(jù),執(zhí)行業(yè)務(wù)代碼,寫響應(yīng)數(shù)據(jù),**最后清除臨時緩存**

> **總結(jié):thrift服務(wù)端處理請求的時候,會先反序列化數(shù)據(jù),接著申請臨時緩存讀取請求數(shù)據(jù),然后執(zhí)行業(yè)務(wù)并返回響應(yīng)數(shù)據(jù),最后請求臨時緩存。**

> 所以壓力測試的時候,thrift性能很高,而且內(nèi)存占用不高,是因為它有自負載調(diào)節(jié),使用NIO模式緩存,并使用線程池處理業(yè)務(wù),每次處理完請求之后及時清除緩存。

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

可以排除掉沒有及時清除緩存的可能,方向明確,極大的可能是在申請NIO緩存的時候出現(xiàn)了問題,回到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() != 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.
        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代碼中給出了答案:

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的開發(fā)者是覺得使用thrift程序員不夠覺得內(nèi)存不夠用嗎,這個換算下來就是1045576TB,這個太夸張了,這等于沒有限制啊,所以肯定不能用默認值的。

步驟七.通信數(shù)據(jù)抓包分析

需要可靠的證據(jù)證明一個客戶端通信的數(shù)據(jù)包的大小。

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

步驟八:踏破鐵鞋無覓處

在論壇中,看到有人用http請求thrift服務(wù)端出現(xiàn)了內(nèi)存溢出的情況,所以我抱著試試看的心態(tài),在瀏覽器中發(fā)起了http請求,

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

步驟九. 找到原因

某些客戶端沒有正常的序列化消息,導(dǎo)致服務(wù)端在處理請求的時候,序列化出來的數(shù)據(jù)特別大,讀取該數(shù)據(jù)的時候出現(xiàn)的內(nèi)存溢出。

查看維護記錄,在別的客戶那里也出現(xiàn)過內(nèi)存溢出導(dǎo)致服務(wù)端崩潰的情況,通過重新安裝客戶端,就不再復(fù)現(xiàn)了。

所以可以確定,客戶端存在著無法正常序列化消息的情況?紤]到,客戶端量比較大,一個一個排除,再重新安裝比較困難,工作量很大,所以可以從服務(wù)端的角度來解決問題,減少維護工作量。

最后可以確定解決方案了,真的是廢了很大的勁,不過也是頗有收獲

問題解決方案

非常簡單

在構(gòu)造TThreadedSelectorServer的時候,增加args.maxReadBufferBytes = 1*1024 * 1024L;也就是說修改maxReadBufferBytes的大小,設(shè)置為1MB。

客戶端與服務(wù)端通過thrift通信的數(shù)據(jù)包,最大十幾K,所以設(shè)置最大1MB,是足夠的。代碼部分修改完成,版本不做改變**
修改完畢后,這次進行了異常流測試,發(fā)送了http請求,使服務(wù)端無法正常序列化。

服務(wù)端處理結(jié)果如下:

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

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

 

使用thrift中的TThreadedSelectorServer吞吐量達到18000以上
由于高性能,申請內(nèi)存和清除內(nèi)存的操作都是非?斓,平均3ms就處理了一個請求。
所以是推薦使用TThreadedSelectorServer

4.修改啟動腳本,增大堆內(nèi)存,分配單獨的直接內(nèi)存。

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

設(shè)置持久代最大值 MaxPermSize:256m

設(shè)置年輕代大小 NewSize:256m

年輕代最大值 MaxNewSize:512M

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

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

之前啟動腳本給服務(wù)端分配的堆內(nèi)存過小,考慮到是NIO,所以在啟動服務(wù)端的時候,有必要單獨分配一個直接內(nèi)存供NIO使用.修改啟動參數(shù)。

增加堆內(nèi)存大小直接內(nèi)存,防止因為服務(wù)端緩存太大,導(dǎo)致thrift服務(wù)沒有內(nèi)存可申請,無法處理請求。

總結(jié):

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

標簽: 代碼 服務(wù)器 公司服務(wù)器 腳本 開發(fā)者 通信

版權(quán)申明:本站文章部分自網(wǎng)絡(luò),如有侵權(quán),請聯(lián)系:west999com@outlook.com
特別注意:本站所有轉(zhuǎn)載文章言論不代表本站觀點!
本站所提供的圖片等素材,版權(quán)歸原作者所有,如需使用,請與原作者聯(lián)系。

上一篇:Http 持久連接與 HttpClient 連接池

下一篇:kafka源碼分析3 : Producer