Netty源碼學(xué)習(xí)(3)--Reactor模式

何為Reactor線程模型?



Reactor模式是事件驅(qū)動的,有一個或多個并發(fā)輸入源,有一個Service Handler,有多個Request Handlers;這個Service Handler會同步的將輸入的請求(Event)多路復(fù)用的分發(fā)給相應(yīng)的Request Handler。


Reactor單線程模型就是指所有的IO操作都在同一個NIO線程上面完成的,也就是IO處理線程是單線程的。NIO線程的職責是:?

(1)作為NIO服務(wù)端,接收客戶端的TCP連接;

(2)作為NIO客戶端,向服務(wù)端發(fā)起TCP連接;

(3)讀取通信對端的請求或則應(yīng)答消息;

(4)向通信對端發(fā)送消息請求或則應(yīng)答消息。

從結(jié)構(gòu)上,這有點類似生產(chǎn)者消費者模式,即有一個或多個生產(chǎn)者將事件放入一個Queue中,而一個或多個消費者主動的從這個Queue中Poll事件來處理;而Reactor模式則并沒有Queue來做緩沖,每當一個Event輸入到Service Handler之后,該Service Handler會立刻的根據(jù)不同的Event類型將其分發(fā)給對應(yīng)的Request Handler來處理

這個做的好處有很多,首先我們可以將處理event的Request handler實現(xiàn)一個單獨的線程,即



這樣Service Handler 和request Handler實現(xiàn)了異步,加快了service Handler處理event的速度,那么每一個request同樣也可以以多線程的形式來處理自己的event,即Thread1 擴展成Thread pool 1,

Netty的Reactor線程模型

1 Reactor單線程模型?

Reactor機制中保證每次讀寫能非阻塞讀寫:Acceptor類接收客戶端的TCP請求消息,當鏈路建立成功之后,通過Dispatch將對應(yīng)的ByteBuffer轉(zhuǎn)發(fā)到指定的handler上,進行消息的處理。


一個線程(單線程)來處理CONNECT事件(Acceptor),一個線程池(多線程)來處理read,一個線程池(多線程)來處理write,那么從Reactor Thread到handler都是異步的,從而IO操作也多線程化。由于Reactor Thread依然為單線程,從性能上考慮依然有所限制。

對于一些小容量的應(yīng)用場景下,可以使用單線程模型,但是對于高負載、大并發(fā)的應(yīng)用場景卻不適合,主要原因如下:?

(1)一個NIO線程處理成千上萬的鏈路,性能無法支撐,即使CPU的負荷達到100%;

(2)當NIO線程負載過重,處理性能就會變慢,導(dǎo)致大量客戶端連接超時然后重發(fā)請求,導(dǎo)致更多堆積未處理的請求,成為性能瓶頸。

(3)可靠性低,只有一個NIO線程,萬一線程假死或則進入死循環(huán),就完全不可用了,這是不能接受的。

2 Reactor多線程模型

Reactor多線程模型與單線程模型最大的區(qū)別在于,IO處理線程不再是一個線程,而是一組NIO處理線程。原理如下圖所:

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

(1)有一個專門的NIO線程—-Acceptor線程用于監(jiān)聽服務(wù)端,接收客戶端的TCP連接請求。

(2)網(wǎng)絡(luò)IO操作—-讀寫等操作由一個專門的線程池負責,線程池可以使用JDK標準的線程池實現(xiàn),包含一個任務(wù)隊列和N個可用的線程,這些NIO線程就負責讀取、解碼、編碼、發(fā)送。

(3)一個NIO線程可以同時處理N個鏈路,但是一個鏈路只對應(yīng)一個NIO線程。

通過Reactor Thread Pool來提高event的分發(fā)能力。

Reactor多線程模型可以滿足絕大多數(shù)的場景,除了一些個別的特殊場景:比如一個NIO線程負責處理客戶所有的連接請求,但是如果連接請求中包含認證的需求(安全認證),在百萬級別的場景下,就存在性能問題了,因為認證本身就要消耗CPU,為了解決這種情景下的性能問題,產(chǎn)生了第三種線程模型:Reactor主從線程模型。

3 Reactor主從模型


主從Reactor線程模型的特點是:服務(wù)端用于接收客戶端連接的不再是一個單獨的NIO線程,而是一個獨立的NIO的線程池。Acceptor接收到客戶端TCP連接請求并處理完成后(可能包含接入認證),再將新創(chuàng)建的SocketChannel注冊到IO線程池(sub reactor)的某個IO處理線程上并處理編解碼和讀寫工作。Acceptor線程池僅負責客戶端的連接與認證,一旦鏈路連接成功,就將鏈路注冊到后端的sub Reactor的IO線程池中。利用主從Reactor模型可以解決服務(wù)端監(jiān)聽線程無法有效處理所有客戶連接的性能不足問題,這也是netty推薦使用的線程模型。

netty的線程模型

netty的線程模型是可以通過設(shè)置啟動類的參數(shù)來配置的,設(shè)置不同的啟動參數(shù),netty支持Reactor單線程模型、多線程模型和主從Reactor多線程模型。?

4. netty的線程模型

netty的線程模型是可以通過設(shè)置啟動類的參數(shù)來配置的,設(shè)置不同的啟動參數(shù),netty支持Reactor單線程模型、多線程模型和主從Reactor多線程模型。

服務(wù)端啟動時創(chuàng)建了兩個NioEventLoopGroup,一個是boss,一個是worker。實際上他們是兩個獨立的Reactor線程池,一個用于接收客戶端的TCP連接,另一個用于處理Io相關(guān)的讀寫操作,或則執(zhí)行系統(tǒng)的Task,定時Task。

Boss線程池職責如下:?

(1)接收客戶端的連接,初始化Channel參數(shù)?

(2)將鏈路狀態(tài)變更時間通知給ChannelPipeline

worker線程池作用是:?

(1)異步讀取通信對端的數(shù)據(jù)報,發(fā)送讀事件到ChannelPipeline?

(2)異步發(fā)送消息到通信對端,調(diào)用ChannelPipeline的消息發(fā)送接口?

(3)執(zhí)行系統(tǒng)調(diào)用Task;?

(4)執(zhí)行定時任務(wù)Task;

通過配置boss和worker線程池的線程個數(shù)以及是否共享線程池等方式,netty的線程模型可以在單線程、多線程、主從線程之間切換。

為了提升性能,netty在很多地方都進行了無鎖設(shè)計。比如在IO線程內(nèi)部進行串行操作,避免多線程競爭造成的性能問題。表面上似乎串行化設(shè)計似乎CPU利用率不高,但是通過調(diào)整NIO線程池的線程參數(shù),可以同時啟動多個串行化的線程并行運行,這種局部無鎖串行線程設(shè)計性能更優(yōu)。?

NioEventLoop是Netty的Reactor線程,它在Netty Reactor線程模型中的職責如下:

1. 作為服務(wù)端Acceptor線程,負責處理客戶端的請求接入2. 作為客戶端Connecor線程,負責注冊監(jiān)聽連接操作位,用于判斷異步連接結(jié)果3. 作為IO線程,監(jiān)聽網(wǎng)絡(luò)讀操作位,負責從SocketChannel中讀取報文4. 作為IO線程,負責向SocketChannel寫入報文發(fā)送給對方,如果發(fā)生寫半包,會自動注冊監(jiān)聽寫事件,用于后續(xù)繼續(xù)發(fā)送半包數(shù)據(jù),直到數(shù)據(jù)全部發(fā)送完成

如下圖,是一個NioEventLoop的處理鏈:



處理鏈中的處理方法是串行化執(zhí)行的

一個客戶端連接只注冊到一個NioEventLoop上,避免了多個IO線程并發(fā)操作

3.2.1 Task

Netty Reactor線程模型中有兩種Task:系統(tǒng)Task和定時Task

系統(tǒng)Task:創(chuàng)建它們的主要原因是,當IO線程和用戶線程都在操作同一個資源時,為了防止并發(fā)操作時鎖的競爭問題,將用戶線程封裝為一個Task,在IO線程負責執(zhí)行,實現(xiàn)局部無鎖化

定時Task:主要用于監(jiān)控和檢查等定時動作

基于以上原因,NioEventLoop不是一個純粹的IO線程,它還會負責用戶線程的調(diào)度

IO線程的分配細節(jié)

線程池對IO線程進行資源管理,是通過EventLoopGroup實現(xiàn)的。線程池平均分配channel到所有的線程(循環(huán)方式實現(xiàn),不是100%準確),

一個線程在同一時間只會處理一個通道的IO操作,這種方式可以確保我們不需要關(guān)心同步問題。

Selector

NioEventLoop是Reactor的核心線程,那么它就就必須實現(xiàn)多路復(fù)用。

ioEevntLoopGroup

EventExecutorGroup:提供管理EevntLoop的能力,他通過next()來為任務(wù)分配執(zhí)行線程,同時也提供了shutdownGracefully這一優(yōu)雅下線的接口

EventLoopGroup繼承了EventExecutorGroup接口,并新添了3個方法

EventLoop next()

ChannelFuture register(Channel channel)

ChannelFuture register(Channel channel, ChannelPromise promise)

EventLoopGroup的實現(xiàn)中使用next().register(channel)來完成channel的注冊,即將channel注冊時就綁定了一個EventLoop,然后EvetLoop將channel注冊到EventLoop的Selector上。

NioEventLoopGroup還有幾點需要注意:

NioEventLoopGroup下默認的NioEventLoop個數(shù)為cpu核數(shù) * 2,因為有很多的io處理

NioEventLoop和java的single線程池在5里差異變大了,它本身不負責線程的創(chuàng)建銷毀,而是由外部傳入的線程池管理

channel和EventLoop是綁定的,即一旦連接被分配到EventLoop,其相關(guān)的I/O、編解碼、超時處理都在同一個EventLoop中,這樣可以確保這些操作都是線程安全的

?服務(wù)端線程模型

結(jié)合Netty的源碼,對服務(wù)端創(chuàng)建線程工作流程進行介紹:

第一步,從用戶線程發(fā)起創(chuàng)建服務(wù)端操作,代碼如下:

通常情況下,服務(wù)端的創(chuàng)建是在用戶進程啟動的時候進行,因此一般由Main函數(shù)或者啟動類負責創(chuàng)建,服務(wù)端的創(chuàng)建由業(yè)務(wù)線程負責完成。在創(chuàng)建服務(wù)端的時候?qū)嵗?個EventLoopGroup,1個EventLoopGroup實際就是一個EventLoop線程組,負責管理EventLoop的申請和釋放。

EventLoopGroup管理的線程數(shù)可以通過構(gòu)造函數(shù)設(shè)置,如果沒有設(shè)置,默認取-Dio.netty.eventLoopThreads,如果該系統(tǒng)參數(shù)也沒有指定,則為可用的CPU內(nèi)核數(shù) × 2。

bossGroup線程組實際就是Acceptor線程池,負責處理客戶端的TCP連接請求,如果系統(tǒng)只有一個服務(wù)端端口需要監(jiān)聽,則建議bossGroup線程組線程數(shù)設(shè)置為1。

workerGroup是真正負責I/O讀寫操作的線程組,通過ServerBootstrap的group方法進行設(shè)置,用于后續(xù)的Channel綁定。

逐步debug,發(fā)現(xiàn),HeadContext及TailContext的父類AbstractChannelHandlerContext構(gòu)造函數(shù),在初始化時,使用group內(nèi)容為pipeline及channel。

如圖中所示,如果group不為空,group.next()返回的就是bossGroup,它的next方法用于從線程組中獲取可用線程.

reactor 線程的啟動

1)bossGroup初始化時,啟動線程,過程如下

EventLoopGroup bossGroup =new NioEventLoopGroup();

debug進入NioEventLoopGroup構(gòu)造函數(shù),繼續(xù)debug到下一個構(gòu)造函數(shù):

此時,線程nThreads數(shù)量為0;,繼續(xù)debug進入下一層構(gòu)造函數(shù),:

此時,構(gòu)造函數(shù)中的參數(shù)值如上圖,繼續(xù)debug進入下一層構(gòu)造函數(shù):

繼續(xù)debug,此時已知,class NioEventLoopGroupextends MultithreadEventLoopGroup,進入下一層后,跳轉(zhuǎn)到

MultithreadEventLoopGroup類中的構(gòu)造函數(shù)中,如下圖。

如果沒有指定創(chuàng)建的線程數(shù)量,則默認創(chuàng)建的線程個數(shù)為DEFAULT_EVENT_LOOP_THREADS,該數(shù)值為:處理器數(shù)量x2

已知:class MultithreadEventLoopGroupextends MultithreadEventExecutorGroupimplements EventLoopGroup

此時,跳轉(zhuǎn)到其父類MultithreadEventExecutorGroupimplements構(gòu)造函數(shù):

?變量children就是用來存放創(chuàng)建的線程的數(shù)組,里面每一個元素都通過children[i] = newChild(threadFactory, args)創(chuàng)建。而newChild方法則由子類NioEventLoopGroup實現(xiàn),debug后跳入NioEventLoopGroup中:

??變量children每個元素的真實類型為NioEventLoop,

debug后跳轉(zhuǎn)到NioEventLoop類中的構(gòu)造函數(shù):

而NioEventLoop的類關(guān)系圖如下

此時首先分析第一句:super(parent, threadFactory, false);

跳轉(zhuǎn)到SingleThreadEventExecutor中:

在構(gòu)造函數(shù)中,啟動線程

線程啟動后,創(chuàng)建任務(wù)隊列taskQueue:

boss線程就在此處創(chuàng)建:thread = threadFactory.newThread(new?Runnable()

同時也創(chuàng)建了線程的任務(wù)隊列,是一個LinkedBlockingQueue結(jié)構(gòu)。

SingleThreadEventExecutor.this.run()由子類NioEventLoop實現(xiàn),后面的文章再進行分析

總結(jié):

EventLoopGroup bossGroup = new NioEventLoopGroup()發(fā)生了以下事情:

? ? ??1、?為NioEventLoopGroup創(chuàng)建數(shù)量為:處理器個數(shù) x 2的,類型為NioEventLoop的實例。每個NioEventLoop實例 都持有一個線程,以及一個類型為LinkedBlockingQueue的任務(wù)隊列

? ? ??2、線程的執(zhí)行邏輯由NioEventLoop實現(xiàn)

? ? ? 3、每個NioEventLoop實例都持有一個selector,并對selector進行優(yōu)化。

另分析一下:分析一下selector = openSelector()

這里對sun.nio.ch.SelectorImpl中的selectedKeys和publicSelectedKeys做了優(yōu)化,NioEventLoop中的變量selectedKeys的類型是SelectedSelectionKeySet,內(nèi)部用兩個數(shù)組存儲,初始分配數(shù)組大小置為1024避免頻繁擴容,當大小超過1024時,對數(shù)組進行雙倍擴容。如源碼所示:


如上圖添加數(shù)據(jù),初始分配數(shù)組大小置為1024避免頻繁擴容,當大小超過1024時,對數(shù)組進行雙倍擴容doubleCapacityA()、doubleCapacityB()。

reactor 線程的執(zhí)行

參考內(nèi)容:

http://www.lxweimin.com/p/9acf36f7e025

http://www.lxweimin.com/p/0d0eece6d467


NioEventLoop中維護了一個線程,線程啟動時會調(diào)用NioEventLoop的run方法,執(zhí)行I/O任務(wù)和非I/O任務(wù):

I/O任務(wù)

即selectionKey中ready的事件,如accept、connect、read、write等,由processSelectedKeys方法觸發(fā)。

非IO任務(wù)

添加到taskQueue中的任務(wù),如register0、bind0等任務(wù),由runAllTasks方法觸發(fā)。

兩種任務(wù)的執(zhí)行時間比由變量ioRatio控制,默認為50,則表示允許非IO任務(wù)執(zhí)行的時間與IO任務(wù)的執(zhí)行時間相等。

初始化時 ioRatio為50:


剖析一下?NioEventLoop?的run方法:

hasTasks()方法判斷當前taskQueue是否有元素。

1、 如果taskQueue中有元素,執(zhí)行?selectNow()?方法,最終執(zhí)行selector.selectNow(),該方法會立即返回。

wakenUp?表示是否應(yīng)該喚醒正在阻塞的select操作

2、 如果taskQueue沒有元素,執(zhí)行?select(oldWakenUp)?方法,代碼如下:

JDK NIO的BUG,例如臭名昭著的epoll bug,它會導(dǎo)致Selector空輪詢,最終導(dǎo)致CPU 100%。若Selector的輪詢結(jié)果為空,也沒有wakeup或新消息處理,則發(fā)生空輪詢,CPU使用率100%,

bug:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6595055


圖A

NioEventLoop中reactor線程的select操作也是一個for循環(huán),在for循環(huán)第一步中,如果發(fā)現(xiàn)當前的定時任務(wù)隊列中有任務(wù)的截止事件快到了(<=0.5ms),就跳出循環(huán)。此外,跳出之前如果發(fā)現(xiàn)目前為止還沒有進行過select操作(if (selectCnt == 0)),那么就調(diào)用一次selectNow(),該方法會立即返回,不會阻塞

selectCnt == 0(selectCnt 用來記錄selector.select方法的執(zhí)行次數(shù)和標識是否執(zhí)行過selector.selectNow())

1、delayNanos(currentTimeNanos):

計算延遲任務(wù)隊列中第一個任務(wù)的到期執(zhí)行時間(即最晚還能延遲多長時間執(zhí)行),默認返回1s。從而生成截止時間點:selectDeadLineNanos;

每個SingleThreadEventExecutor都持有一個延遲執(zhí)行任務(wù)的優(yōu)先隊列PriorityQueue,名為:delayedTaskQueue,啟動線程時,往隊列中加入一個任務(wù)。


延遲執(zhí)行任務(wù)的優(yōu)先隊列PriorityQueue??
啟動線程時,往隊列中加入一個任務(wù)??

netty里面定時任務(wù)隊列是按照延遲時間從小到大進行排序,?delayNanos(currentTimeNanos)方法即取出第一個定時任務(wù)的延遲時間

2、如果延遲任務(wù)隊列中第一個任務(wù)的最晚還能延遲執(zhí)行的時間小于500000納秒,且selectCnt == 0(selectCnt 用來記錄selector.select方法的執(zhí)行次數(shù)和標識是否執(zhí)行過selector.selectNow()),則執(zhí)行selector.selectNow()方法并立即返回。

3、否則執(zhí)行selector.select(timeoutMillis),阻塞式select操作

執(zhí)行到這一步,說明netty任務(wù)隊列里面隊列為空,并且所有定時任務(wù)延遲時間還未到(大于0.5ms),于是,在這里進行一次阻塞select操作,截止到第一個定時任務(wù)的截止時間

阻塞select操作結(jié)束之后,netty又做了一系列的狀態(tài)判斷來決定是否中斷本次輪詢,中斷本次輪詢的條件有

輪詢到IO事件 (selectedKeys != 0)

oldWakenUp 參數(shù)為true

任務(wù)隊列里面有任務(wù)(hasTasks)

第一個定時任務(wù)即將要被執(zhí)行 (hasScheduledTasks())

用戶主動喚醒(wakenUp.get())


如果第一個定時任務(wù)的延遲非常長,比如一個小時,那么有沒有可能線程一直阻塞在select操作,當然有可能!But,只要在這段時間內(nèi),有新任務(wù)加入,該阻塞就會被釋放

外部線程調(diào)用execute方法添加任務(wù):

調(diào)用wakeup方法喚醒selector阻塞,可以看到,在外部線程添加任務(wù)的時候,會調(diào)用wakeup方法來喚醒?



nio bug ,問題是圍繞一個最初注冊Selector的通道,因為I/O在服務(wù)器端關(guān)閉(由于早期客戶端退出)。但是服務(wù)器端只有當它執(zhí)行I/O(讀/寫),從而進入IO異常時才能知道這種通道。這種情況下,服務(wù)器端(選擇器)不知道通道已經(jīng)關(guān)閉(對等復(fù)位),從而出現(xiàn)錯誤操作,繼續(xù)對?key(selector和channel的配對)進行空輪訓(xùn),但是其相關(guān)的通道已關(guān)閉或無效。選擇器會一直空輪訓(xùn),從而導(dǎo)致cpu使用率100%。

此處解決方式:

包括上述描述delayNanos(currentTimeNanos)、如果延遲任務(wù)隊列中第一個任務(wù)的最晚還能延遲執(zhí)行的時間小于500000納秒,且selectCnt == 0(selectCnt 用來記錄selector.select方法的執(zhí)行次數(shù)和標識是否執(zhí)行過selector.selectNow()),則執(zhí)行selector.selectNow()方法并立即返回。等,

run方法繼續(xù)執(zhí)行會有以下操作:

netty 會在每次進行?selector.select(timeoutMillis)?之前記錄一下開始時間currentTimeNanos,在select之后記錄一下結(jié)束時間,判斷select操作是否至少持續(xù)了timeoutMillis秒。

如果持續(xù)的時間大于等于timeoutMillis,說明就是一次有效的輪詢,重置selectCnt標志,否則,表明該阻塞方法并沒有阻塞這么長時間,可能觸發(fā)了jdk的空輪詢bug,當空輪詢的次數(shù)超過一個閥值的時候,默認是512,就開始重建selector。對selector進行rebuild后,需要重新執(zhí)行方法selectNow,檢查是否有已ready的selectionKey。


rebuildSelector()?

下面我們簡單描述一下netty 通過rebuildSelector來fix空輪詢bug的過程,rebuildSelector的操作其實很簡單:new一個新的selector,將之前注冊到老的selector上的的channel重新轉(zhuǎn)移到新的selector上。

方法由下面三個圖組成

通過openSelector()方法創(chuàng)建一個新的selector,然后執(zhí)行一個死循環(huán),只要執(zhí)行過程中出現(xiàn)過一次并發(fā)修改selectionKeys異常,就重新開始轉(zhuǎn)移

轉(zhuǎn)移步驟為

1. 拿到有效的key

2. 取消該key在舊的selector上的事件注冊

3. 將該key對應(yīng)的channel注冊到新的selector上

4. 重新綁定channel和新的key的關(guān)系: selector = newSelector;

5.?將原有的selector廢棄:oldSelector.close();



Selector BUG出現(xiàn)的原因

若Selector的輪詢結(jié)果為空,也沒有wakeup或新消息處理,則發(fā)生空輪詢,CPU使用率100%,

Netty的解決辦法

1. 對Selector的select操作周期進行統(tǒng)計,每完成一次空的select操作進行一次計數(shù),

2. 若在某個周期內(nèi)連續(xù)發(fā)生N次空輪詢,則觸發(fā)了epoll死循環(huán)bug。

3. 重建Selector,判斷是否是其他線程發(fā)起的重建請求,若不是則將原SocketChannel從舊的Selector上去除注冊,重新注冊到新的Selector上,并將原來的Selector關(guān)閉。


processSelectedKeys?

對selector進行rebuild后,需要重新執(zhí)行方法selectNow,檢查是否有已ready的selectionKey。

方法selectNow()或select(oldWakenUp)返回后,執(zhí)行方法processSelectedKeys和runAllTasks。

processSelectedKeys?用來處理有事件發(fā)生的selectkey,

圖中1處理優(yōu)化過的selectedKeys,2是正常的處理

selectedKeys?被引用過的地方

selectedKeys是一個?SelectedSelectionKeySet?類對象,在NioEventLoop?的?openSelector?方法中創(chuàng)建,之后就通過反射將selectedKeys與?sun.nio.ch.SelectorImpl?中的兩個field綁定;sun.nio.ch.SelectorImpl?中我們可以看到,這兩個field其實是兩個HashSet。

SelectedSelectionKeySet,內(nèi)部用兩個數(shù)組存儲,初始分配數(shù)組大小置為1024避免頻繁擴容,當大小超過1024時,對數(shù)組進行雙倍擴容。源碼分析前文有描述。



processSelectedKeysOptimized

方法源碼如下:兩圖組成:

該過程分為以下三個步驟:

1.取出IO事件以及對應(yīng)的netty channel類

? ? ? ? 拿到當前SelectionKey之后,將selectedKeys[i]置為null,這里簡單解釋一下這么做的理由:想象一下這種場景,假設(shè)一個NioEventLoop平均每次輪詢出N個IO事件,高峰期輪詢出3N個事件,那么selectedKeys的物理長度要大于等于3N,如果每次處理這些key,不置selectedKeys[i]為空,那么高峰期一過,這些保存在數(shù)組尾部的selectedKeys[i]對應(yīng)的SelectionKey將一直無法被回收,SelectionKey對應(yīng)的對象可能不大,但是要知道,它可是有attachment的,這里的attachment具體是什么下面會講到,但是有一點我們必須清楚,attachment可能很大,這樣一來,這些元素是GC root可達的,很容易造成gc不掉,內(nèi)存泄漏就發(fā)生了

2.處理該channel

拿到對應(yīng)的attachment之后,netty做了如下判斷


processSelectedKey

1).對于boss NioEventLoop來說,輪詢到的是基本上就是連接事件,后續(xù)的事情就通過他的pipeline將連接扔給一個worker NioEventLoop處理

2).對于worker NioEventLoop來說,輪詢到的基本上都是io讀寫事件,后續(xù)的事情就是通過他的pipeline將讀取到的字節(jié)流傳遞給每個channelHandler來處理

3.判斷是否該再來次輪詢


netty的reactor線程經(jīng)歷前兩個步驟,分別是抓取產(chǎn)生過的IO事件以及處理IO事件,每次在抓到IO事件之后,都會將 needsToSelectAgain 重置為false,那么什么時候needsToSelectAgain會重新被設(shè)置成true呢?

needsToSelectAgain初始化都為false;needsToSelectAgain =false;

在NioEventLoop類中,只有下面一處將needsToSelectAgain設(shè)置為true

查看cancel方法被調(diào)用位置:

在channel從selector上移除的時候,調(diào)用cancel函數(shù)將key取消,并且當被去掉的key到達?CLEANUP_INTERVAL?的時候,設(shè)置needsToSelectAgain為true,CLEANUP_INTERVAL默認值為256

也就是說,對于每個NioEventLoop而言,每隔256個channel從selector上移除的時候,就標記 needsToSelectAgain 為true,我們還是跳回到上面這段代碼


每滿256次,就會進入到if的代碼塊,首先,將selectedKeys的內(nèi)部數(shù)組全部清空,方便被jvm垃圾回收,然后重新調(diào)用selectAgain重新填裝一下?selectionKey


netty這么做的目的我想應(yīng)該是每隔256次channel斷線,重新清理一下selectionKey,保證現(xiàn)存的SelectionKey及時有效

netty的reactor線程第二步做的事情為處理IO事件,netty使用數(shù)組替換掉jdk原生的HashSet來保證IO事件的高效處理,每個SelectionKey上綁定了netty類AbstractChannel對象作為attachment,在處理每個SelectionKey的時候,就可以找到AbstractChannel,然后通過pipeline的方式將處理串行到ChannelHandler,回調(diào)到用戶方法。不斷地輪詢是否有IO事件發(fā)生,并且在輪詢的過程中不斷檢查是否有定時任務(wù)和普通任務(wù),保證了netty的任務(wù)隊列中的任務(wù)得到有效執(zhí)行,輪詢過程順帶用一個計數(shù)器避開了了jdk空輪詢的bug。



reactor線程task的調(diào)度

runAllTasks(longtimeoutNanos);

代碼表示了盡量在一定的時間內(nèi),將所有的任務(wù)都取出來run一遍。timeoutNanos?表示該方法最多執(zhí)行這么長時間,reactor線程如果在此停留的時間過長,那么將積攢許多的IO事件無法處理(見reactor線程的前面兩個步驟),最終導(dǎo)致大量客戶端請求阻塞,因此,默認情況下,netty將控制內(nèi)部隊列的執(zhí)行時間

被調(diào)用情況:

NioEventLoop中run方法調(diào)用:

分析源碼runAllTasks:


從scheduledTaskQueue中的任務(wù)delayedTask轉(zhuǎn)移定時任務(wù)到taskQueue(mpsc queue);

從scheduledTaskQueue從拉取一個定時任務(wù)。首先分析fetchFromDelayedQueue()方法,由父類SingleThreadEventExecutor實現(xiàn)

功能是將延遲任務(wù)隊列(delayedTaskQueue)中已經(jīng)超過延遲執(zhí)行時間的任務(wù)遷移到非IO任務(wù)隊列(taskQueue)中.然后依次從taskQueue取出任務(wù)執(zhí)行,每執(zhí)行64個任務(wù),就進行耗時檢查,如果已執(zhí)行時間超過預(yù)先設(shè)定的執(zhí)行時間,則停止執(zhí)行非IO任務(wù),避免非IO任務(wù)太多,影響IO任務(wù)的執(zhí)行。

nanoTime()=System.nanoTime() -START_TIME;

循環(huán)執(zhí)行任務(wù)

下面為執(zhí)行任務(wù)的核心:

將已運行任務(wù)runTasks加一,每隔0x3F任務(wù),即每執(zhí)行完64個任務(wù)之后,判斷當前時間是否超過本次reactor任務(wù)循環(huán)的截止時間了,如果超過,那就break掉,如果沒有超過,那就繼續(xù)執(zhí)行。可以看到,netty對性能的優(yōu)化考慮地相當?shù)闹艿剑僭O(shè)netty任務(wù)隊列里面如果有海量小任務(wù),如果每次都要執(zhí)行完任務(wù)都要判斷一下是否到截止時間,那么效率是比較低下的

總結(jié):

當前reactor線程調(diào)用當前eventLoop執(zhí)行任務(wù),直接執(zhí)行,否則,添加到任務(wù)隊列稍后執(zhí)行

netty內(nèi)部的任務(wù)分為普通任務(wù)和定時任務(wù),分別落地到MpscQueue和PriorityQueue

netty每次執(zhí)行任務(wù)循環(huán)之前,會將已經(jīng)到期的定時任務(wù)從PriorityQueue轉(zhuǎn)移到MpscQueue

netty每隔64個任務(wù)檢查一下是否該退出任務(wù)循環(huán)

參考:

http://www.lxweimin.com/p/58fad8e42379


總結(jié):NioEventLoop實現(xiàn)的線程執(zhí)行邏輯做了以下事情

先后執(zhí)行IO任務(wù)和非IO任務(wù),兩類任務(wù)的執(zhí)行時間比由變量ioRatio控制,默認是非IO任務(wù)允許執(zhí)行和IO任務(wù)相同的時間

如果taskQueue存在非IO任務(wù),或者delayedTaskQueue存在已經(jīng)超時的任務(wù),則執(zhí)行非阻塞的selectNow()方法,否則執(zhí)行阻塞的select(time)方法

如果阻塞的select(time)方法立即返回0的次數(shù)超過某個值(默認為512次),說明觸發(fā)了epoll的cpu 100% bug,通過對selector進行rebuild解決:即重新創(chuàng)建一個selector,然后將原來的selector中已注冊的所有channel重新注冊到新的selector中,并將老的selectionKey全部cancel掉,最后將老的selector關(guān)閉

如果select的結(jié)果不為0,則依次處理每個ready的selectionKey,根據(jù)readyOps的值,進行不同的分發(fā)處理,譬如accept、read、write、connect等

執(zhí)行完IO任務(wù)后,再執(zhí)行非IO任務(wù),其中會將delayedTaskQueue已超時的任務(wù)加入到taskQueue中。每執(zhí)行64個任務(wù),就進行耗時檢查,如果已執(zhí)行時間超過通過ioRatio和之前執(zhí)行IO任務(wù)的耗時計算出來的非IO任務(wù)預(yù)計執(zhí)行時間,則停止執(zhí)行剩下的非IO任務(wù)


歡迎關(guān)注公眾號

![image.png](https://upload-images.jianshu.io/upload_images/9954986-ff18ec52a01cc662.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

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