作為一名Java開發者,我們都知道Java程序是執行在Java虛擬機器上的,而Java程序要想正常執行則需要向計算機申請記憶體,其中主要為Java物件例項所佔用的堆(heap)記憶體(當然還有其他的也會佔用記憶體,比如棧等),這些記憶體一般劃分為Java虛擬機器所佔記憶體。
在當今網路通訊過程中,不可避免地需要用到高效能IO通訊框架Netty,Spring Cloud Gateway也不例外用到了Netty進行網路通訊,當然還有很多框架也都應用到了Netty,比如:Dubbo、RocketMQ等等。而Netty為了減少網路通訊過程中資料的複製,也就是使用者態,核心態之間資料的複製,會大量地分配直接記憶體,相對於Java虛擬機器的堆記憶體而言,相當於是堆外記憶體。
而我們本次出現的線上事故也和Netty的直接記憶體相關。
場景再現上週四中午,睡得正香,突然線上出現了大量介面502(Http 502錯誤表示的是閘道器錯誤,這個問題是由後端伺服器之間不良的IP通訊造成的,可能包括正在嘗試訪問的網站的 Web 伺服器)報警,同時運維監控到我們組剛上線的內網閘道器發生宕機,情急之下馬上先重啟了閘道器服務(萬能的重啟)重啟之後,服務介面可用,不在報警,然後開始排查具體產生宕機的原因,首先跟蹤的具體日誌如下:
錯誤日誌
io.netty.util.internal.OutOfDirectMemoryError: failed to allocate
看到以上的日誌,大體可以知道是直接記憶體分配不足導致,為什麼會出現分配不足呢,於是有看了最近幾天運維監控記憶體分配情況,如下:
記憶體申請分配
其實從上圖可以看出,自從服務上線後已用記憶體就一直在申請、上升,沒有釋放,那麼接下來就是定位為什麼會出現記憶體不釋放的問題了,因為我們應用的閘道器專案是使用的Spring Cloud Gateway進行搭建的,而Spring Cloud Gateway又是使用的Netty框架進行搭建的,這正好和以上報錯io.netty.util.internal.OutOfDirectMemoryError日誌恰巧對應上,下面就查閱了好多資料,說Gateway低版本確實存在過該問題,升級版本即可解決此類問題,於是將現有的Spring Cloud版本在Finchley基礎上升到了Hoxton,並在模擬環境進行了壓測(併發1000),壓了半個小時,並沒有出現宕機異常,於是當天晚上就將程式碼進行上線,但是上線之後檢視ELK日誌,發現還是存在很多的錯誤日誌如下:
記憶體洩漏日誌
LEAK: ByteBuf.release() was not called before it's garbage-collected
竟然是記憶體洩漏最終導致的記憶體溢位,按理說像Spring Cloud Gateway這麼成熟的框架不應該會出現類似的問題,於是排查我們的專案程式碼,發現竟然是我們自己閘道器專案的一個全域性過濾XSS攻擊的filter,裡面有使用Netty的一個databuffer,但是這個databuffer沒有進行釋放導致,於是將該databuffer進行手工釋放DataBufferUtils.release(dataBuffer); 修改完該瑕疵之後,線上記憶體監控趨於平穩,如下圖:
記憶體申請使用監控
總結解決此類記憶體溢位問題、JVM問題快速的方法一定是結合監控和日誌進行排查,因為沒有監控和日誌我們就無從下手,可能只能考經驗和猜,但是這樣無疑會浪費大量的時間,所以平時一定要做好監控,以防關鍵時候手忙腳亂。
還有就是開源的優秀的框架是個好東西,但是我們在使用的過程中一定事先做好評估,也就是可能會遇到問題,帶來的弊端,像Netty我們在使用過程中要對記憶體分配,IO有一定的瞭解;使用MQ要了解MQ可能會有訊息重發、訊息順序、訊息丟失等問題;使用Redis作快取,需要了解如何防止快取雪崩、快取穿透等一系列問題。
最後,透過本次線上事故我們也認識到了記憶體洩漏可能會造成記憶體溢位的嚴重問題,記憶體洩漏不可小覷,使用ThreadLocal時候也得注意。
不斷分享開發過程用到的技術和麵試經常被問到的問題,如果您也對IT技術比較感興趣可以「關注」我