0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

一步步解決長連接Netty服務(wù)內(nèi)存泄漏

OSC開源社區(qū) ? 來源:OSCHINA 社區(qū) ? 2023-04-27 14:06 ? 次閱讀

線上應(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)吞吐量

dab1abd8-e4c0-11ed-ab56-dac502259ad0.png 這個長連接服務(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):

dac11424-e4c0-11ed-ab56-dac502259ad0.png

每次發(fā)生內(nèi)存泄漏,內(nèi)存快耗盡時,總得重啟下,雖說重啟是最快解決的方法,但是程序員是天生懶惰的,要數(shù)著日子來重啟,那絕對不是一個優(yōu)秀程序員的行為!問題必須徹底解決!

問題排查與復(fù)現(xiàn)

排查

遇到問題,毫無頭緒,首先還是需要去案發(fā)第一現(xiàn)場,排查 “死者 (應(yīng)用實例)” 死亡現(xiàn)場,通過在發(fā)生 FullGC 的時間點,通過 Digger 查詢ERROR日志,沒想到還真找到破案的第一線索:

dac7f78a-e4c0-11ed-ab56-dac502259ad0.png

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

dad006aa-e4c0-11ed-ab56-dac502259ad0.png

2、模擬足夠多的長連接請求,我們使用 Postman 定時批量發(fā)請求,以達(dá)到服務(wù)的堆外內(nèi)存泄漏。

啟動項目,通過JProfilerJVM 監(jiān)控工具,我們觀察到內(nèi)存緩慢的增長,最終觸發(fā)了本地Netty的堆外內(nèi)存泄漏,本地復(fù)現(xiàn)成功:

dadcaae0-e4c0-11ed-ab56-dac502259ad0.jpg

dae86272-e4c0-11ed-ab56-dac502259ad0.png

_那問題具體出現(xiàn)在代碼中哪塊呢?_我們最重要的是定位具體代碼,在開啟了Netty的高級內(nèi)存泄漏級別為高級,來定位下:

3、開啟Netty的高級內(nèi)存泄漏檢測級別,JVM 參數(shù)如下:

-Dio.netty.leakDetectionLevel=advanced

daf2393c-e4c0-11ed-ab56-dac502259ad0.png

再啟動項目,模擬請求,達(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)

dafa8b00-e4c0-11ed-ab56-dac502259ad0.jpg

不得不說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)險也大,因此使用方式三,如下:

db039efc-e4c0-11ed-ab56-dac502259ad0.jpg

線上實例內(nèi)存正常

問題修復(fù)后,線上服務(wù)正常,內(nèi)存使用率也沒有再出現(xiàn)因泄漏而增長,從線上我們增加的日志中看出,F(xiàn)ullHttpRequest中ByteBuf內(nèi)存釋放成功。從此長連接前置內(nèi)存泄漏的問題徹底解決。

db0d8390-e4c0-11ed-ab56-dac502259ad0.png

總結(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 改動會比較大。





審核編輯:劉清

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • JVM
    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)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    虛擬現(xiàn)實正一步步向我們走來

    顯然,虛擬現(xiàn)實大發(fā)展的春天正在到來,虛擬現(xiàn)實正一步步走向消費者。那么,虛擬現(xiàn)實究竟涉及哪些重點技術(shù)領(lǐng)域?又將對整個產(chǎn)業(yè)帶來怎樣的影響?未來的“抓手”又有哪些?
    發(fā)表于 10-26 16:38 ?874次閱讀

    外國牛人教你一步步快速打造首臺機(jī)器人(超詳細(xì))

    外國牛人教你一步步快速打造首臺機(jī)器人(超詳細(xì))
    發(fā)表于 08-15 19:30

    一步步寫嵌入式操作系統(tǒng)—ARM編程的方法與實踐ch02

    一步步寫嵌入式操作系統(tǒng)—ARM編程的方法與實踐ch02
    發(fā)表于 08-20 20:54

    CC2530一步步演示程序燒寫

    CC2530一步步演示程序燒寫第一步——先安裝IAR開發(fā)環(huán)境第二歩——安裝CC2530燒寫工具第三歩——CC2530串口配置軟件使用具體完整步驟看下面文檔
    發(fā)表于 03-03 14:33

    一步步建立_STM32_UCOS_模板

    一步步建立_STM32_UCOS_模板
    發(fā)表于 09-29 11:46

    菜鳥一步步入門SAM4S-XPLAINED--IAR開發(fā)環(huán)境

    菜鳥一步步入門SAM4S-XPLAINED--IAR開發(fā)環(huán)境
    發(fā)表于 01-25 10:55

    一步步進(jìn)行調(diào)試GPRS模塊

    背景:在不知道硬件是否正確情況下,一步步進(jìn)行調(diào)試,最終完成調(diào)試。以下是自己調(diào)試步驟。1、從gprs模塊TX ,RX 單獨焊接兩個線出來,通過上位機(jī)發(fā)送AT指令,是否能正常工作。
    發(fā)表于 01-25 07:33

    stm32是如何一步步實現(xiàn)設(shè)置地址匹配接收喚醒中斷功能的

    為什么要設(shè)置地址匹配接收喚醒中斷呢?stm32是如何一步步實現(xiàn)設(shè)置地址匹配接收喚醒中斷功能的?
    發(fā)表于 02-28 08:07

    手把手教內(nèi)存條超頻 一步步爬上DDR600

    手把手教內(nèi)存條超頻 一步步爬上DDR600  隨著電腦配件的價格逐漸走低,很多玩家開始不僅為了省錢而攢機(jī),而越來越趨向于國外
    發(fā)表于 01-12 09:35 ?3553次閱讀

    一步步寫嵌入式操作系統(tǒng)

    一步步寫嵌入式操作系統(tǒng)_ARM編程的方法與實踐
    發(fā)表于 07-14 11:32 ?0次下載

    看電工技術(shù)是如何一步步淪為勤雜工的

    相信很多的電工老師傅也都聽說過這種話,那電工究竟是不是勤雜工?電工技術(shù)工種是如何一步步的淪為勤雜工的,我們今天就重點來看看。
    的頭像 發(fā)表于 02-18 15:47 ?4103次閱讀

    看電路是怎么把電壓一步步頂上去的?資料下載

    電子發(fā)燒友網(wǎng)為你提供看電路是怎么把電壓一步步頂上去的?資料下載的電子資料下載,更有其他相關(guān)的電路圖、源代碼、課件教程、中文資料、英文資料、參考設(shè)計、用戶指南、解決方案等資料,希望可以幫助到廣大的電子工程師們。
    發(fā)表于 04-16 08:47 ?13次下載
    看電路是怎么把電壓<b class='flag-5'>一步步</b>頂上去的?資料下載

    ROM與RAM 單片機(jī)上電后如何一步步執(zhí)行?資料下載

    電子發(fā)燒友網(wǎng)為你提供ROM與RAM 單片機(jī)上電后如何一步步執(zhí)行?資料下載的電子資料下載,更有其他相關(guān)的電路圖、源代碼、課件教程、中文資料、英文資料、參考設(shè)計、用戶指南、解決方案等資料,希望可以幫助到廣大的電子工程師們。
    發(fā)表于 04-21 08:53 ?12次下載
    ROM與RAM 單片機(jī)上電后如何<b class='flag-5'>一步步</b>執(zhí)行?資料下載

    一步步重新演繹汽車駕駛體驗

    一步步重新演繹汽車駕駛體驗
    發(fā)表于 11-04 09:52 ?0次下載
    <b class='flag-5'>一步步</b>重新演繹汽車駕駛體驗

    基于一步步蒸餾(Distilling step-by-step)機(jī)制

    為優(yōu)化LLM為“小模型/少數(shù)據(jù)/好效果”,提供了種新思路:”一步步蒸餾”(Distilling step-by-step)
    的頭像 發(fā)表于 05-16 10:24 ?1119次閱讀
    基于<b class='flag-5'>一步步</b>蒸餾(Distilling step-by-step)機(jī)制