Netty精粹之基於EventLoop機制的高效線程模型

Infoq有篇文章提到經過Netty4+Thrift壓縮二進制編碼技術有人實現了10W TPS(1K的複雜POJO對象)跨節點遠程服務調用,對於RPC應用來講高性能的三個主題永遠是IO模型、數據協議、線程模型,10W TPS的測試結果一方面歸功於Thrift方面壓縮二進制編碼技術的高效(這裏有protobuf和thrift相關測試數據)。另外一方面還要歸功於Netty精心設計的高效線程模型。本文主要對Netty線程模型的設計進行分析,結合對比JavaScript或Node單線程模型的設計與實現,以及對IO密集型和計算密集型應用方面線程模型的設計進行一些思考。java


單線程Reactor模式ajax

Netty線程模型整體上能夠說是Reactor模式的一種變種,咱們先看看什麼是Reactor模式。這裏主要參考維基百科上對Ractor的定義與描述。編程

Reactor模式是一種事件處理模式,單個或多個事件(Event)併發地投遞到事件處理服務(Service Handler),事件處理服務將事件進行分離,同步的將他們分發到對應的事件處理器中去處理。Reactor模式有下面幾種參與者:服務器

  1. 資源:任何提供系統的輸入或者消費系統的輸出的資源,如:Socket句柄。
    網絡

  2. 同步事件分離器:一般使用event loop來進行對資源的阻塞等待,當有資源就緒的時候事件分離器將資源傳遞給事件分發器。
    多線程

  3. 事件分發器:處理請求處理器的註冊或者反註冊,將資源從時間分離器分發到資源對應的請求處理器中同步執行。併發

  4. 請求處理器:應用定義的對相關資源的請求處理。框架

下面用一張圖表示通用Reactor模式的示意圖:異步

Reactor模式示意圖
工具

Reactor模式的優勢與缺點:

Reactor模式使得應用代碼和Reactor實現相分離,這使得用戶能夠將應用代碼設計成最大程度可複用的模塊,因爲對於請求處理器的調用的是同步的,用戶不須要去考慮併發問題,同時也減小了多線程對系統資源的消耗。另外一方面,相比於過程化模式的程序,Reactor模式下的程序相對比較難於Debug,同時單線程的設計在多核時代不可以充分利用多核處理器資源,影響了系統的擴展性。

這是最簡單的單線程Reactor模式,網上也有對於多線程Reactor模式的一些介紹(這裏),本文不作過多介紹,多線程Reactor模式也是在原有的模型基礎上進行的變種。


Netty線程模型

Netty是一款高效的NIO框架和工具,基於JAVA NIO提供的API實現。在JAVA NIO方面Selector給Reactor模式提供了基礎,Netty結合Selector和Reactor模式設計了高效的線程模型,Reactor模式的參與者主要有下面一些組件:

  1. Selector

  2. EventLoopGroup/EventLoop

  3. ChannelPipeline

下面對其功能和其在Netty之Reactor模式中扮演的角色進行介紹。


Selector

Selector是JAVA NIO提供的SelectableChannel多路複用器,它內部維護着三個SelectionKey集合,負責配合select操做將就緒的IO事件分離出來,落地爲SelectionKey,我前面有一篇文章的一部分對Selector進行了相對詳細的介紹(這裏)。在Netty線程模型中,我認爲Selector充當着demultiplexer的角色,而對於SelectionKey咱們能夠將它當作Reactor模式中的資源。


EventLoopGroup/EventLoop

EventLoopGroup是一組EventLoop的抽象,因爲Netty對Reactor模式進行了變種,實際上爲更好的利用多核CPU資源,Netty實例中通常會有多個EventLoop同時工做,每一個EventLoop維護着一個Selector實例,相似單線程Reactor模式地工做着。至於多少線程可有用戶決定,Netty也根據實際上的處理器核數提供了一個默認的數字,咱們也建議使用這個數字:

private static final int DEFAULT_EVENT_LOOP_THREADS;

static {
    DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
            "io.netty.eventLoopThreads", Runtime.getRuntime().availableProcessors() * 2));

    if (logger.isDebugEnabled()) {
        logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
    }
}

EventLoopGroup提供next接口,能夠總一組EventLoop裏面按照必定規則獲取其中一個EventLoop來處理任務,對於EventLoopGroup這裏須要瞭解的是在Netty中,在Netty服務器編程中咱們須要BossEventLoopGroup和WorkerEventLoopGroup兩個EventLoopGroup來進行工做。一般一個服務端口即一個ServerSocketChannel對應一個Selector和一個EventLoop線程,也就是咱們建議BossEventLoopGroup的線程數參數這是爲1。BossEventLoop負責接收客戶端的鏈接並將SocketChannel交給WorkerEventLoopGroup來進行IO處理。下面是他們的工做示意圖:

Netty多線程工做示意圖

如上圖,BossEventLoopGroup一般是一個單線程的EventLoop,EventLoop維護着一個註冊了ServerSocketChannel的Selector實例,BoosEventLoop不斷輪詢Selector將鏈接事件分離出來,一般是OP_ACCEPT事件,而後將accept獲得的SocketChannel交給WorkerEventLoopGroup,WorkerEventLoopGroup會由next選擇其中一個EventLoopGroup來將這個SocketChannel註冊到其維護的Selector並對其後續的IO事件進行處理。在Reactor模式中BossEventLoopGroup主要是對多線程的擴展,而每一個EventLoop的實現涵蓋IO事件的分離,和分發(Dispatcher)。


ChannelPipeline

在Netty中ChannelPipeline維護着一個ChannelHandler的鏈表隊列,每一個SocketChannel都有一個維護着一個ChannelPipeline實例,而每一個ChannelPipeline實例一般維護着一個ChannelHandler鏈表隊列,因爲SocketChannel是和SelectionKey關聯的,也就是Reactor模式中的資源,當EventLoop將SelectionKey分離出來的時候會將SelectionKey關聯的Channel交給Channel關聯的ChannelHandler鏈來處理,那麼ChannelPipeline實際上是擔任着Reactor模式中的請求處理器這個角色。既然提到ChannelPipeline,這裏對其也進行一些簡單的介紹吧。

ChannelPipeline的默認實現是DefaultChannelPipeline,DefaultChannelPipeline自己維護着一個用戶不可見的tail和head的ChannelHandler,他們分別位於鏈表隊列的頭部和尾部。tail在更上從的部分,而head在靠近網絡層的方向。在Netty中關於ChannelHandler有兩個重要的接口,ChannelInBoundHandler和ChannelOutBoundHandler。inbound能夠理解爲網絡數據從外部流向系統內部,而outbound能夠理解爲網絡數據從系統內部流向系統外部。用戶實現的ChannelHandler能夠根據須要實現其中一個或多個接口,將其放入Pipeline中的鏈表隊列中,ChannelPipeline會根據不一樣的IO事件類型來找到相應的Handler來處理,同時鏈表隊列是責任鏈模式的一種變種,自上而下或自下而上全部知足事件關聯的Handler都會對事件進行處理。

責任鏈模式處理示意圖

上面部分主要是對比Reactor模式對Netty的線程模型進行相應的對比介紹,下面主要會結合JavaScript單線程模型多介紹一些Netty對EventLoop的實現及相應的思考。


JavaScript單線程模型

衆所周知,JavaScript是單線程的,也就是任什麼時候刻同時只能有一個線程堆棧在執行,那麼對於下面這段代碼可能有同窗會疑惑這,這個是怎麼執行的:

console.log("A");
setTimeout(function timeout() {
    console.log("B");
}, 10);
console.log("C");
....//biz code
console.log("D");

最初的想法是咱們設置了一個定時任務,10ms以後執行,若是在biz code處的code須要執行20ms以上,那麼timeout怎麼可以順利執行呢,並且單線程是如何作到既執行下面的biz code又執行timeout的呢。事實上若是biz code的部分若是執行時間大於10ms,那麼timeout並不會當即準時執行的。要明白其中的緣由,咱們能夠從一張圖來理解JavaScript的單線程模型:

JavaScript單線程模型工做示意圖

首先簡單理解下eventloop機制,即一個線程在執行完主線程後會不斷輪詢callback隊列,取出就緒任務執行,每一個循環稱爲一個tick。由於JavaScript只有一個線程執行,所以也只有一個線程堆棧,結合上面的code實例接單說明一下對應堆棧的變更:

console.log("A")入棧執行,輸出"A",console.log("A")出棧。setTimeout入棧,WebAPIs後臺不斷檢查timeout對象的超時時間是否已經到達,若是到達則會將對於的callback也即timeout放入callback隊列。接下來console.log("C")會入棧執行,輸出"C",而後出棧。...最後console.log("D")會入棧執行,輸出"D",而後出棧。主區域代碼執行完畢線程會不斷輪詢callback隊列來查詢是否有就緒callback,若是有則取出執行,若是沒有則繼續輪詢。而對於超時或者是咱們使用ajax的callback,後臺會根據IO操做或超時時間是否完畢來決定是否將callback放入callback隊列,這就是EventLoop機制。Node的單線程EventLoop模型相比於JavaScript的單線程EventLoop模型相似,可是更復雜一些,總體模型能夠做爲參考去理解。


Netty EventLoop

理解完JavaScript的EventLoop機制以後咱們再回過頭來看看Netty EventLoop機制的具體實現。對比JavaScript單線程模型圖,我畫了一張Netty的單線程模型圖:

Netty單線程EventLoop示意圖

在Netty的EventLoop線程中,這個線程主要須要處理IO事件和其餘兩種任務,分別爲定時任務和通常任務。Netty提供可一個參數ioRatio用於用戶調整單線程對於IO處理時間和任務處理時間的分配的比率。這樣根據實際應用場景用戶能夠對這個值進行調整,默認值是50,也就是這個線程會將處理IO的時間和處理任務的時間控制爲1:1。

final long ioStartTime = System.nanoTime();

processSelectedKeys();//處理IO事件

final long ioTime = System.nanoTime() - ioStartTime;//處理IO事件的時間
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);//計算用於處理任務的時間

這樣儘管一個EventLoop會關聯多個Channel,這些Channel在單個線程下並不會出現併發問題,同時對於異步任務的處理也同樣,Netty這樣設計即免去了併發問題的煩惱,有減小了多線程上下文切換帶來的性能損耗,同時基於EventLoopGroup實現的有限的線程數可以充分利用CPU處理能力。


關於IO密集型和CPU密集型的思考

Netty基於單線程設計的EventLoop可以同時處理成千上萬的客戶端鏈接的IO事件,缺點是單線程不可以處理時間過長的任務,這樣會阻塞使得IO事件的處理被阻塞,嚴重的時候回形成IO事件堆積,服務不可以高效響應客戶端請求。所謂時間過長的任務一般是佔用CPU資源比較長的任務,也即CPU密集型,對於業務應用也多是業務代碼的耗時。這點和Node是極其類似的,我能夠認爲這是基於單線程的EventLoop模型的通病,咱們不可以將過長的任務交給這個單線程來處理,也就是不適合CPU密集型應用。那麼問題怎麼解決呢,參照Node的解決方案,當咱們遇到須要處理時間很長的任務的時候,咱們能夠將它交給子線程來處理,主線程繼續去EventLoop,當子線程計算完畢再講結果交給主線程。這也是一般基於Netty的應用的解決方案,一般業務代碼執行時間比較長,咱們不可以把業務邏輯交給這個單線程來處理,所以咱們須要額外的線程池來分配線程資源來專門處理耗時較長的業務邏輯,這是比較通用的設計方案。


本文由做者原創,歡迎轉載需註明出處。

相關文章
相關標籤/搜索