線上又OOM,我太難了......
本文轉載自微信公眾號「石杉的架構筆記」,作者中華石杉。轉載本文請聯系石杉的架構筆記公眾號。
今天給大家分享一個我們之前基于 dubbo 開發一個線上系統時候遇到的內存泄漏生產問題的排查與優化實踐經驗。
相信對于大家多看一些類似的案例,以后對于大家自己在線上系統遇到各種生產問題的時候,進行排查和優化的思路會有很大的啟發。
事故背景
先給大家簡單說一下這個問題的發生背景,線上生產環境部署了兩個系統,我們可以認為是系統 A 和系統 B,同時系統 B 因為是大流量核心系統,所以部署了幾十臺機器,定位就是集群部署要抗每秒幾萬的 TPS 的,兩臺系統之間是基于 dubbo 作為 rpc 調用框架,注冊中心用的是 zookeeper。
如下圖所示:
在這個背景之下,某一天系統 B 因為更新了代碼,因此發起了一次幾十臺機器的全量滾動更新和部署。
也就是說,系統 B 的開發團隊基于最新的代碼把幾十臺機器依次用最新代碼重新部署了一遍,也就是每臺機器都會有一次系統停止和重啟的過程。
如下圖所示:
沒想到生產環境的災難性故障就這么突然發生了,在系統 B 的幾十臺機器依次重新部署之后,結果系統 A 的開發團隊驚訝的發現自己的系統居然過了一會就發送了 jvm 內存使用率飆升超過 90% 的告警,而且很快系統 A 居然就直接 OOM 內存溢出崩潰了。
如下圖所示:
于是系統 B 的開發團隊順利的把一個大版本更新了幾十臺機器之后,心滿意足的欣賞自己的成果呢,系統 A 的開發團隊突然開始一臉懵逼的手忙腳亂進行了生產故障的排查。
那么大家可以想想,這個時候,如果是你負責的線上系統突然給你發送內存使用率飆升超過 90%,而且很快就 oom 內存溢出,你會怎么排查?
排查思路
這里給大家說說當時我們是怎么進行排查的,首先,遇到這種內存突然飆升然后導致 oom 的情況,先看看是不是外部對你的請求流量過大導致的。
因為往往這種突發性的問題,都是外部流量突然飆升導致的,這里先給分析一種外部流量突然飆升導致系統 oom 的場景。
假設你平時常規化運作的時候,每次一批請求過來會在你的 jvm 年輕代里創建一批對象,接著這批請求處理完畢了,之前創建的那批對象就會成為垃圾對象了,然后下一批請求過來,又在 jvm 年輕代里創建了一批對象。
如下圖所示:
那么正常情況下,你的 jvm 年輕代里肯定對象會越來越多是不是?但是其實一般到了一定時候,年輕代里的存活對象基本很少,因為大部分的對象都是之前已經處理完畢的請求創建的對象,他們其實都是一些沒用的垃圾對象。
所以其實正常情況下跑一段時間后,會觸發一下 jvm 年輕代的垃圾回收,把垃圾對象都回收掉就行了。
如下圖:
所以正常情況下,是不會出現什么問題的,但是如果是突發性的大流量來襲呢?
這個時候就不好說了,因為很可能在短時間內突然涌入大量的請求,這些請求創建了大量的對象,瞬間就填滿了年輕代,然后這個時候觸發年輕代 gc 后,發現大量的對象是沒法回收的,此時只能怎么辦?
只能把這些對象轉移到老年代里去了,如下圖:
那么這個時候年輕代里的大量存活對象都轉移到老年代里去了,老年代里幾乎也被填滿了,然后此時年輕代里因為流量太大瞬時再次被填滿,此時年輕代里大量的存活對象該何去何從?這個時候你去老年代嗎?
老年代都塞滿了存活對象,即使觸發了老年代 gc 也沒法回收他們,年輕代也沒地方放這些存活對象了,這個時候會如何?
很簡單,由于瞬時并發流量太大,同時創建了太多的存活對象,塞滿了老年代和年輕代,我們很可能會收到報警說 jvm 年輕代和老年代內存使用率都超過了 90%。
而且這些對象都是存活的都沒法回收,此時再要創建新的對象,就沒地方創建了,接著就會報出 oom 內存溢出異常來了。
如下圖:
所以說瞬時流量激增可能會導致系統 A 發送內存使用率超過 90%,而且很快就 oom 的問題,但是到底是不是這個問題導致的呢?
雖然我們可以思路順暢的推演出上述場景,但是我們這個時候趕緊看一下系統 A 的線上 QPS 指標監控,結果一臉懵逼的發現,系統 A 根本就沒有流量激增,人家的流量一切都很平穩,所以根本不是這個原因導致的問題。
那既然不是這個問題,那還有什么問題會導致這個現象呢?
很簡單,第二種問題就是內存泄漏,也就是說,在某種特殊條件下,觸發了一個內存泄漏的行為,就是你的系統不停的產生某一類對象,這一類對象明明都不用了,結果還一直放在內存里,而且根本回收不掉。
就這么不停的積累這類對象,就會導致內存使用率不停的攀升,最后導致 oom 內存溢出。
如下圖:
那么針對這個內存泄漏的問題,這個時候我們到底應該怎么排查呢?很簡單,這個時候你到底是真程序員還是假程序員,得亮亮真功夫了。
往往這種內存類的問題,過段的用 jmap 這個命令,去對線上運行的系統 jvm 進程生成一個內存 dump 快照出來,然后把 dump 快照下載到本地,用 MAT這個工具就可以分析這個內存快照。
在 MAT 工具中我們會看到你的 jvm 里到底是什么破對象占用了那么大的空間,才導致了你的內存使用率飆升到 90%+ 的。
這個時候其實導致內存泄漏的原因有很多種,比如說你們自己代碼寫的不好,就是每次請求都創建某一類對象,這類對象給扔到某個 class 的靜態 map 里一直放著,從來不回收,也沒法回收,導致這類無用對象一直增長,最后導致了 oom。
另外還有一種比較常見的現象,就是我們的系統使用了一些開源框架,這些開源框架在某種特殊場景下創建了一堆的對象,沒法回收,他自己也從來不回收,導致了開源框架悄咪咪創建的這批對象占用了大量內存,導致了內存泄漏。
所以在這里給大家說一下我們當時遇到的一個問題,大家重點吸收排查思路,下面的具體 case by case 的個別案例可以作為一個例子看一下。
排查案例
就我們當時的 case 來說,經過 MAT 一通排查,發現占用了大量內存的對象是 dubbo 框架創建的,dubbo 框架創建了一種用于進行 rpc 調用的大對象,這類對象一直創建一直增長,然后從來不回收,最后導致了內存泄漏和內存溢出。
如下圖:
那么 dubbo 框架為什么會不停的創建一類用于進行 rpc 調用的對象呢?
這就得分析 dubbo 框架的源碼了,當時經過 dubbo 框架源碼的分析,我們得出了以下的問題發生流程:
當系統 B 在線上進行幾十臺機器的滾動發布的時候,每一臺機器被發布,都會導致注冊中心感知到服務變動,然后注冊中心會把這幾十臺機器的地址列表都給系統 A 推送過去。
也就是說,連續發布幾十臺機器,就會導致注冊中心推送幾十次最新地址列表,每一次推送都包含了幾十臺機器的地址。
因此,假設系統 B 部署了 50 臺機器,等于隨著 50 臺機器依次重新發布,會導致注冊中心一共給系統 A 推送 50*50=2500 條機器地址。
如下圖:
而系統 A 的 dubbo 框架等于會收到短時間內頻繁推送的幾千條機器地址,然后對每條機器地址,其實 dubbo 框架都會去創建一個對應的 rpc 調用類的對象。
如下圖所示:
其實本來 dubbo 創建幾千次 rpc 調用對象也沒什么,但是問題就出在了一個特殊的 case 上了。
那就是系統 B 那邊并沒有去設置對外提供的是什么 rpc 協議,因為 dubbo 是支持多種不同的 rpc 協議的,比如說 dubbo 協議、http 協議,等等。
所以在當時的那個較老的 dubbo 版本中,就出現了一個隱藏的問題,就是如果系統 B 沒設置具體對外提供的協議版本,就會導致系統 A 收到幾千條機器地址后,除了創建 dubbo 協議的對象,還會創建幾千個基于 http rest 類協議的 rpc 調用對象。
可是系統 B 又沒提供 http rest 接口,因此創建會全部失敗,但是背后創建的大量對象又會放著,沒法回收。
這就導致了 dubbo 框架不停的創建出來大量的對象,占用了 90% 的內存,最后導致了內存溢出。
如下圖:
那么這個問題是如何解決的呢?其實問題的核心在于排查思路和背后的原理,最后問題的解決往往是 case by case 的。
比如我們這個case里,其實就很簡單,就是要讓系統 B 設置好對外提供的 dubbo protocol 協議,避免上面那種因為 protocol 協議沒設置導致創建了大量的無用對象沒法回收。
總結
最后希望大家看完今天的生產排查與優化案例后,未來在自己工作中遇到了類似的問題,能給大家提供一種問題排查的思路幫助大家。