線上應(yīng)用長連接 Netty 服務(wù)出現(xiàn)內(nèi)存泄漏了!
應(yīng)用介紹
說起支付業(yè)務(wù)的長連接服務(wù),真是說來話長,我們這就長話短說: 隨著業(yè)務(wù)及系統(tǒng)架構(gòu)的復(fù)雜化,一些場景,用戶操作無法同步得到結(jié)果。一般采用的短連接輪訓(xùn)的策略,客戶端需要不停的發(fā)起請求,時效性較差還浪費服務(wù)器資源。 短輪訓(xùn)痛點:
時效性差
耗費服務(wù)器性能
建立、關(guān)閉鏈接頻繁
相比于短連接輪訓(xùn)策略,長連接服務(wù)可做到實時推送數(shù)據(jù),并且在一個鏈接保持期間可進(jìn)行多次數(shù)據(jù)推送。服務(wù)應(yīng)用常見場景:PC 端掃碼支付,用戶打開掃碼支付頁面,手機(jī)掃碼完成支付,頁面實時展示支付成功信息,提供良好的用戶體驗。 長連服務(wù)優(yōu)勢:
時效性高提升用戶體驗
減少鏈接建立次數(shù)
一次鏈接多次推送數(shù)據(jù)
提高系統(tǒng)吞吐量
這個長連接服務(wù)使用?Netty?框架,Netty?的高性能為這個應(yīng)用帶來了無上的榮光,承接了眾多長連接使用場景的業(yè)務(wù):
PC 收銀臺微信支付
聲波紅包
POS 線下掃碼支付
問題現(xiàn)象
回到線上問題,出現(xiàn)內(nèi)存泄漏的是長連接前置服務(wù),觀察線上服務(wù),這個應(yīng)用的內(nèi)存泄漏的現(xiàn)象總伴隨著內(nèi)存的增長,這個增長真是非常的緩慢,緩慢,緩慢,2、3 個月內(nèi)從 30% 慢慢增長到 70%,極難發(fā)現(xiàn):
每次發(fā)生內(nèi)存泄漏,內(nèi)存快耗盡時,總得重啟下,雖說重啟是最快解決的方法,但是程序員是天生懶惰的,要數(shù)著日子來重啟,那絕對不是一個優(yōu)秀程序員的行為!問題必須徹底解決!
問題排查與復(fù)現(xiàn)
排查
遇到問題,毫無頭緒,首先還是需要去案發(fā)第一現(xiàn)場,排查 “死者 (應(yīng)用實例)” 死亡現(xiàn)場,通過在發(fā)生 FullGC 的時間點,通過 Digger 查詢ERROR日志,沒想到還真找到破案的第一線索:
io.netty.util.ResourceLeakDetector [176] - LEAK: ByteBuf.release() was not called before it's garbage-collected. Enable advanced leak reporting to find out where the leak occurred. To enable advanced leak reporting, specify the JVM option '-Dio.netty.leakDetection.level=advanced' or call ResourceLeakDetector.setLevel() See http://netty.io/wiki/reference-counted-objects.html for more information.線上日志竟然有一個明顯的"LEAK"泄漏字樣,作為技術(shù)人的敏銳的技術(shù)嗅覺,和找 Bug 的直覺,可以確認(rèn),這就是事故案發(fā)第一現(xiàn)場。 我們憑借下大學(xué)四六級英文水平的,繼續(xù)翻譯下線索,原來是這吶!
ByteBuf.release() 在垃圾回收之前沒有被調(diào)用。啟用高級泄漏報告以找出泄漏發(fā)生的位置。要啟用高級泄漏報告,請指定 JVM 選項 “-Dio.netty.leakDetectionLevel=advanced” 或調(diào)用 ResourceLeakDetector.setLevel()
啊哈!這信息不就是說了嘛!ByteBuf.release()在垃圾回收前沒有調(diào)用,有ByteBuf對象沒有被釋放,ByteBuf可是分配在直接內(nèi)存的,沒有被釋放,那就意味著堆外內(nèi)存泄漏,所以內(nèi)存一直是非常緩慢的增長,GC 都不能夠進(jìn)行釋放。
提供了這個線索,那到底是我們應(yīng)用中哪段代碼出現(xiàn)了ByteBuf對象的內(nèi)存泄漏呢?
項目這么大,Netty 通信處理那么多,怎么找呢?自己從中搜索,那肯定是不靠譜,找到了又怎么釋放呢?
復(fù)現(xiàn)
面對這一連三問?別著急,Netty 的日志提示還是非常完善:啟用高級泄漏報告找出泄漏發(fā)生位置嘛,生產(chǎn)上不可能啟用,并且生產(chǎn)發(fā)生時間極長,時間上來不及,而且未經(jīng)驗證,不能直接生產(chǎn)發(fā)布,那就本地代碼復(fù)現(xiàn)一下!找到具體代碼位置。
為了本地復(fù)現(xiàn)Netty泄漏,定位詳細(xì)的內(nèi)存泄漏代碼,我們需要做這幾步:
1、配置足夠小的本地 JVM 內(nèi)存,以便快速模擬堆外內(nèi)存泄漏。
如圖,我們設(shè)置設(shè)置 PermSize=30M, MaxPermSize=43M
2、模擬足夠多的長連接請求,我們使用 Postman 定時批量發(fā)請求,以達(dá)到服務(wù)的堆外內(nèi)存泄漏。
啟動項目,通過JProfilerJVM 監(jiān)控工具,我們觀察到內(nèi)存緩慢的增長,最終觸發(fā)了本地Netty的堆外內(nèi)存泄漏,本地復(fù)現(xiàn)成功:
_那問題具體出現(xiàn)在代碼中哪塊呢?_我們最重要的是定位具體代碼,在開啟了Netty的高級內(nèi)存泄漏級別為高級,來定位下:
3、開啟Netty的高級內(nèi)存泄漏檢測級別,JVM 參數(shù)如下:
-Dio.netty.leakDetectionLevel=advanced
再啟動項目,模擬請求,達(dá)到本地應(yīng)用 JVM 內(nèi)存泄漏,Netty 輸出如下具體日志信息,可以看到,具體的日志信息比之前的信息更加完善:
2020-09-24 2059.078 [nioEventLoopGroup-3-1] INFO io.netty.handler.logging.LoggingHandler [101] - [id: 0x2a5e5026, L:/00008883] READ: [id: 0x926e140c, L:/127.0.0.1:8883 - R:/127.0.0.1:58920] 2020-09-24 2059.078 [nioEventLoopGroup-3-1] INFO io.netty.handler.logging.LoggingHandler [101] - [id: 0x2a5e5026, L:/00008883] READ COMPLETE 2020-09-24 2059.079 [nioEventLoopGroup-2-8] ERROR io.netty.util.ResourceLeakDetector [171] - LEAK: ByteBuf.release() was not called before it's garbage-collected. See http://netty.io/wiki/reference-counted-objects.html for more information. WARNING: 1 leak records were discarded because the leak record count is limited to 4. Use system property io.netty.leakDetection.maxRecords to increase the limit. Recent access records: 5 #5: io.netty.buffer.AdvancedLeakAwareCompositeByteBuf.readBytes(AdvancedLeakAwareCompositeByteBuf.java:476) io.netty.buffer.AdvancedLeakAwareCompositeByteBuf.readBytes(AdvancedLeakAwareCompositeByteBuf.java:36) com.jd.jr.keeplive.front.service.nettyServer.handler.LongRotationServerHandler.getClientMassageInfo(LongRotationServerHandler.java:169) com.jd.jr.keeplive.front.service.nettyServer.handler.LongRotationServerHandler.handleHttpFrame(LongRotationServerHandler.java:121) com.jd.jr.keeplive.front.service.nettyServer.handler.LongRotationServerHandler.channelRead(LongRotationServerHandler.java:80) io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:362) io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:348) io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:340) io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:86) ...... #4: Hint: 'LongRotationServerHandler#0' will handle the message from this point. io.netty.buffer.AdvancedLeakAwareCompositeByteBuf.touch(AdvancedLeakAwareCompositeByteBuf.java:1028) io.netty.buffer.AdvancedLeakAwareCompositeByteBuf.touch(AdvancedLeakAwareCompositeByteBuf.java:36) io.netty.handler.codec.http.HttpObjectAggregator$AggregatedFullHttpMessage.touch(HttpObjectAggregator.java:359) ...... #3: Hint: 'HttpServerExpectContinueHandler#0' will handle the message from this point. io.netty.buffer.AdvancedLeakAwareCompositeByteBuf.touch(AdvancedLeakAwareCompositeByteBuf.java:1028) io.netty.buffer.AdvancedLeakAwareCompositeByteBuf.touch(AdvancedLeakAwareCompositeByteBuf.java:36) io.netty.handler.codec.http.HttpObjectAggregator$AggregatedFullHttpMessage.touch(HttpObjectAggregator.java:359) ...... #2: Hint: 'HttpHeartbeatHandler#0' will handle the message from this point. io.netty.buffer.AdvancedLeakAwareCompositeByteBuf.touch(AdvancedLeakAwareCompositeByteBuf.java:1028) io.netty.buffer.AdvancedLeakAwareCompositeByteBuf.touch(AdvancedLeakAwareCompositeByteBuf.java:36) io.netty.handler.codec.http.HttpObjectAggregator$AggregatedFullHttpMessage.touch(HttpObjectAggregator.java:359) ...... #1: Hint: 'IdleStateHandler#0' will handle the message from this point. io.netty.buffer.AdvancedLeakAwareCompositeByteBuf.touch(AdvancedLeakAwareCompositeByteBuf.java:1028) io.netty.buffer.AdvancedLeakAwareCompositeByteBuf.touch(AdvancedLeakAwareCompositeByteBuf.java:36) io.netty.handler.codec.http.HttpObjectAggregator$AggregatedFullHttpMessage.touch(HttpObjectAggregator.java:359) ...... Created at: io.netty.util.ResourceLeakDetector.track(ResourceLeakDetector.java:237) io.netty.buffer.AbstractByteBufAllocator.compositeDirectBuffer(AbstractByteBufAllocator.java:217) io.netty.buffer.AbstractByteBufAllocator.compositeBuffer(AbstractByteBufAllocator.java:195) io.netty.handler.codec.MessageAggregator.decode(MessageAggregator.java:255) ......開啟高級的泄漏檢測級別后,通過上面異常日志,我們可以看到內(nèi)存泄漏的具體地方:com.jd.jr.keeplive.front.service.nettyServer.handler.LongRotationServerHandler.getClientMassageInfo(LongRotationServerHandler.java:169)
不得不說Netty內(nèi)存泄漏排查這點是真香!真香好評!
問題解決
找到問題了,那我么就需要解決,如何釋放ByteBuf內(nèi)存呢?
如何回收泄漏的 ByteBuf
其實Netty官方也針對這個問題做了專門的討論,一般的經(jīng)驗法則是,最后訪問引用計數(shù)對象的一方負(fù)責(zé)銷毀該引用計數(shù)對象,具體來說:
如果一個 [發(fā)送] 組件將一個引用計數(shù)的對象傳遞給另一個 [接收] 組件,則發(fā)送組件通常不需要銷毀它,而是由接收組件進(jìn)行銷毀。
如果一個組件使用了一個引用計數(shù)的對象,并且知道沒有其他對象將再訪問它(即,不會將引用傳遞給另一個組件),則該組件應(yīng)該銷毀它。
總結(jié)起來主要三個方式:
方式一:手動釋放,哪里使用了,使用完就手動釋放。
方式二:升級ChannelHandler為SimpleChannelHandler, 在SimpleChannelHandler中,Netty對收到的所有消息都調(diào)用了ReferenceCountUtil.release(msg)。
方式三:如果處理過程中不確定ByteBuf是否應(yīng)該被釋放,那交給 Netty 的ReferenceCountUtil.release(msg)來釋放,這個方法會判斷上下文是否可以釋放。 考慮到長連接前置應(yīng)用使用的是ChannelHandler,如果升級SimpleChannelHandler對現(xiàn)有 API 接口變動比較大,同時如果手動釋放,不確定是否應(yīng)該釋放風(fēng)險也大,因此使用方式三,如下:
線上實例內(nèi)存正常
問題修復(fù)后,線上服務(wù)正常,內(nèi)存使用率也沒有再出現(xiàn)因泄漏而增長,從線上我們增加的日志中看出,F(xiàn)ullHttpRequest中ByteBuf內(nèi)存釋放成功。從此長連接前置內(nèi)存泄漏的問題徹底解決。
總結(jié)
一、Netty 的內(nèi)存泄漏排查其實并不難,Netty 提供了比較完整的排查內(nèi)存泄漏工具 JVM 選項-Dio.netty.leakDetection.level 目前有 4 個泄漏檢測級別的:
DISABLED - 完全禁用泄漏檢測。不推薦。
SIMPLE - 抽樣 1% 的緩沖區(qū)是否有泄漏。默認(rèn)。
ADVANCED - 抽樣 1% 的緩沖區(qū)是否泄漏,以及能定位到緩沖區(qū)泄漏的代碼位置。
PARANOID - 與 ADVANCED 相同,只是它適用于每個緩沖區(qū),適用于自動化測試階段。如果生成輸出包含 “LEAK:”,則可能會使生成失敗。
本次內(nèi)存泄漏問題,我們通過本地設(shè)置泄漏檢測級別為高級,即:-Dio.netty.leakDetectionLevel=advanced定位到了具體內(nèi)存泄漏的代碼。 同時 Netty 也給出了避免泄漏的最佳實踐:
在 PARANOID 泄漏檢測級別以及 SIMPLE 級別運行單元測試和集成測試。
在 SIMPLE 級別向整個集群推出應(yīng)用程序之前,請先在相當(dāng)長的時間內(nèi)查看是否存在泄漏。
如果有泄漏,灰度發(fā)布中使用 ADVANCED 級別,以獲得有關(guān)泄漏來源的一些提示。
不要將泄漏的應(yīng)用程序部署到整個群集。
二、解決 Netty 內(nèi)存泄漏,Netty 也提供了指導(dǎo)方案,主要有三種方式 方式一:手動釋放,哪里使用了,使用完就手動釋放,這個對使用方要求比較高了。
方式二:如果處理過程中不確定ByteBuf是否應(yīng)該被釋放,那交給Netty的ReferenceCountUtil.release(msg)來釋放,這個方法會判斷上下文中是否可以釋放,簡單方便。
方式三:升級ChannelHandler為SimpleChannelHandler, 在 SimpleChannelHandler 中,Netty 對收到的所有消息都調(diào)用了ReferenceCountUtil.release(msg),升級接口,可能對現(xiàn)有 API 改動會比較大。
審核編輯:劉清
-
JVM
+關(guān)注
關(guān)注
0文章
157瀏覽量
12197 -
內(nèi)存泄漏
+關(guān)注
關(guān)注
0文章
39瀏覽量
9200
原文標(biāo)題:一步步解決長連接 Netty 服務(wù)內(nèi)存泄漏
文章出處:【微信號:OSC開源社區(qū),微信公眾號:OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論