記一次內(nèi)存溢出的分析經(jīng)歷

來源:http://www.cnblogs.com/superfj/p/8474288.html

說在前面的話

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

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

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

我知道,你有過纠拔!

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

背景:

有一個(gè)項(xiàng)目做一個(gè)系統(tǒng)秉剑,分客戶端和服務(wù)端,客戶端用c++寫的稠诲,用來收集信息然后傳給服務(wù)端(客戶端的數(shù)量還是比較多的侦鹏,正常的有幾千個(gè)),服務(wù)端用Java寫的(帶管理頁(yè)面)臀叙,屬于RPC模式略水,中間的通信框架使用的是thrift。

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

thrift版本:0.9.2.

問題定位與分析

步驟一.初步分析

客戶端無法連接服務(wù)端跨释,查看服務(wù)器的端口開啟狀況,服務(wù)端口并沒有開啟厌处。于是啟動(dòng)服務(wù)端煤傍,啟動(dòng)幾秒后,服務(wù)端崩潰嘱蛋,重復(fù)啟動(dòng)蚯姆,服務(wù)端依舊在啟動(dòng)幾秒后崩潰五续。

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

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

客戶端搜集的主機(jī)信息龄恋,主機(jī)策略都是放在緩存中疙驾,可能是因?yàn)榫彺孑^大造成的,但是通過日志可以看出是因?yàn)門hrift服務(wù)拋出的堆內(nèi)存溢出異常與緩存大小無關(guān)郭毕。

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

可以發(fā)現(xiàn)每次拋出異常的時(shí)候都會(huì)伴隨著幾十個(gè)客戶端在向服務(wù)端發(fā)送日志它碎,往往在發(fā)送幾十條日志之后,服務(wù)崩潰显押“飧兀可以假設(shè)是不是堆內(nèi)存設(shè)置的太小了?

查看啟動(dòng)參數(shù)配置乘碑,最大堆內(nèi)存為256MB挖息。修改啟動(dòng)配置,啟動(dòng)的時(shí)候分配更多的堆內(nèi)存兽肤,改成java -server -Xms512m -Xmx768m套腹。

結(jié)果是,能堅(jiān)持多一點(diǎn)的時(shí)間资铡,依舊會(huì)內(nèi)存溢出服務(wù)崩潰电禀。得出結(jié)論,一味的擴(kuò)大內(nèi)存是沒有用的笤休。

為了證明結(jié)論是正確的尖飞,做了這樣的實(shí)驗(yàn):

內(nèi)存設(shè)置為256MB,在公司服務(wù)器上部署了服務(wù)端店雅,使用Java VisualVM遠(yuǎn)程監(jiān)控服務(wù)器堆內(nèi)存政基。

模擬客戶現(xiàn)場(chǎng),注冊(cè)3000個(gè)客戶端底洗,使用300個(gè)線程同時(shí)發(fā)送日志腋么。

結(jié)果和想象的一樣咕娄,沒有出現(xiàn)內(nèi)存溢出的情況亥揖,如下圖:

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

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

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

說明:

一個(gè)AcceptThread執(zhí)行accept客戶端請(qǐng)求操作吁峻,將accept到的Transport交給SelectorThread線程滑负,AcceptThread中有個(gè)balance均衡器分配到SelectorThread在张;SelectorThread執(zhí)行read,write操作矮慕,read到一個(gè)FrameBuffer(封裝了方法名帮匾,參數(shù),參數(shù)類型等數(shù)據(jù)痴鳄,和讀取寫入瘟斜,調(diào)用方法的操作)交給WorkerProcess線程池執(zhí)行方法調(diào)用。

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

步驟五.細(xì)致一點(diǎn)描述thrift處理過程

服務(wù)端服務(wù)啟動(dòng)后螺句,會(huì)listen()一直監(jiān)聽客戶端的請(qǐng)求,當(dāng)收到請(qǐng)求accept()后橡类,交給線程池去處理這個(gè)請(qǐng)求

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

process的順序是猫态,先申請(qǐng)臨時(shí)緩存讀取這個(gè)請(qǐng)求數(shù)據(jù)佣蓉,處理請(qǐng)求數(shù)據(jù),執(zhí)行業(yè)務(wù)代碼亲雪,寫響應(yīng)數(shù)據(jù)勇凭,最后清除臨時(shí)緩存

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

所以壓力測(cè)試的時(shí)候璧函,thrift性能很高,而且內(nèi)存占用不高基显,是因?yàn)樗凶载?fù)載調(diào)節(jié)蘸吓,使用NIO模式緩存,并使用線程池處理業(yè)務(wù)撩幽,每次處理完請(qǐng)求之后及時(shí)清除緩存库继。

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

可以排除掉沒有及時(shí)清除緩存的可能,方向明確窜醉,極大的可能是在申請(qǐng)NIO緩存的時(shí)候出現(xiàn)了問題宪萄,回到thrift框架,查看FrameBuffer的read方法代碼:

publicboolean?read()?{

//?try?to?read?the?frame?size?completely?

if(this.state_?==?AbstractNonblockingServer.FrameBufferState.READING_FRAME_SIZE)?{

if(!this.internalRead())?{

returnfalse;

}

//?if?the?frame?size?has?been?read?completely,?then?prepare?to?read?the?actual?time

if(this.buffer_.remaining()?!=0)?{

returntrue;

}

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?");

returnfalse;

}

//?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.");

returnfalse;

}

if(AbstractNonblockingServer.this.readBufferBytesAllocated.get()?+?(long)frameSize?>?AbstractNonblockingServer.this.MAX_READ_BUFFER_BYTES)?{

returntrue;

}

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())?{

returnfalse;

}else{

if(this.buffer_.remaining()?==0)?{

this.selectionKey_.interestOps(0);

this.state_?=?AbstractNonblockingServer.FrameBufferState.READ_FRAME_COMPLETE;

}

returntrue;

}

}else{

this.LOGGER.error("Read?was?called?but?state?is?invalid?("+this.state_?+")");

returnfalse;

}

}

說明:

MAX_READ_BUFFER_BYTES這個(gè)值即為對(duì)讀取的包的長(zhǎng)度限制榨惰,如果超過長(zhǎng)度限制拜英,就不會(huì)再讀了。

這個(gè)MAX_READ_BUFFER_BYTES是多少呢琅催,thrift代碼中給出了答案:

publicabstractstaticclassAbstractNonblockingServerArgs>extendsAbstractServerArgs{

publiclongmaxReadBufferBytes?=9223372036854775807L;

publicAbstractNonblockingServerArgs(TNonblockingServerTransport?transport){

super(transport);

this.transportFactory(newFactory());

}

}

從上面源碼可以看出居凶,默認(rèn)值居然給到了long的最大值9223372036854775807L虫给。

所以thrift的開發(fā)者是覺得使用thrift程序員不夠覺得內(nèi)存不夠用嗎,這個(gè)換算下來就是1045576TB侠碧,這個(gè)太夸張了狰右,這等于沒有限制啊,所以肯定不能用默認(rèn)值的舆床。

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

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

這個(gè)是我抓到包最大的長(zhǎng)度,最大一個(gè)包長(zhǎng)度只有215B挨队,所以需要限制一下讀取大小

步驟八:踏破鐵鞋無覓處

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

果不其然,出現(xiàn)了內(nèi)存溢出的錯(cuò)誤腾夯,和客戶現(xiàn)場(chǎng)出現(xiàn)的問題一摸一樣颊埃。這個(gè)讀取內(nèi)存的時(shí)候數(shù)量過大,超過了256MB蝶俱。

很明顯的一個(gè)問題班利,正常的一個(gè)HTTP請(qǐng)求不會(huì)有256MB的,考慮到thrift在處理請(qǐng)求的時(shí)候有反序列化這個(gè)操作榨呆。

可以做出假設(shè)是不是反序列化的問題罗标,不是thrift IDL定義的不能正常的反序列化?

驗(yàn)證這個(gè)假設(shè)积蜻,我用Java socket寫了一個(gè)tcp客戶端闯割,向thrift服務(wù)端發(fā)送請(qǐng)求,果不其然竿拆!java.lang.OutOfMemoryError: Java heap space宙拉。

這個(gè)假設(shè)是正確的,客戶端請(qǐng)求數(shù)據(jù)不是用thrift IDL定義的話,無法正常序列化丙笋,序列化出來的數(shù)據(jù)會(huì)異常的大谢澈!大到超過1個(gè)G的都有。

步驟九. 找到原因

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

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

所以可以確定,客戶端存在著無法正常序列化消息的情況酪惭∠3眨考慮到,客戶端量比較大春感,一個(gè)一個(gè)排除砌创,再重新安裝比較困難,工作量很大鲫懒,所以可以從服務(wù)端的角度來解決問題嫩实,減少維護(hù)工作量。

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

問題解決方案

非常簡(jiǎn)單

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

2.客戶端與服務(wù)端通過thrift通信的數(shù)據(jù)包朦乏,最大十幾K球及,所以設(shè)置最大1MB,是足夠的呻疹。代碼部分修改完成桶略,版本不做改變,修改完畢后诲宇,這次進(jìn)行了異常流測(cè)試际歼,發(fā)送了http請(qǐng)求,使服務(wù)端無法正常序列化姑蓝。

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

thrift會(huì)拋出錯(cuò)誤日志鹅心,并直接沒有讀這個(gè)消息,返回false,不處理這樣的請(qǐng)求纺荧,將其視為錯(cuò)誤請(qǐng)求旭愧。

3.國(guó)外有人對(duì)thrift一些server做了壓力測(cè)試,如下圖所示:

使用thrift中的TThreadedSelectorServer吞吐量達(dá)到18000以上宙暇,由于高性能输枯,申請(qǐng)內(nèi)存和清除內(nèi)存的操作都是非常快的占贫,平均3ms就處理了一個(gè)請(qǐng)求桃熄。所以是推薦使用TThreadedSelectorServer。

4.修改啟動(dòng)腳本型奥,增大堆內(nèi)存瞳收,分配單獨(dú)的直接內(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時(shí),將讀取內(nèi)存限制設(shè)置為1MB界弧,最為合適凡蜻,正常流和異常流的情況下不會(huì)有內(nèi)存溢出的風(fēng)險(xiǎn)。

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

增加堆內(nèi)存大小直接內(nèi)存茅姜,防止因?yàn)榉?wù)端緩存太大,導(dǎo)致thrift服務(wù)沒有內(nèi)存可申請(qǐng)月匣,無法處理請(qǐng)求钻洒。

擴(kuò)展閱讀

JVM面試題

深入了解Java之虛擬機(jī)內(nèi)存

深入分析 ThreadLocal 內(nèi)存泄漏問題

Java 虛擬機(jī)類加載機(jī)制

Java 內(nèi)存模型 JMM 淺析

Java面試筆試:面試為什么需要了解Java虛擬機(jī)?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末锄开,一起剝皮案震驚了整個(gè)濱河市素标,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌萍悴,老刑警劉巖头遭,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異癣诱,居然都是意外死亡计维,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門撕予,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鲫惶,“玉大人,你說我怎么就攤上這事实抡∏纺福” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵吆寨,是天一觀的道長(zhǎng)赏淌。 經(jīng)常有香客問我,道長(zhǎng)啄清,這世上最難降的妖魔是什么六水? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上缩擂,老公的妹妹穿的比我還像新娘。我一直安慰自己添寺,他們只是感情好胯盯,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著计露,像睡著了一般博脑。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上票罐,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天叉趣,我揣著相機(jī)與錄音,去河邊找鬼该押。 笑死疗杉,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蚕礼。 我是一名探鬼主播烟具,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼奠蹬!你這毒婦竟也來了朝聋?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤囤躁,失蹤者是張志新(化名)和其女友劉穎冀痕,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體狸演,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡言蛇,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了宵距。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片猜极。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖消玄,靈堂內(nèi)的尸體忽然破棺而出跟伏,到底是詐尸還是另有隱情,我是刑警寧澤翩瓜,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布受扳,位于F島的核電站,受9級(jí)特大地震影響兔跌,放射性物質(zhì)發(fā)生泄漏勘高。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望华望。 院中可真熱鬧蕊蝗,春花似錦、人聲如沸赖舟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)宾抓。三九已至子漩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間石洗,已是汗流浹背幢泼。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留讲衫,地道東北人缕棵。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像涉兽,于是被迫代替她去往敵國(guó)和親挥吵。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容