深刻Netty邏輯架構,從Reactor線程模型開始

本文是Netty系列第6篇

上一篇文章咱們從一個Netty的使用Demo,瞭解了用Netty構建一個Server服務端應用的基本方式。而且從這個Demo出發,簡述了Netty的邏輯架構。web

今天主要是深刻學習下 邏輯架構 中的EventLoop 和 EventLoopGroup,掌握Netty的線程模型,這是Netty最精髓的知識點之一。安全

本文預計閱讀時間約 「15分鐘」,將重點圍繞如下幾個問題展開:微信

  • 什麼是Reactor線程模型?網絡

  • EventLoopGroup、EventLoop 怎麼實現Reactor線程模型?數據結構

  • 深刻Netty的線程模型優化多線程

    • Netty3和Netty4的線程模型變化架構

    • 什麼是Netty4線程模型的無鎖串行化併發

  • 從線程模型看最佳實踐app

先簡單回顧下上一篇的邏輯架構圖,看看EventLoop 和 EventLoopGroup是在什麼位置。socket



1.什麼是Reactor線程模型?

先來回顧下咱們在Netty系列的第2篇介紹的I/O線程模型,包括BIO、NIO、I/O多路複用、信號驅動IO、AIO。IO多路複用在Java中有專門的NIO包封裝了相關的方法。

前面的文章也說過,使用Netty而不是直接使用Java NIO包,就是由於Netty幫咱們封裝了許多對NIO包的使用細節,作了許多優化。

其中很是著名的,就是Netty的「Reactor線程模型」。

前置知識若是還不太清楚,能夠回頭看看前面幾篇文章:


《沒搞清楚網絡I/O模型?那怎麼入門Netty》
《從網絡I/O模型到Netty,先深刻了解下I/O多路複用》
《從I/O多路複用到Netty,還要跨過Java NIO包》

Reactor模式 是一種「事件驅動」模式。

「Reactor線程模型」就是經過 單個線程 使用Java NIO包中的Selector的select()方法,進行監聽。當獲取到事件(如accept、read等)後,就會分配(dispatch)事件進行相應的事件處理(handle)。

若是要給 Reactor線程模型 下一個更明確的定義,應該是:

Reactor線程模式 = Reactor(I/O多路複用)+ 線程池

其中Reactor負責監聽和分配事件,線程池負責處理事件。

而後,根據Reactor的數量和線程池的數量,又能夠將Reactor分爲三種模型

  • 單Reactor單線程模型 (固定大小爲1的線程池)

  • 單Reactor多線程模型

  • 多Reactor多線程模型 (通常是主從2個Reactor)

1.1 單Reactor單線程模型

Reactor內部經過 selector 監聽鏈接事件,收到事件後經過dispatch進行分發。

  • 若是是鏈接創建的事件,經過accept接受鏈接,並建立一個Handler來處理鏈接後續的各類事件。

  • 若是是讀寫事件,直接調用鏈接對應的Handler來處理,Handler完成 read => (decode => compute => encode) => send 的所有流程

這個過程當中,不管是事件監聽、事件分發、仍是事件處理,都始終只有 一個線程 執行全部的事情。

缺點:
在請求過多時,會沒法支撐。由於只有一個線程,沒法發揮多核CPU性能。
並且一旦某個Handler發生阻塞,服務端就徹底沒法處理其餘鏈接事件。

1.2 單Reactor多線程模型

爲了提升性能,咱們能夠把複雜的事件處理handler交給線程池,那就能夠演進爲 「單Reactor多線程模型」 。



這種模型和第一種模型的主要區別是把業務處理從以前的單一線程脫離出來,換成線程池處理。

1)Reactor線程

經過select監聽客戶請求,若是是鏈接創建的事件,經過accept接受鏈接,並建立一個Handler來處理鏈接後續的讀寫事件。這裏的Handler只負責響應事件、read和write事件,會將具體的業務處理交由Worker線程池處理。

只處理鏈接事件、讀寫事件。

2)Worker線程池

處理全部業務事件,包括(decode => compute => encode) 過程。

充分利用多核機器的資源,提升性能。

缺點:
在極個別特殊場景中,一個Reactor線程負責監聽和處理全部的客戶端鏈接可能會存在性能問題。例如併發百萬客戶端鏈接(雙11、春運搶票)

1.3 多Reactor多線程模型

爲了充分利用多核能力,能夠構建兩個 Reactor,這就演進爲 「主從Reactor線程模型」 。

1)主Reactor

主 Reactor 單獨監聽server socket,accept新鏈接,而後將創建的 SocketChannel 註冊給指定的 從Reactor,

2)從Reactor

從Reactor 將鏈接加入到鏈接隊列進行監聽,並建立handler進行事件處理。執行事件的讀寫、分發,把業務處理就扔給worker線程池完成。

3)Worker線程池
處理全部業務事件,充分利用多核機器的資源,提升性能。

輕鬆處理百萬併發。

缺點:
實現比較複雜。

不過有了Netty,一切都變得簡單了。

Netty幫咱們封裝好了一切,能夠快速使用主從Reactor線程模型(Netty4的實現上增長了無鎖串行化設計),具體代碼這裏就不貼了,能夠看看上一篇的Demo。

2. EventLoop、EventLoopGroup 怎麼實現Reactor線程模型?

上面咱們已經瞭解了Reactor線程模型,瞭解了它的核心就是:

Reactor線程模式 = Reactor(I/O多路複用)+ 線程池

它的運行模式包括四個步驟:

  • 鏈接註冊:創建鏈接後,將channel註冊到selector上

  • 事件輪詢:selcetor上輪詢(select()函數)獲取已經註冊的channel的全部I/O事件(多路複用)

  • 事件分發:把準備就緒的I/O事件分配到對應線程進行處理

  • 事件處理:每一個worker線程執行事件任務

那這樣的模型在Netty中具體怎麼實現呢?

這就須要咱們瞭解下EventLoop和EventLoopGroup了。

2.1 EventLoop是什麼

EventLoop 不是Netty獨有的,它自己是一個通用的 事件等待和處理的程序模型。主要用來解決多線程資源消耗高的問題。例如 Node.js 就採用了 EventLoop 的運行機制。

那麼,在Netty中,EventLoop是什麼呢?

  • 一個Reactor模型的事件處理器。

  • 單獨一個線程。

  • 一個EventLoop內部會維護一個selector和一個「taskQueue任務隊列」,分別負責處理 「I/O事件」 和 「任務」。

「taskQueue任務隊列」是多生產者單消費者隊列,在多線程併發添加任務時,能夠保證線程安全。

「I/O事件」即selectionKey中的事件,如accept、connect、read、write等;

「任務」包括 普通任務、定時任務等。

  • 普通任務:經過 NioEventLoop 的 execute() 方法向任務隊列 taskQueue 中添加任務。例如 Netty 在寫數據時會封裝 WriteAndFlushTask 提交給 taskQueue。

  • 定時任務:經過調用 NioEventLoop 的 schedule() 方法向 定時任務隊列 scheduledTaskQueue 添加一個定時任務,用於週期性執行該任務(如心跳消息發送等)。定時任務隊列的任務 到了執行時間後,會合併到 普通任務 隊列中進行真正執行。

一圖勝千言:

EventLoop單線程運行,循環往復執行三個動做:

  • selector事件輪詢

  • I/O事件處理

  • 任務處理

2.2 EventLoopGroup是什麼

EventLoopGroup比較簡單,能夠簡單理解爲一個「EventLoop線程池」。

Tips:

監聽一個端口,只會綁定到 BossEventLoopGroup 中的一個 Eventloop,因此, BossEventLoopGroup 配置多個線程也無用,除非你同時監聽多個端口。

2.3 具體實現

Netty能夠經過簡單配置,支持單Reactor單線程模型 、單Reactor多線程模型 、多Reactor多線程模型。

咱們以 「多Reactor多線程模型」 爲例,來看看Netty是如何經過EventLoop來實現的。

仍是一圖勝千言:

咱們結合Reactor線程模型的四個步驟來梳理一下:

1)鏈接註冊

master EventLoopGroup中有一個EventLoop,綁定某個特定端口進行監聽。

一旦有新的鏈接進來觸發accept類型事件,就會在當前EventLoop的I/O事件處理階段,將這個鏈接分配給slave EventLoopGroup中的某一個EventLoop,進行後續 事件的監聽。

2)事件輪詢

slave EventLoopGroup中的EventLoop,會經過selcetor對綁定到自身的channel進行輪詢,獲取已經註冊的channel的全部I/O事件(多路複用)。

固然,EventLoopGroup中會有 多個EventLoop 運行,各自循環處理。具體EventLoop數量是由 用戶指定的線程數 或者 默認爲核數的2倍。

3)事件分發

當slave EventLoopGroup中的EventLoop獲取到I/O事件後,會在EventLoop的 I/O事件處理(processSelectedKeys) 階段分發給對應ChannelPipeline進行處理。

注意,仍然在當前線程進行串行處理

4)事件處理

在ChannelPipeline中對I/O事件進行處理。

I/O事件處理完後,EventLoop在 任務處理(runAllTasks) 階段,對隊列中的任務進行消費處理。

至此,咱們就能徹底梳理清楚EventLoopGroup/EventLoop 和 Reactor線程模型的關係了。

咦,好像有什麼地方不對勁?

沒錯,細心的朋友可能會發現,slave EventLoopGroup中並非

一個selector + 線程池

而是有多個EventLoop組成的

多selector + 多個單線程

這是爲何呢?

那就得繼續深刻了解下Netty4的線程模型優化了。

3. 深刻Netty的線程模型優化

上文說過,對每一個EventLoop來講,都是單線程運行,並循環往復執行三個動做:

  • selector事件輪詢

  • I/O事件處理

  • 任務處理

在slave EventLoopGroup中,並非 「一個selector + 線程池」模式,而是有多個EventLoop組成的 「多selector + 多個單線程「 模型,這是爲何呢?

這主要是由於咱們分析的是Netty4的線程模型,跟Netty3的傳統Reactor模型相比有了不一樣之處。

3.1 Netty3和Netty4的線程模型變化

在Netty3的線程模型中,分爲 讀事件處理模型 和 寫事件處理模型。

  • read事件的ChannelHandler都是由Netty的 I/O 線程(對應Netty 4 中的 EventLoop)中負責執行。

  • I/O線程調度執行ChannelPipeline中Handler鏈的對應方法,直到業務實現的End Handler。

  • End Handler將消息封裝成Runnable,放入到業務線程池中執行,I/O線程返回,繼續讀/寫等I/O操做。

  • write事件是由調用線程處理,多是 I/O 線程,也多是業務線程。

  • 若是是業務線程,那麼業務線程會執行ChannelPipeline中的Channel Handler。

  • 執行到系統最後一個ChannelHandler,將編碼後的消息Push到發送隊列中,業務線程返回。

  • Netty的I/O線程從發送消息隊列中取出消息,調用SocketChannel的write方法進行消息發送。

由上文能夠看到,在Netty3的線程模型中,是採用「selector + 業務線程池」的模型。

注意,在這種模型下,讀寫模型不一致。尤爲是讀事件、寫事件的「執行線程」是不同的。

可是在Netty4的線程模型中,採用了「多selector + 多個單線程」模型。

讀事件:

  • I/O線程NioEventLoop從SocketChannel中讀取數據,將ByteBuf投遞到ChannelPipeline,觸發ChannelRead事件;

  • I/O線程NioEventLoop調用ChannelHandler鏈,直到將消息投遞到業務線程,而後I/O線程返回,繼續後續的操做。

寫事件:

  • 業務線程調用ChannelHandlerContext.write(Object msg)方法進行消息發送。

  • ChannelHandlerInvoker將發送消息封裝成 任務,放入到EventLoop的Mpsc任務隊列中,業務線程返回。後續由EventLoop在循環中統一調度和執行。

  • I/O線程EventLoop在進行 任務處理 時,從Mpsc任務隊列中獲取任務,調用ChannelPipeline進行處理,處理Outbound事件,直到將消息放入發送隊列,而後喚醒Selector,執行寫操做。

Netty4中,不管讀寫,都是經過I/O線程(也就是EventLoop)來統一處理。

爲何Netty4的線程模型作了這樣的變化?答案就是 無鎖串行化設計

3.2 什麼是Netty4線程模型的無鎖串行化

咱們先看看Netty3的線程模型存在什麼問題:

  • 讀/寫線程模型 不一致,帶來額外的開發心智負擔。

  • 寫操做由業務線程發起時,一般業務會使用 線程池多線程併發執行 某個業務流程,因此某一個時刻會有多個業務線程同時操做ChannelHandler,咱們須要對ChannelHandler進行併發保護,大大下降了開發效率。

  • 頻繁的線程上下文切換,會帶來額外的性能損耗。

而Netty4線程模型的 「無鎖串行化」設計,就很好地解決了這些問題。

一圖勝千言:

從事件輪詢、消息的讀取、編碼以及後續Handler的執行,始終都由I/O線程NioEventLoop內部進行串行操做,這就意味着整個流程不會進行線程上下文的切換,避免多線程競爭致使的性能降低,數據也不會面臨被併發修改的風險。

表面上看,串行化設計彷佛CPU利用率不高,併發程度不夠。可是,經過調整slave EventLoopGroup的線程參數,能夠同時啓動多個NioEventLoop,串行化的線程並行運行,這種局部無鎖化的串行線程設計相比「一個隊列-多個工做線程模型」性能更優。

總結下Netty4無鎖串行化設計的優勢:

  • 一個EventLoop會處理一個channel全生命週期的全部事件。從消息的讀取、編碼以及後續Handler的執行,始終都由I/O線程NioEventLoop負責。

  • 每一個EventLoop會有本身獨立的任務隊列。

  • 整個流程不會進行線程上下文的切換,數據也不會面臨被併發修改的風險。

  • 對於用戶而言,統一的讀寫線程模型,也下降了使用的心智負擔。

4. 從線程模型看最佳實踐

NioEventLoop 無鎖串行化的設計這麼好,它就天衣無縫了嗎?

不是的!

在特定的場景下,Netty3的線程模型可能性能更高。好比編碼和其它寫操做很是耗時,由多個業務線程併發執行,性能確定高於單個EventLoop線程串行執行。

所以,雖然單線程執行避免了線程切換,可是它的缺陷就是不能執行時間過長的 I/O 操做,一旦某個 I/O 事件發生阻塞,那麼後續的全部 I/O 事件都沒法執行,甚至形成事件積壓。

因此,Netty4的線程模型的最佳實踐須要注意如下兩點:

  • 不管讀/寫,不在自定義ChannelHandler中作耗時操做。

  • 不把耗時操做放進 任務隊列。




本文從Reactor線程模型開始提及,到Netty如何用EventLoop實現Reactor線程模型。

而後對Netty4的線程模型優化作了詳細介紹,尤爲是「無鎖串行化設計」。

最後從EventLoop線程模型出發,說明了平常開發中使用Netty4開發的最佳實踐。

但願你們能對EventLoop有全面的認識。

另外,限於篇幅,EventLoop中有兩個很是重要的數據結構沒有展開介紹,大家知道是什麼嗎?

後面會單獨寫兩篇進行分析,敬請期待。

若是有任何疑問或者建議,歡迎 寫留言 或者 微信 和我聯繫哦~


參考書目:
《Netty in Action》



往期熱門筆記合集推薦:


原創:阿丸筆記(微信公衆號:aone_note),歡迎 分享,轉載請保留出處。

掃描下方二維碼能夠關注我哦~

                                                                              以爲不錯,就點個 再看 吧👇


若是有任何疑問或者建議,歡迎 寫留言 或者 微信 和我聯繫哦~

本文分享自微信公衆號 - 阿丸筆記(aone_note)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索