Netty高級功能(一):流控和流量整形

這一章節,我們通過例子學習netty的一些高級特性。

1、netty客戶端流控

在有些場景下,由于各種原因,會導致客戶端消息發送積壓,進而導致OOM。

  • 1、當netty服務端并發壓力過大,超過了服務端的處理能力時,channel中的消息服務端不能及時消費,這時channel堵塞,客戶端消息就會堆積在發送隊列中
  • 2、網絡瓶頸,當客戶端發送速度超過網絡鏈路處理能力,會導致客戶端發送隊列積壓
  • 3、當對端讀取速度小于己方發送速度,導致自身TCP發送緩沖區滿,頻繁發生write 0字節時,待發送消息會在netty發送隊列中排隊

這三種情況下,如果客戶端沒有流控保護,這時候就很容易發生內存泄露。

原因:

在我們調用channel的write和writeAndFlush時
io.netty.channel.AbstractChannelHandlerContext#writeAndFlush(java.lang.Object, io.netty.channel.ChannelPromise),如果發送方為業務線程,則將發送操作封裝成WriteTask(繼承Runnable),放到Netty的NioEventLoop中執行,當NioEventLoop無法完成如此多的消息的發送的時候,發送任務隊列積壓,進而導致內存泄漏。

解決方案:

為了防止在高并發場景下,由于服務端處理慢導致的客戶端消息積壓,客戶端需要做并發保護,防止自身發生消息積壓。Netty提供了一個高低水位機制,可以實現客戶端精準的流控。

io.netty.channel.ChannelConfig#setWriteBufferHighWaterMark 高水位
io.netty.channel.ChannelConfig#setWriteBufferLowWaterMark 低水位

當發送隊列待發送的字節數組達到高水位時,對應的channel就變為不可寫狀態,由于高水位并不影響業務線程調用write方法把消息加入到待發送隊列,因此在消息發送時要先對channel的狀態進行判斷(ctx.channel().isWritable)。

這里涉及到的知識點是netty的消息發送機制。

netty的消息發送機制

業務調用write方法后,經過ChannelPipeline職責鏈處理,消息被投遞到發送緩沖區待發送,調用flush之后會執行真正的發送操作,底層通過調用Java NIO的SocketChannel進行非阻塞write操作,將消息發送到網絡上,

image.png

當用戶線程(業務線程)發起write操作時,Netty會進行判斷,如果發現不少NioEventLoop(I/O線程),則將發送消息封裝成WriteTask,放入NioEventLoop的任務隊列,由NioEventLoop線程執行,代碼如下

io.netty.channel.AbstractChannelHandlerContext#write(java.lang.Object, io.netty.channel.ChannelPromise)

    @Override
    public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) {
        if (msg == null) {
            throw new NullPointerException("msg");
        }

        if (isNotValidPromise(promise, true)) {
            ReferenceCountUtil.release(msg);
            // cancelled
            return promise;
        }

        write(msg, true, promise);

        return promise;
    }

 private void write(Object msg, boolean flush, ChannelPromise promise) {
        AbstractChannelHandlerContext next = findContextOutbound();
        final Object m = pipeline.touch(msg, next);
        EventExecutor executor = next.executor();
        if (executor.inEventLoop()) {
            if (flush) {
                next.invokeWriteAndFlush(m, promise);
            } else {
                next.invokeWrite(m, promise);
            }
        } else {
            AbstractWriteTask task;
            if (flush) {
                task = WriteAndFlushTask.newInstance(next, m, promise);
            }  else {
                task = WriteTask.newInstance(next, m, promise);
            }
            safeExecute(executor, task, promise, m);
        }
    }
 private static void safeExecute(EventExecutor executor, Runnable runnable, ChannelPromise promise, Object msg) {
        try {
//這里的executor執行的是netty自己實現的SingleThreadEventExecutor#execute方法,
            executor.execute(runnable);
        } catch (Throwable cause) {
            try {
                promise.setFailure(cause);
            } finally {
                if (msg != null) {
                    ReferenceCountUtil.release(msg);
                }
            }
        }
    }

io.netty.util.concurrent.SingleThreadEventExecutor#execute

@Override
  public void execute(Runnable task) {
      if (task == null) {
          throw new NullPointerException("task");
      }

      boolean inEventLoop = inEventLoop();
      if (inEventLoop) {
          addTask(task);
      } else {
          startThread();
          addTask(task);
          if (isShutdown() && removeTask(task)) {
              reject();
          }
      }

      if (!addTaskWakesUp && wakesUpForTask(task)) {
          wakeup(inEventLoop);
      }
  }

Netty的NioEventLoop線程內部維護了一個Queue<Runnable> taskQuue,除了處理網絡IO讀寫操作,同時還負責執行網絡讀寫相關的Task,NioEventLoop遍歷taskQueue,執行消息發送任務,代碼調用入路徑如下,具體的就不貼了,太長了
io.netty.channel.nio.NioEventLoop#run
-----> io.netty.util.concurrent.SingleThreadEventExecutor#runAllTasks(long)
----->io.netty.util.concurrent.AbstractEventExecutor#safeExecute
這里safeExecute執行的task,就是前面write寫入時包裝的AbstractWriteTask,AbstractWriteTask的run中
io.netty.channel.AbstractChannelHandlerContext.AbstractWriteTask#run

經過一些系統處理操作,最終會調用io.netty.channel.ChannelOutboundBuffer#addMessage方法,將發送消息加入發送隊列(鏈表)。

我們上面寫的流程從NioSocketChannel到ChnnelOutbountBuffer,實際上在這個過程中,為了對發送速度和消息積壓數進行控制,Netty還提供了高低水位機制,當消息隊列中積壓的待發送消息總字節數到達高水位時,修改Channel的狀態為不可寫,并發送通知事件;當消息發送完成后,對低水位進行判斷,如果當前積壓的待發送字節數低于低水位時,則修改channel狀態為可寫,并發送通知事件,具體代碼見下
io.netty.channel.ChannelOutboundBuffer#incrementPendingOutboundBytes(long);
io.netty.channel.ChannelOutboundBuffer#decrementPendingOutboundBytes(long);

image.png

總結:在實際項目中,根據業務QPS規劃,客戶端處理性能、網絡帶寬、鏈路數、消息平均碼流大小等綜合因數,設置Netty高水位(setWriteBufferHighWaterMark)值,可以防止在發送隊列處于高水位時繼續發送消息,導致積壓更嚴重,甚至發生內存泄漏。在系統中合理利用Netty的高低水位機制做消息發送的流控,既可以保護自身,同時又能減輕服務端的壓力,可以提升系統的可靠性。

那么代碼中,怎么使用呢?

image.png

同時在業務發送消息時,添加socketChannel.isWritable()是否可以發送判斷

    public static boolean sendMessage(String clientId,Object message){
        if(StringUtils.isEmpty(clientId)){
            log.error(" clientId 為空,找不到客戶端!");
            return false;
        }
        SocketChannel socketChannel = FactoryMap.getChannelByDevNo(clientId);
        if(socketChannel !=null ){
            if(socketChannel.isWritable()){
                socketChannel.writeAndFlush(message);
                //更新數據庫中消息狀態
                return true;
            }else {
                log.error("channel不可寫");
                return false;
            }
        }else {
            log.error(" 客戶端未連接服務器!發送消息失??!{}",clientId);
        }
        return false;
    }

2、netty服務端 流量整形

前面講的流控(高低水位控制),主要是根據發送消息隊列積壓的大小來控制客戶端channel的寫狀態,然后用戶手動根據channel.isWritable()來控制消息是否發送,用戶可以手動控制消息不能及時發送后的處理方案(比如,過期、超時)。通常用在客戶端比較多。

流量整形呢,是一種主動調整流量輸出速度的措施,一個典型的應用是基于下游網絡節點的TPS指標控制本地流量的輸出。大多數商用系統都由多個網元或者部件組成,例如參與短信互動,會涉及手機,基站,短信中心,短信網關,SP/CP等網元,不同網元或者部件的處理性能不同,為了防止突發的業務洪峰的 導致下游網元被沖垮,有時候需要消停提供流量整形功能。

image.png

Netty流量整形的主要作用:
1、防止由于上下游網元性能不均衡導致下游網元被沖垮,業務流程中斷;
2、防止由于通信模塊接收消息過快,后端業務線程處理不及時,導致出現“撐死”問題。
例如,之前有博客的讀者咨詢過我一個問題,他們設備向服務端不間斷的上報數據,有1G左右,而服務端處理不過來這么多數據,這種情況下,其實就可以使用流量整形來控制接收消息速度。

原理和使用

原理:攔截channelRead和write方法,計算當前需要發送的消息大小,對讀取和發送閾值進行判斷,如果達到了閾值,則暫停讀取和發送消息,待下一個周期繼續處理,以實現在某個周期內對消息讀寫速度進行控制。

使用:將流量整形ChannelHandler添加到業務解碼器之前,

image.png
注意事項:
  • 全局流量整形實例只需要創建一次
    GlobalChannelTrafficShapingHandler 和 GlobalTrafficShapingHandler 是全局共享的,因此實例只需要創建一次,添加到不同的ChannelPipeline即可,不要創建多個實例,否則流量整形將失效。

  • 流量整形參數調整不要過于頻繁

  • 消息發送保護機制
    通過流量整形可以控制發送速度,但是它的控制原理是將待發送的消息封裝成Task放入消息隊列,等待執行時間到達后繼續發送,所以如果業務發送線程不判斷channle的可以狀態,就可能會導致OOM問題。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372