更多石杉老師出品的原創(chuàng)文章恢准,vx ?? ”石杉的架構(gòu)筆記“ 點進(jìn)關(guān)注領(lǐng)取
今天給大家分享一個我們之前基于dubbo開發(fā)一個線上系統(tǒng)時候遇到的內(nèi)存泄漏生產(chǎn)問題的排查與優(yōu)化實踐經(jīng)驗尔崔,相信對于大家多看一些類似的案例,以后對于大家自己在線上系統(tǒng)遇到各種生產(chǎn)問題的時候勿璃,進(jìn)行排查和優(yōu)化的思路會有很大的啟發(fā)。
事故背景
先給大家簡單說一下這個問題的發(fā)生背景,線上生產(chǎn)環(huán)境部署了兩個系統(tǒng)颈畸,我們可以認(rèn)為是系統(tǒng)A和系統(tǒng)B伞访,同時系統(tǒng)B因為是大流量核心系統(tǒng)掂骏,所以部署了幾十臺機(jī)器,定位就是集群部署要抗每秒幾萬的TPS的厚掷,兩臺系統(tǒng)之間是基于dubbo作為rpc調(diào)用框架弟灼,注冊中心用的是zookeeper,如下圖所示冒黑。
在這個背景之下田绑,某一天系統(tǒng)B因為更新了代碼,因此發(fā)起了一次幾十臺機(jī)器的全量滾動更新和部署抡爹,也就是說掩驱,系統(tǒng)B的開發(fā)團(tuán)隊基于最新的代碼把幾十臺機(jī)器依次用最新代碼重新部署了一遍,也就是每臺機(jī)器都會有一次系統(tǒng)停止和重啟的過程冬竟,如下圖所示欧穴。
沒想到生產(chǎn)環(huán)境的災(zāi)難性故障就這么突然發(fā)生了,在系統(tǒng)B的幾十臺機(jī)器依次重新部署之后泵殴,結(jié)果系統(tǒng)A的開發(fā)團(tuán)隊驚訝的發(fā)現(xiàn)自己的系統(tǒng)居然過了一會就發(fā)送了jvm內(nèi)存使用率飆升超過90%的告警涮帘,而且很快系統(tǒng)A居然就直接OOM內(nèi)存溢出崩潰了,如下圖所示笑诅。
于是系統(tǒng)B的開發(fā)團(tuán)隊順利的把一個大版本更新了幾十臺機(jī)器之后调缨,心滿意足的欣賞自己的成果呢映屋,系統(tǒng)A的開發(fā)團(tuán)隊突然開始一臉懵逼的手忙腳亂進(jìn)行了生產(chǎn)故障的排查。那么大家可以想想同蜻,這個時候棚点,如果是你負(fù)責(zé)的線上系統(tǒng)突然給你發(fā)送內(nèi)存使用率飆升超過90%,而且很快就oom內(nèi)存溢出湾蔓,你會怎么排查瘫析?
排查思路
這里給大家說說當(dāng)時我們是怎么拉進(jìn)行排查的,首先默责,遇到這種內(nèi)存突然飆升然后導(dǎo)致oom的情況贬循,先看看是不是外部對你的請求流量過大導(dǎo)致的,因為往往這種突發(fā)性的問題桃序,都是外部流量突然飆升導(dǎo)致的杖虾,這里先給分析一種外部流量突然飆升導(dǎo)致系統(tǒng)oom的場景。
假設(shè)你平時常規(guī)化運作的時候媒熊,每次一批請求過來會在你的jvm年輕代里創(chuàng)建一批對象奇适,接著這批請求處理完畢了,之前創(chuàng)建的那批對象就會成為垃圾對象了芦鳍,然后下一批請求過來嚷往,又在jvm年輕代里創(chuàng)建了一批對象,如下圖所示柠衅。
那么正常情況下皮仁,你的jvm年輕代里肯定對象會越來越多是不是?但是其實一般到了一定時候菲宴,年輕代里的存活對象基本很少贷祈,因為大部分的對象都是之前已經(jīng)處理完畢的請求創(chuàng)建的對象,他們其實都是一些沒用的垃圾對象喝峦,所以其實正常情況下跑一段時間后势誊,會觸發(fā)一下jvm年輕代的垃圾回收,把垃圾對象都回收掉就行了愈犹,如下圖键科。
所以正常情況下,是不會出現(xiàn)什么問題的漩怎,但是如果是突發(fā)性的大流量來襲呢勋颖?這個時候就不好說了,因為很可能在短時間內(nèi)突然涌入大量的請求勋锤,這些請求創(chuàng)建了大量的對象饭玲,瞬間就填滿了年輕代,然后這個時候觸發(fā)年輕代gc后叁执,發(fā)現(xiàn)大量的對象是沒法回收的茄厘,此時只能怎么辦矮冬?只能把這些對象轉(zhuǎn)移到老年代里去了,如下圖次哈。
那么這個時候年輕代里的大量存活對象都轉(zhuǎn)移到老年代里去了胎署,老年代里幾乎也被填滿了,然后此時年輕代里因為流量太大瞬時再次被填滿窑滞,此時年輕代里大量的存活對象該何去何從琼牧?這個時候你去老年代嗎?老年代都塞滿了存活對象哀卫,即使觸發(fā)了老年代gc也沒法回收他們巨坊,年輕代也沒地方放這些存活對象了,這個時候會如何此改?
很簡單趾撵,由于瞬時并發(fā)流量太大,同時創(chuàng)建了太多的存活對象共啃,塞滿了老年代和年輕代占调,我們很可能會收到報警說jvm年輕代和老年代內(nèi)存使用率都超過了90%,而且這些對象都是存活的都沒法回收勋磕,此時再要創(chuàng)建新的對象妈候,就沒地方創(chuàng)建了敢靡,接著就會報出oom內(nèi)存溢出異常來了挂滓,如下圖。
所以說瞬時流量激增可能會導(dǎo)致系統(tǒng)A發(fā)送內(nèi)存使用率超過90%啸胧,而且很快就oom的問題赶站,但是到底是不是這個問題導(dǎo)致的呢?雖然我們可以思路順暢的推演出上述場景纺念,但是我們這個時候趕緊看一下系統(tǒng)A的線上QPS指標(biāo)監(jiān)控贝椿,結(jié)果一臉懵逼的發(fā)現(xiàn),系統(tǒng)A根本就沒有流量激增陷谱,人家的流量一切都很平穩(wěn)烙博,所以根本不是這個原因?qū)е碌膯栴}。
那既然不是這個問題烟逊,那還有什么問題會導(dǎo)致這個現(xiàn)象呢渣窜?很簡單,第二種問題就是內(nèi)存泄漏宪躯,也就是說乔宿,在某種特殊條件下,觸發(fā)了一個內(nèi)存泄漏的行為访雪,就是你的系統(tǒng)不停的產(chǎn)生某一類對象详瑞,這一類對象明明都不用了掂林,結(jié)果還一直放在內(nèi)存里,而且根本回收不掉坝橡,就這么不停的積累這類對象泻帮,就會導(dǎo)致內(nèi)存使用率不停的攀升,最后導(dǎo)致oom內(nèi)存溢出计寇,如下圖刑顺。
那么針對這個內(nèi)存泄漏的問題,這個時候我們到底應(yīng)該怎么排查呢饲常?很簡單蹲堂,這個時候你到底是真程序員還是假程序員,得亮亮真功夫了贝淤,往往這種內(nèi)存類的問題柒竞,過段的用jmap這個命令,去對線上運行的系統(tǒng)jvm進(jìn)程生成一個內(nèi)存dump快照出來播聪,然后把dump快照下載到本地朽基,用MAT這個工具就可以分析這個內(nèi)存快照。
在MAT工具中我們會看到你的jvm里到底是什么破對象占用了那么大的空間离陶,才導(dǎo)致了你的內(nèi)存使用率飆升到90%+的稼虎。
這個時候其實導(dǎo)致內(nèi)存泄漏的原因有很多種,比如說你們自己代碼寫的不好招刨,就是每次請求都創(chuàng)建某一類對象霎俩,這類對象給扔到某個class的靜態(tài)map里一直放著,從來不回收沉眶,也沒法回收打却,導(dǎo)致這類無用對象一直增長,最后導(dǎo)致了oom谎倔。
另外還有一種比較常見的現(xiàn)象柳击,就是我們的系統(tǒng)使用了一些開源框架,這些開源框架在某種特殊場景下創(chuàng)建了一堆的對象片习,沒法回收捌肴,他自己也從來不回收,導(dǎo)致了開源框架悄咪咪創(chuàng)建的這批對象占用了大量內(nèi)存藕咏,導(dǎo)致了內(nèi)存泄漏状知。
所以在這里給大家說一下我們當(dāng)時遇到的一個問題,大家重點吸收排查思路侈离,下面的具體case by case的個別案例可以作為一個例子看一下试幽。
排查案例
就我們當(dāng)時的case來說,經(jīng)過MAT一通排查,發(fā)現(xiàn)占用了大量內(nèi)存的對象是dubbo框架創(chuàng)建的铺坞,dubbo框架創(chuàng)建了一種用于進(jìn)行rpc調(diào)用的大對象起宽,這類對象一直創(chuàng)建一直增長,然后從來不回收济榨,最后導(dǎo)致了內(nèi)存泄漏和內(nèi)存溢出坯沪,如下圖。
那么dubbo框架為什么會不停的創(chuàng)建一類用于進(jìn)行rpc調(diào)用的對象呢擒滑?這就得分析dubbo框架的源碼了腐晾,當(dāng)時經(jīng)過dubbo框架源碼的分析,我們得出了以下的問題發(fā)生流程丐一,當(dāng)系統(tǒng)B在線上進(jìn)行幾十臺機(jī)器的滾動發(fā)布的時候藻糖,每一臺機(jī)器被發(fā)布,都會導(dǎo)致注冊中心感知到服務(wù)變動库车,然后注冊中心會把這幾十臺機(jī)器的地址列表都給系統(tǒng)A推送過去巨柒,也就是說,連續(xù)發(fā)布幾十臺機(jī)器柠衍,就會導(dǎo)致注冊中心推送幾十次最新地址列表洋满,每一次推送都包含了幾十臺機(jī)器的地址。
因此珍坊,假設(shè)系統(tǒng)B部署了50臺機(jī)器牺勾,等于隨著50臺機(jī)器依次重新發(fā)布,會導(dǎo)致注冊中心一共給系統(tǒng)A推送50*50=2500條機(jī)器地址阵漏,如下圖驻民。
而系統(tǒng)A的dubbo框架等于會收到短時間內(nèi)頻繁推送的幾千條機(jī)器地址,然后對每條機(jī)器地址袱饭,其實dubbo框架都會去創(chuàng)建一個對應(yīng)的rpc調(diào)用類的對象川无,如下圖所示。
其實本來dubbo創(chuàng)建幾千次rpc調(diào)用對象也沒什么虑乖,但是問題就出在了一個特殊的case上了,那就是系統(tǒng)B那邊并沒有去設(shè)置對外提供的是什么rpc協(xié)議晾虑,因為dubbo是支持多種不同的rpc協(xié)議的疹味,比如說dubbo協(xié)議、http協(xié)議帜篇,等等糙捺。
所以在當(dāng)時的那個較老的dubbo版本中,就出現(xiàn)了一個隱藏的問題笙隙,就是如果系統(tǒng)B沒設(shè)置具體對外提供的協(xié)議版本洪灯,就會導(dǎo)致系統(tǒng)A收到幾千條機(jī)器地址后,除了創(chuàng)建dubbo協(xié)議的對象竟痰,還會創(chuàng)建幾千個基于http rest類協(xié)議的rpc調(diào)用對象签钩,因為系統(tǒng)B又沒提供http rest接口掏呼,因此創(chuàng)建會全部失敗,但是背后創(chuàng)建的大量對象又會放著铅檩,沒法回收憎夷,這就導(dǎo)致了dubbo框架不停的創(chuàng)建出來大量的對象,占用了90%的內(nèi)存昧旨,最后導(dǎo)致了內(nèi)存溢出拾给,如下圖。
那么這個問題是如何解決的呢兔沃?其實問題的核心在于排查思路和背后的原理蒋得,最后問題的解決往往是case by case的,比如我們這個case里乒疏,其實就很簡單窄锅,就是要讓系統(tǒng)B設(shè)置好對外提供的dubbo protocol協(xié)議,避免上面那種因為protocol協(xié)議沒設(shè)置導(dǎo)致創(chuàng)建了大量的無用對象沒法回收缰雇。
總結(jié)
最后希望大家看完今天的生產(chǎn)排查與優(yōu)化案例后入偷,未來在自己工作中遇到了類似的問題,能給大家提供一種問題排查的思路幫助大家械哟。
更多石杉老師出品的原創(chuàng)文章疏之,vx ?? ”石杉的架構(gòu)筆記“ 點進(jìn)關(guān)注領(lǐng)取