天天看點

EventLoop和EventLoopGroup

Netty架構的主要線程就是I/O線程,線程模型設計的好壞,決定了系統的吞吐量、并發性和安全性等架構品質屬性。Netty的線程模型被精心地設計,既提升了架構的并發性能,又能在很大程度避免鎖,局部實作了無鎖化設計。

一般首先會想到的是經典的Reactor線程模型,盡管不同的NIO架構對于Reactor模式的實作存在差異,但本質上還是遵循了Reactor的基礎線程模型。

Reactor單線程模型,是指所有的I/O操作都在同一個NIO線程上面完成。

NIO線程的職責如下:

作為NIO服務端,接收用戶端的TCP連接配接;

作為NIO用戶端,向服務端發起TCP連接配接;

讀取通信對端的請求或者應答消息;

向通信對端發送消息請求或者應答消息。

由于Reactor模式使用的是異步非阻塞I/O,所有的I/O操作都不會導緻阻塞,理論上一個線程可以獨立處理所有I/O相關的操作。從架構層面看,一個NIO線程确實可以完成其承擔的職責。例如,通過Acceptor類接收用戶端的TCP連接配接請求消息,當鍊路建立成功之後,通過Dispatch将對應的ByteBuffer派發到指定的Handler上,進行消息解碼。使用者線程消息編碼後通過NIO線程将消息發送給用戶端。

在一些小容量應用場景下,可以使用單線程模型。但是這對于高負載、大并發的應用場景卻不合适,主要原因如下:

一個NIO線程同時處理成百上千的鍊路,性能上無法支撐,即便NIO線程的CPU負荷達到100%,也無法滿足海量消息的編碼、解碼、讀取和發送。

當NIO線程負載過重之後,處理速度将變慢,這會導緻大量用戶端連接配接逾時,逾時之後往往會進行重發,這更加重了NIO線程的負載,最終會導緻大量消息積壓和處理逾時,成為系統的性能瓶頸。

可靠性問題:一旦NIO線程意外跑飛,或者進入死循環,會導緻整個系統通信子產品不可用,不能接收和處理外部消息,造成節點故障。

Rector多線程模型與單線程模型最大的差別就是有一組NIO線程來處理I/O操作。

Reactor多線程模型的特點如下:

有專門一個NIO線程——Acceptor線程用于監聽服務端,接收用戶端的TCP連接配接請求。

網絡I/O操作——讀、寫等由一個NIO線程池負責,線程池可以采用标準的JDK線程池實作,它包含一個任務隊列和N個可用的線程,由這些NIO線程負責消息的讀取、解碼、編碼和發送。

一個NIO線程可以同時處理N條鍊路,但是一個鍊路隻對應一個NIO線程,防止發生并發操作問題。

在絕大多數場景下,Reactor多線程模型可以滿足性能需求。但是,在個别特殊場景中,一個NIO線程負責監聽和處理所有的用戶端連接配接可能會存在性能問題。例如并發百萬用戶端連接配接,或者服務端需要對用戶端握手進行安全認證,但是認證本身非常損耗性能。在這類場景下,單獨一個Acceptor線程可能會存在性能不足的問題,為了解決性能問題,産生了第三種Reactor線程模型——主從Reactor多線程模型。

主從Reactor線程模型的特點是:服務端用于接收用戶端連接配接的不再是一個單獨的NIO線程,而是一個獨立的NIO線程池。Acceptor接收到用戶端TCP連接配接請求并處理完成後(可能包含接入認證等),将新建立的SocketChannel注冊到I/O線程池(sub reactor線程池)的某個I/O線程上,由它負責SocketChannel的讀寫和編解碼工作。Acceptor線程池僅僅用于用戶端的登入、握手和安全認證,一旦鍊路建立成功,就将鍊路注冊到後端subReactor線程池的I/O線程上,由I/O線程負責後續的I/O操作。

利用主從NIO線程模型,可以解決一個服務端監聽線程無法有效處理所有用戶端連接配接的性能不足問題。是以,在Netty的官方demo中,推薦使用該線程模型。

Netty的線程模型并不是一成不變的,它實際取決于使用者的啟動參數配置。通過設定不同的啟動參數,Netty可以同時支援Reactor單線程模型、多線程模型和主從Reactor多線層模型。

下面讓我們通過一張原理圖(圖18-4)來快速了解Netty的線程模型。

EventLoop和EventLoopGroup

可以通過Netty服務端啟動代碼來了解它的線程模型:

服務端啟動的時候,建立了兩個NioEventLoopGroup,它們實際是兩個獨立的Reactor線程池。一個用于接收用戶端的TCP連接配接,另一個用于處理I/O相關的讀寫操作,或者執行系統Task、定時任務Task等。

Netty用于接收用戶端請求的線程池職責如下。

(1)接收用戶端TCP連接配接,初始化Channel參數;

(2)将鍊路狀态變更事件通知給ChannelPipeline。

Netty處理I/O操作的Reactor線程池職責如下。

(1)異步讀取通信對端的資料報,發送讀事件到ChannelPipeline;

(2)異步發送消息到通信對端,調用ChannelPipeline的消息發送接口;

(3)執行系統調用Task;

(4)執行定時任務Task,例如鍊路空閑狀态監測定時任務。

通過調整線程池的線程個數、是否共享線程池等方式,Netty的Reactor線程模型可以在單線程、多線程和主從多線程間切換,這種靈活的配置方式可以最大程度地滿足不同使用者的個性化定制。

為了盡可能地提升性能,Netty在很多地方進行了無鎖化的設計,例如在I/O線程内部進行串行操作,避免多線程競争導緻的性能下降問題。表面上看,串行化設計似乎CPU使用率不高,并發程度不夠。但是,通過調整NIO線程池的線程參數,可以同時啟動多個串行化的線程并行運作,這種局部無鎖化的串行線程設計相比一個隊列—多個工作線程的模型性能更優。

它的設計原理如圖:

EventLoop和EventLoopGroup

Netty的NioEventLoop讀取到消息之後,直接調用ChannelPipeline的fireChannelRead (Object msg)。隻要使用者不主動切換線程,一直都是由NioEventLoop調用使用者的Handler,期間不進行線程切換。這種串行化處理方式避免了多線程操作導緻的鎖的競争,從性能角度看是最優的。

Netty的多線程程式設計最佳實踐如下。

(1)建立兩個NioEventLoopGroup,用于邏輯隔離NIO Acceptor和NIO I/O線程。

(2)盡量不要在ChannelHandler中啟動使用者線程(解碼後用于将POJO消息派發到後端業務線程的除外)。

(3)解碼要放在NIO線程調用的解碼Handler中進行,不要切換到使用者線程中完成消息的解碼。

(4)如果業務邏輯操作非常簡單,沒有複雜的業務邏輯計算,沒有可能會導緻線程被阻塞的磁盤操作、資料庫操作、網路操作等,可以直接在NIO線程上完成業務邏輯編排,不需要切換到使用者線程。

(5)如果業務邏輯處理複雜,不要在NIO線程上完成,建議将解碼後的POJO消息封裝成Task,派發到業務線程池中由業務線程執行,以保證NIO線程盡快被釋放,處理其他的I/O操作。

Netty的NioEventLoop并不是一個純粹的I/O線程,它除了負責I/O的讀寫之外,還兼顧處理以下兩類任務:

系統Task:通過調用NioEventLoop的execute(Runnable task)方法實作,Netty有很多系統Task,建立它們的主要原因是:當I/O線程和使用者線程同時操作網絡資源時,為了防止并發操作導緻的鎖競争,将使用者線程的操作封裝成Task放入消息隊列中,由I/O線程負責執行,這樣就實作了局部無鎖化。

定時任務:通過調用NioEventLoop的schedule(Runnable command, long delay, TimeUnit unit)方法實作。

正是因為NioEventLoop具備多種職責,是以它的實作比較特殊,它并不是個簡單的Runnable。

EventLoop和EventLoopGroup

它實作了EventLoop接口、EventExecutorGroup接口和ScheduledExecutorService接口,正是因為這種設計,導緻NioEventLoop和其父類功能實作非常複雜。

作為NIO架構的Reactor線程,NioEventLoop需要處理網絡I/O讀寫事件,是以它必須聚合一個多路複用器對象。

Selector的初始化非常簡單,直接調用Selector.open()方法就能建立并打開一個新的Selector。Netty對Selector的selectedKeys進行了優化,使用者可以通過io.netty.noKeySetOptimization開關決定是否啟用該優化項。預設不打開selectedKeys優化功能。

下一篇: Unsafe