一区二区三区在线-一区二区三区亚洲视频-一区二区三区亚洲-一区二区三区午夜-一区二区三区四区在线视频-一区二区三区四区在线免费观看

服務器之家:專注于服務器技術及軟件下載分享
分類導航

PHP教程|ASP.NET教程|Java教程|ASP教程|編程技術|正則表達式|C/C++|IOS|C#|Swift|Android|VB|R語言|JavaScript|易語言|vb.net|

服務器之家 - 編程語言 - 編程技術 - 從線上偶發(fā)的宕機事件看Netty流量控制

從線上偶發(fā)的宕機事件看Netty流量控制

2021-09-23 23:34Kirito的技術分享 編程技術

目前移動端的使用場景中會用到大量的消息推送,push消息可以幫助運營人員更高效地實現(xiàn)運營目標(比如給用戶推送營銷活動或者提醒APP新功能)。

 從線上偶發(fā)的宕機事件看Netty流量控制

業(yè)務背景

目前移動端的使用場景中會用到大量的消息推送,push消息可以幫助運營人員更高效地實現(xiàn)運營目標(比如給用戶推送營銷活動或者提醒APP新功能)。

對于推送系統(tǒng)來說需要具備以下兩個特性:

  • 消息秒級送到用戶,無延時,支持每秒百萬推送,單機百萬長連接。

  • 支持通知、文本、自定義消息透傳等展現(xiàn)形式。正是由于以上原因,對于系統(tǒng)的開發(fā)和維護帶來了挑戰(zhàn)。下圖是推送系統(tǒng)的簡單描述(API->推送模塊->手機)。

從線上偶發(fā)的宕機事件看Netty流量控制

問題背景

推送系統(tǒng)中長連接集群在穩(wěn)定性測試、壓力測試階運行一段時間后隨機會出現(xiàn)一個進程掛掉的情況,概率較小(頻率為一個月左右發(fā)生一次),這會影響部分客戶端消息送到的時效。

推送系統(tǒng)中的長連接節(jié)點(Broker系統(tǒng))是基于Netty開發(fā),此節(jié)點維護了服務端和手機終端的長連接,線上問題出現(xiàn)后,添加Netty內存泄露監(jiān)控參數進行問題排查,觀察多天但并未排查出問題。

由于長連接節(jié)點是Netty開發(fā),為便于讀者理解,下面簡單介紹一下Netty。

Netty介紹

Netty是一個高性能、異步事件驅動的NIO框架,基于Java NIO提供的API實現(xiàn)。它提供了對TCP、UDP和文件傳輸的支持,作為當前最流行的NIO框架,Netty在互聯(lián)網領域、大數據分布式計算領域、游戲行業(yè)、通信行業(yè)等獲得了廣泛的應用,HBase,Hadoop,Bees,Dubbo等開源組件也基于Netty的NIO框架構建。

問題分析

猜想

最初猜想是長連接數導致的,但經過排查日志、分析代碼,發(fā)現(xiàn)并不是此原因造成。

長連接數:39萬,如下圖:

從線上偶發(fā)的宕機事件看Netty流量控制

 

連接數 

 

每個channel字節(jié)大小1456, 按40萬長連接計算,不致于產生內存過大現(xiàn)象。

查看GC日志

查看GC日志,發(fā)現(xiàn)進程掛掉之前頻繁full GC(頻率5分鐘一次),但內存并未降低,懷疑堆外內存泄露。

分析heap內存情況

ChannelOutboundBuffer對象占將近5G內存,泄露原因基本可以確定:ChannelOutboundBuffer的entry數過多導致,查看ChannelOutboundBuffer的源碼可以分析出,是ChannelOutboundBuffer中的數據。

沒有寫出去,導致一直積壓;

ChannelOutboundBuffer內部是一個鏈表結構。

從線上偶發(fā)的宕機事件看Netty流量控制

從上圖分析數據未寫出去,為什么會出現(xiàn)這種情況?

代碼中實際有判斷連接是否可用的情況(Channel.isActive),并且會對超時的連接進行關閉。從歷史經驗來看,這種情況發(fā)生在連接半打開(客戶端異常關閉)的情況比較多---雙方不進行數據通信無問題。

按上述猜想,測試環(huán)境進行重現(xiàn)和測試。

1)模擬客戶端集群,并與長連接服務器建立連接,設置客戶端節(jié)點的防火墻,模擬服務器與客戶端網絡異常的場景(即要模擬Channel.isActive調用成功,但數據實際發(fā)送不出去的情況)。
2)調小堆外內存,持續(xù)發(fā)送測試消息給之前的客戶端。消息大小(1K左右)。
3)按照128M內存來計算,實際上調用9W多次就會出現(xiàn)。

問題解決

啟用autoRead機制

當channel不可寫時,關閉autoRead;

  1. public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { 
  2.     if (!ctx.channel().isWritable()) { 
  3.         Channel channel = ctx.channel(); 
  4.         ChannelInfo channelInfo = ChannelManager.CHANNEL_CHANNELINFO.get(channel); 
  5.         String clientId = ""
  6.         if (channelInfo != null) { 
  7.             clientId = channelInfo.getClientId(); 
  8.         } 
  9.  
  10.         LOGGER.info("channel is unwritable, turn off autoread, clientId:{}", clientId); 
  11.         channel.config().setAutoRead(false); 
  12.     } 

當數據可寫時開啟autoRead;

  1. @Override
  2. public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception 
  3.     Channel channel = ctx.channel(); 
  4.     ChannelInfo channelInfo = ChannelManager.CHANNEL_CHANNELINFO.get(channel); 
  5.     String clientId = ""
  6.     if (channelInfo != null) { 
  7.         clientId = channelInfo.getClientId(); 
  8.     } 
  9.     if (channel.isWritable()) { 
  10.         LOGGER.info("channel is writable again, turn on autoread, clientId:{}", clientId); 
  11.         channel.config().setAutoRead(true); 
  12.     } 

說明:

從線上偶發(fā)的宕機事件看Netty流量控制

autoRead的作用是更精確的速率控制,如果打開的時候Netty就會幫我們注冊讀事件。當注冊了讀事件后,如果網絡可讀,則Netty就會從channel讀取數據。那如果autoread關掉后,則Netty會不注冊讀事件。

這樣即使是對端發(fā)送數據過來了也不會觸發(fā)讀事件,從而也不會從channel讀取到數據。當recv_buffer滿時,也就不會再接收數據。

設置高低水位

  1. serverBootstrap.option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(1024 * 10248 * 1024 * 1024)); 

注:高低水位配合后面的isWritable使用

增加channel.isWritable()的判斷

channel是否可用除了校驗channel.isActive()還需要加上channel.isWrite()的判斷,isActive只是保證連接是否激活,而是否可寫由isWrite來決定。

  1. private void writeBackMessage(ChannelHandlerContext ctx, MqttMessage message) { 
  2.     Channel channel = ctx.channel(); 
  3.     //增加channel.isWritable()的判斷
  4.     if (channel.isActive() && channel.isWritable()) { 
  5.         ChannelFuture cf = channel.writeAndFlush(message); 
  6.         if (cf.isDone() && cf.cause() != null) { 
  7.             LOGGER.error("channelWrite error!", cf.cause()); 
  8.             ctx.close(); 
  9.         } 
  10.     } 

注:isWritable可以來控制ChannelOutboundBuffer,不讓其無限制膨脹。其機制就是利用設置好的channel高低水位來進行判斷。

問題驗證

修改后再進行測試,發(fā)送到27W次也并不報錯;

解決思路分析

一般Netty數據處理流程如下:將讀取的數據交由業(yè)務線程處理,處理完成再發(fā)送出去(整個過程是異步的),Netty為了提高網絡的吞吐量,在業(yè)務層與socket之間增加了一個ChannelOutboundBuffer。

在調用channel.write的時候,所有寫出的數據其實并沒有寫到socket,而是先寫到ChannelOutboundBuffer。當調用channel.flush的時候才真正的向socket寫出。因為這中間有一個buffer,就存在速率匹配了,而且這個buffer還是無界的(鏈表),也就是你如果沒有控制channel.write的速度,會有大量的數據在這個buffer里堆積,如果又碰到socket寫不出數據的時候(isActive此時判斷無效)或者寫得慢的情況。

很有可能的結果就是資源耗盡,而且如果ChannelOutboundBuffer存放的是

DirectByteBuffer,這會讓問題更加難排查。

流程可抽象如下:

從線上偶發(fā)的宕機事件看Netty流量控制

從上面的分析可以看出,步驟一寫太快(快到處理不過來)或者下游發(fā)送不出數據都會造成問題,這實際是一個速率匹配問題。

Netty源碼說明

超過高水位

當ChannelOutboundBuffer的容量超過高水位設定閾值后,isWritable()返回false,設置channel不可寫(setUnwritable),并且觸發(fā)fireChannelWritabilityChanged()。

  1. private void incrementPendingOutboundBytes(long size, boolean invokeLater) { 
  2.     if (size == 0) { 
  3.         return
  4.     } 
  5.  
  6.     long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, size); 
  7.     if (newWriteBufferSize > channel.config().getWriteBufferHighWaterMark()) { 
  8.         setUnwritable(invokeLater); 
  9.     } 
  10. private void setUnwritable(boolean invokeLater) { 
  11.     for (;;) { 
  12.         final int oldValue = unwritable; 
  13.         final int newValue = oldValue | 1
  14.         if (UNWRITABLE_UPDATER.compareAndSet(this, oldValue, newValue)) { 
  15.             if (oldValue == 0 && newValue != 0) { 
  16.                 fireChannelWritabilityChanged(invokeLater); 
  17.             } 
  18.             break
  19.         } 
  20.     } 

低于低水位

當ChannelOutboundBuffer的容量低于低水位設定閾值后,isWritable()返回true,設置channel可寫,并且觸發(fā)fireChannelWritabilityChanged()。

  1. private void decrementPendingOutboundBytes(long size, boolean invokeLater, boolean notifyWritability) { 
  2.     if (size == 0) { 
  3.         return
  4.     } 
  5.  
  6.     long newWriteBufferSize = TOTAL_PENDING_SIZE_UPDATER.addAndGet(this, -size); 
  7.     if (notifyWritability && newWriteBufferSize < channel.config().getWriteBufferLowWaterMark()) { 
  8.         setWritable(invokeLater); 
  9.     } 
  10. private void setWritable(boolean invokeLater) { 
  11.     for (;;) { 
  12.         final int oldValue = unwritable; 
  13.         final int newValue = oldValue & ~1
  14.         if (UNWRITABLE_UPDATER.compareAndSet(this, oldValue, newValue)) { 
  15.             if (oldValue != 0 && newValue == 0) { 
  16.                 fireChannelWritabilityChanged(invokeLater); 
  17.             } 
  18.             break
  19.         } 
  20.     } 

總結

當ChannelOutboundBuffer的容量超過高水位設定閾值后,isWritable()返回false,表明消息產生堆積,需要降低寫入速度。

當ChannelOutboundBuffer的容量低于低水位設定閾值后,isWritable()返回true,表明消息過少,需要提高寫入速度。通過以上三個步驟修改后,部署線上觀察半年未發(fā)生問題出現(xiàn)。

原文鏈接:https://mp.weixin.qq.com/s?__biz=MzI0NzEyODIyOA==&mid=2247487422&idx=1&sn=f9684b46fc38f347036c21b4fb3c3979&utm_source=tuicool&utm_medium=referral

延伸 · 閱讀

精彩推薦
主站蜘蛛池模板: 四虎影视4hu最新地址在线884 | 大象传媒免费网址 | 欧美日韩精 | 日本高清有码视频 | 免费国产好深啊好涨好硬视频 | 热99精品只有里视频最新 | 免费视频左左视频 | 为什么丈夫插我我却喜欢被打着插 | chinesegay黑袜玩奴 | 91中文字幕yellow字幕网 | 激情视频网址 | 成人永久免费福利视频网站 | 波多野结衣178部中文字幕 | 法国贵妇一级伦理hd | 成年男人永久免费看片 | 九九热在线免费观看 | 日本高清全集免费观看 | 秋霞啪啪网 | 天堂资源wwww在线看 | 亚洲无限观看 | 日韩在线免费播放 | 国产精品国产精品国产三级普 | 水蜜桃一二二区视在线 | 91夜夜人人揉人人捏人人添 | 亚洲欧美日韩国产精品影院 | 国产日本欧美亚洲精品视 | 四虎www.| 5x视频在线观看 | 丰满大屁股美女一级毛片 | 2019中文字幕| 久久久这里有精品999 | 91你懂的 | 521色香蕉网在线观看免费 | 99re免费在线视频 | 天堂网在线.www天堂在线视频 | 秋霞一级毛片 | 免费网址视频在线看 | 色哟哟哟在线精品观看视频 | 色狠狠成人综合网 | 日韩福利一区 | 日本大学生xxxxx69泡妞 |