解決Netty那些事兒之Reactor在Netty中的實現(創(chuàng)建篇)-下
接上文解決Netty那些事兒之Reactor在Netty中的實現(創(chuàng)建篇)-上
Netty對JDK NIO 原生Selector的優(yōu)化
首先在NioEventLoop
中有一個Selector優(yōu)化開關DISABLE_KEY_SET_OPTIMIZATION
,通過系統(tǒng)變量-D io.netty.noKeySetOptimization
指定,默認是開啟的,表示需要對JDK NIO原生Selector
進行優(yōu)化。
如果優(yōu)化開關DISABLE_KEY_SET_OPTIMIZATION
是關閉的,那么直接返回JDK NIO原生的Selector
。
下面為Netty對JDK NIO原生的Selector
的優(yōu)化過程:
獲取
JDK NIO原生Selector
的抽象實現類sun.nio.ch.SelectorImpl
。JDK NIO原生Selector
的實現均繼承于該抽象類。用于判斷由SelectorProvider
創(chuàng)建出來的Selector
是否為JDK默認實現
(SelectorProvider
第三種加載方式)。因為SelectorProvider
可以是自定義加載,所以它創(chuàng)建出來的Selector
并不一定是JDK NIO 原生的。
JDK NIO Selector的抽象類sun.nio.ch.SelectorImpl
這里筆者來簡單介紹下JDK NIO中的Selector
中這幾個字段的含義,我們可以和上篇文章講到的epoll在內核中的結構做類比,方便大家后續(xù)的理解:

Set<SelectionKey> selectedKeys
?類似于我們上篇文章講解Epoll
時提到的就緒隊列eventpoll->rdllist
,Selector
這里大家可以理解為Epoll
。Selector
會將自己監(jiān)聽到的IO就緒
的Channel
放到selectedKeys
中。
這里的
SelectionKey
暫且可以理解為Channel
在Selector
中的表示,類比上圖中epitem結構
里的epoll_event
,封裝IO就緒Socket的信息。其實SelectionKey
里包含的信息不止是Channel
還有很多IO相關的信息。后面我們在詳細介紹。
HashSet<SelectionKey> keys:
這里存放的是所有注冊到該Selector
上的Channel
。類比epoll中的紅黑樹結構rb_root
SelectionKey
在Channel
注冊到Selector
中后生成。
Set<SelectionKey> publicSelectedKeys
?相當于是selectedKeys
的視圖,用于向外部線程返回IO就緒
的SelectionKey
。這個集合在外部線程中只能做刪除操作不可增加元素
,并且不是線程安全的
。Set<SelectionKey> publicKeys
相當于keys
的不可變視圖,用于向外部線程返回所有注冊在該Selector
上的SelectionKey
這里需要重點關注
抽象類sun.nio.ch.SelectorImpl
中的selectedKeys
和publicSelectedKeys
這兩個字段,注意它們的類型都是HashSet
,一會優(yōu)化的就是這里?。。。?/strong>
判斷由
SelectorProvider
創(chuàng)建出來的Selector
是否是JDK NIO原生的Selector
實現。因為Netty優(yōu)化針對的是JDK NIO 原生Selector
。判斷標準為sun.nio.ch.SelectorImpl
類是否為SelectorProvider
創(chuàng)建出Selector
的父類。如果不是則直接返回。不在繼續(xù)下面的優(yōu)化過程。
通過前面對SelectorProvider
的介紹我們知道,這里通過provider.openSelector()
創(chuàng)建出來的Selector
實現類為KQueueSelectorImpl類
,它繼承實現了sun.nio.ch.SelectorImpl
,所以它是JDK NIO 原生的Selector
實現
創(chuàng)建
SelectedSelectionKeySet
通過反射替換掉sun.nio.ch.SelectorImpl類
中selectedKeys
和publicSelectedKeys
的默認HashSet
實現。
為什么要用SelectedSelectionKeySet
替換掉原來的HashSet
呢??
因為這里涉及到對HashSet類型
的sun.nio.ch.SelectorImpl#selectedKeys
集合的兩種操作:
插入操作:?通過前邊對
sun.nio.ch.SelectorImpl類
中字段的介紹我們知道,在Selector
監(jiān)聽到IO就緒
的SelectionKey
后,會將IO就緒
的SelectionKey
插入sun.nio.ch.SelectorImpl#selectedKeys
集合中,這時Reactor線程
會從java.nio.channels.Selector#select(long)
阻塞調用中返回(類似上篇文章提到的epoll_wait
)。遍歷操作:
Reactor線程
返回后,會從Selector
中獲取IO就緒
的SelectionKey
集合(也就是sun.nio.ch.SelectorImpl#selectedKeys
),Reactor線程
遍歷selectedKeys
,獲取IO就緒
的SocketChannel
,并處理SocketChannel
上的IO事件
。
我們都知道HashSet
底層數據結構是一個哈希表
,由于Hash沖突
這種情況的存在,所以導致對哈希表
進行插入
和遍歷
操作的性能不如對數組
進行插入
和遍歷
操作的性能好。
還有一個重要原因是,數組可以利用CPU緩存的優(yōu)勢來提高遍歷的效率。后面筆者會有一篇專門的文章來講述利用CPU緩存行如何為我們帶來性能優(yōu)勢。
所以Netty為了優(yōu)化對sun.nio.ch.SelectorImpl#selectedKeys
集合的插入,遍歷
性能,自己用數組
這種數據結構實現了SelectedSelectionKeySet
,用它來替換原來的HashSet
實現。
【文章福利】小編推薦自己的Linux內核技術交流群:【749907784】整理了一些個人覺得比較好的學習書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。。ê曨l教程、電子書、實戰(zhàn)項目及代碼)? ? ?


SelectedSelectionKeySet
初始化
SelectionKey[] keys
數組大小為1024
,當數組容量不夠時,擴容為原來的兩倍大小。通過數組尾部指針
size
,在向數組插入元素的時候可以直接定位到插入位置keys[size++]
。操作一步到位,不用像哈希表
那樣還需要解決Hash沖突
。對數組的遍歷操作也是如絲般順滑,CPU直接可以在緩存行中遍歷讀取數組元素無需訪問內存。比
HashSet
的迭代器java.util.HashMap.KeyIterator
?遍歷方式性能不知高到哪里去了。
看到這里不禁感嘆,從各種小的細節(jié)可以看出Netty對性能的優(yōu)化簡直淋漓盡致,對性能的追求令人發(fā)指。細節(jié)真的是魔鬼。
Netty通過反射的方式用
SelectedSelectionKeySet
替換掉sun.nio.ch.SelectorImpl#selectedKeys
,sun.nio.ch.SelectorImpl#publicSelectedKeys
這兩個集合中原來HashSet
的實現。
反射獲取
sun.nio.ch.SelectorImpl
類中selectedKeys
和publicSelectedKeys
。
Java9
版本以上通過sun.misc.Unsafe
設置字段值的方式
通過反射的方式用SelectedSelectionKeySet
替換掉hashSet
實現的sun.nio.ch.SelectorImpl#selectedKeys,sun.nio.ch.SelectorImpl#publicSelectedKeys
。
將與sun.nio.ch.SelectorImpl
類中selectedKeys
和publicSelectedKeys
關聯好的Netty優(yōu)化實現SelectedSelectionKeySet
,設置到io.netty.channel.nio.NioEventLoop#selectedKeys
字段中保存。
后續(xù)
Reactor線程
就會直接從io.netty.channel.nio.NioEventLoop#selectedKeys
中獲取IO就緒
的SocketChannel
用
SelectorTuple
封裝unwrappedSelector
和wrappedSelector
返回給NioEventLoop
構造函數。到此Reactor
中的Selector
就創(chuàng)建完畢了。
所謂的
unwrappedSelector
是指被Netty優(yōu)化過的JDK NIO原生Selector。所謂的
wrappedSelector
就是用SelectedSelectionKeySetSelector
裝飾類將unwrappedSelector
和與sun.nio.ch.SelectorImpl類
關聯好的Netty優(yōu)化實現SelectedSelectionKeySet
封裝裝飾起來。
wrappedSelector
會將所有對Selector
的操作全部代理給unwrappedSelector
,并在發(fā)起輪詢IO事件
的相關操作中,重置SelectedSelectionKeySet
清空上一次的輪詢結果。
到這里Reactor的核心Selector就創(chuàng)建好了,下面我們來看下用于保存異步任務的隊列是如何創(chuàng)建出來的。
newTaskQueue
我們繼續(xù)回到創(chuàng)建Reactor
的主線上,到目前為止Reactor
的核心Selector
就創(chuàng)建好了,前邊我們提到Reactor
除了需要監(jiān)聽IO就緒事件
以及處理IO就緒事件
外,還需要執(zhí)行一些異步任務,當外部線程向Reactor
提交異步任務后,Reactor
就需要一個隊列來保存這些異步任務,等待Reactor線程
執(zhí)行。
下面我們來看下Reactor
中任務隊列的創(chuàng)建過程:
在
NioEventLoop
的父類SingleThreadEventLoop
中提供了一個靜態(tài)變量DEFAULT_MAX_PENDING_TASKS
用來指定Reactor
任務隊列的大小??梢酝ㄟ^系統(tǒng)變量-D io.netty.eventLoop.maxPendingTasks
進行設置,默認為Integer.MAX_VALUE
,表示任務隊列默認為無界隊列
。根據
DEFAULT_MAX_PENDING_TASKS
變量的設定,來決定創(chuàng)建無界任務隊列還是有界任務隊列。
Reactor
內的異步任務隊列的類型為MpscQueue
,它是由JCTools
提供的一個高性能無鎖隊列,從命名前綴Mpsc
可以看出,它適用于多生產者單消費者
的場景,它支持多個生產者線程安全的訪問隊列,同一時刻只允許一個消費者線程讀取隊列中的元素。
我們知道Netty中的
Reactor
可以線程安全
的處理注冊其上的多個SocketChannel
上的IO數據
,保證Reactor線程安全
的核心原因正是因為這個MpscQueue
,它可以支持多個業(yè)務線程在處理完業(yè)務邏輯后,線程安全的向MpscQueue
添加異步寫任務
,然后由單個Reactor線程
來執(zhí)行這些寫任務
。既然是單線程執(zhí)行,那肯定是線程安全的了。
Reactor對應的NioEventLoop類型繼承結構

NioEventLoop
的繼承結構也是比較復雜,這里我們只關注在Reactor
創(chuàng)建過程中涉及的到兩個父類SingleThreadEventLoop
,SingleThreadEventExecutor
。
剩下的繼承體系,我們在后邊隨著Netty
源碼的深入在慢慢介紹。
前邊我們提到,其實Reactor
我們可以看作是一個單線程的線程池,只有一個線程用來執(zhí)行IO就緒事件的監(jiān)聽
,IO事件的處理
,異步任務的執(zhí)行
。用MpscQueue
來存儲待執(zhí)行的異步任務。
命名前綴為SingleThread
的父類都是對Reactor
這些行為的分層定義。也是本小節(jié)要介紹的對象
SingleThreadEventLoop
Reactor
負責執(zhí)行的異步任務分為三類:
普通任務:
這是Netty最主要執(zhí)行的異步任務,存放在普通任務隊列taskQueue
中。在NioEventLoop
構造函數中創(chuàng)建。定時任務:
?存放在優(yōu)先級隊列中。后續(xù)我們介紹。尾部任務:
?存放于尾部任務隊列tailTasks
中,尾部任務一般不常用,在普通任務執(zhí)行完后 Reactor線程會執(zhí)行尾部任務。**使用場景:**比如對Netty 的運行狀態(tài)做一些統(tǒng)計數據,例如任務循環(huán)的耗時、占用物理內存的大小等等都可以向尾部隊列添加一個收尾任務完成統(tǒng)計數據的實時更新。
SingleThreadEventLoop
負責對尾部任務隊列tailTasks
進行管理。并且提供Channel
向Reactor
注冊的行為。
SingleThreadEventExecutor
SingleThreadEventExecutor
主要負責對普通任務隊列
的管理,以及異步任務的執(zhí)行
,Reactor線程的啟停
。
到現在為止,一個完整的Reactor架構
就被創(chuàng)建出來了。

3. 創(chuàng)建Channel到Reactor的綁定策略
到這一步,Reactor線程組NioEventLoopGroup
里邊的所有Reactor
就已經全部創(chuàng)建完畢。
無論是Netty服務端NioServerSocketChannel
關注的OP_ACCEPT
事件也好,還是Netty客戶端NioSocketChannel
關注的OP_READ
和OP_WRITE
事件也好,都需要先注冊到Reactor
上,Reactor
才能監(jiān)聽Channel
上關注的IO事件
實現IO多路復用
。
NioEventLoopGroup
(Reactor線程組)里邊有眾多的Reactor
,那么以上提到的這些Channel
究竟應該注冊到哪個Reactor
上呢?這就需要一個綁定的策略來平均分配。
還記得我們前邊介紹MultithreadEventExecutorGroup類
的時候提到的構造器參數EventExecutorChooserFactory
嗎?
這時候它就派上用場了,它主要用來創(chuàng)建Channel
到Reactor
的綁定策略。默認為DefaultEventExecutorChooserFactory.INSTANCE
。
下面我們來看下具體的綁定策略:
DefaultEventExecutorChooserFactory
我們看到在newChooser
方法綁定策略有兩個分支,不同之處在于需要判斷Reactor線程組中的Reactor
個數是否為2的次冪
。
Netty中的綁定策略就是采用round-robin
輪詢的方式來挨個選擇Reactor
進行綁定。
采用round-robin
的方式進行負載均衡,我們一般會用round % reactor.length
取余的方式來挨個平均的定位到對應的Reactor
上。
如果Reactor
的個數reactor.length
恰好是2的次冪
,那么就可以用位操作&
運算round & reactor.length -1
來代替%
運算round % reactor.length
,因為位運算的性能更高。具體為什么&
運算能夠代替%
運算,筆者會在后面講述時間輪的時候為大家詳細證明,這里大家只需記住這個公式,我們還是聚焦本文的主線。
了解了優(yōu)化原理,我們在看代碼實現就很容易理解了。
利用%
運算的方式Math.abs(idx.getAndIncrement() % executors.length)
來進行綁定。
利用&
運算的方式idx.getAndIncrement() & executors.length - 1
來進行綁定。
又一次被Netty對性能的極致追求所折服~~~~
4. 向Reactor線程組中所有的Reactor注冊terminated回調函數
當Reactor線程組NioEventLoopGroup
中所有的Reactor
已經創(chuàng)建完畢,Channel
到Reactor
的綁定策略也創(chuàng)建完畢后,我們就來到了創(chuàng)建NioEventGroup
的最后一步。
俗話說的好,有創(chuàng)建就有啟動,有啟動就有關閉,這里會創(chuàng)建Reactor關閉
的回調函數terminationListener
,在Reactor
關閉時回調。
terminationListener
回調的邏輯很簡單:
通過
AtomicInteger terminatedChildren
變量記錄已經關閉的Reactor
個數,用來判斷NioEventLoopGroup
中的Reactor
是否已經全部關閉。如果所有
Reactor
均已關閉,設置NioEventLoopGroup
中的terminationFuture
為success
。表示Reactor線程組
關閉成功。
我們在回到文章開頭Netty服務端代碼模板
現在Netty的主從Reactor線程組
就已經創(chuàng)建完畢,此時Netty服務端的骨架已經搭建完畢,骨架如下:

總結
本文介紹了首先介紹了Netty對各種IO模型
的支持以及如何輕松切換各種IO模型
。
還花了大量的篇幅介紹Netty服務端的核心引擎主從Reactor線程組
的創(chuàng)建過程。在這個過程中,我們還提到了Netty對各種細節(jié)進行的優(yōu)化,展現了Netty對性能極致的追求。
好了,Netty服務端的骨架已經搭好,剩下的事情就該綁定端口地址然后接收連接了.
原文作者:bin的技術小屋
