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

何為Reactor線程模型?



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


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

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

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

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

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

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

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



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

Netty的Reactor線程模型

1 Reactor單線程模型?

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


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

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

(1)一個(gè)NIO線程處理成千上萬(wàn)的鏈路,性能無(wú)法支撐,即使CPU的負(fù)荷達(dá)到100%;

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

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

2 Reactor多線程模型

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

Reactor多線程模型的特點(diǎn)如下:?

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

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

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

通過(guò)Reactor Thread Pool來(lái)提高event的分發(fā)能力。

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

3 Reactor主從模型


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

netty的線程模型

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

4. netty的線程模型

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

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

Boss線程池職責(zé)如下:?

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

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

worker線程池作用是:?

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

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

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

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

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

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

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

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

如下圖,是一個(gè)NioEventLoop的處理鏈:



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

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

3.2.1 Task

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

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

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

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

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

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

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

Selector

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

ioEevntLoopGroup

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

EventLoopGroup繼承了EventExecutorGroup接口,并新添了3個(gè)方法

EventLoop next()

ChannelFuture register(Channel channel)

ChannelFuture register(Channel channel, ChannelPromise promise)

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

NioEventLoopGroup還有幾點(diǎn)需要注意:

NioEventLoopGroup下默認(rèn)的NioEventLoop個(gè)數(shù)為cpu核數(shù) * 2,因?yàn)橛泻芏嗟膇o處理

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

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

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

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

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

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

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

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

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

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

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

reactor 線程的啟動(dòng)

1)bossGroup初始化時(shí),啟動(dòng)線程,過(guò)程如下

EventLoopGroup bossGroup =new NioEventLoopGroup();

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

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

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

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

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

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

已知:class MultithreadEventLoopGroupextends MultithreadEventExecutorGroupimplements EventLoopGroup

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

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

??變量children每個(gè)元素的真實(shí)類型為NioEventLoop,

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

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

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

跳轉(zhuǎn)到SingleThreadEventExecutor中:

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

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

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

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

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

總結(jié):

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

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

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

? ? ? 3、每個(gè)NioEventLoop實(shí)例都持有一個(gè)selector,并對(duì)selector進(jìn)行優(yōu)化。

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

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


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

reactor 線程的執(zhí)行

參考內(nèi)容:

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

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


NioEventLoop中維護(hù)了一個(gè)線程,線程啟動(dòng)時(shí)會(huì)調(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í)行時(shí)間比由變量ioRatio控制,默認(rèn)為50,則表示允許非IO任務(wù)執(zhí)行的時(shí)間與IO任務(wù)的執(zhí)行時(shí)間相等。

初始化時(shí) ioRatio為50:


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

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

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

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

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

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

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


圖A

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

selectCnt == 0(selectCnt 用來(lái)記錄selector.select方法的執(zhí)行次數(shù)和標(biāo)識(shí)是否執(zhí)行過(guò)selector.selectNow())

1、delayNanos(currentTimeNanos):

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

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


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

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

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

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

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

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

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

oldWakenUp 參數(shù)為true

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

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

用戶主動(dòng)喚醒(wakenUp.get())


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

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

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



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

此處解決方式:

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

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

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

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


rebuildSelector()?

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

方法由下面三個(gè)圖組成

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

轉(zhuǎn)移步驟為

1. 拿到有效的key

2. 取消該key在舊的selector上的事件注冊(cè)

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

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

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



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

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

Netty的解決辦法

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

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

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


processSelectedKeys?

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

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

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

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

selectedKeys?被引用過(guò)的地方

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

SelectedSelectionKeySet,內(nèi)部用兩個(gè)數(shù)組存儲(chǔ),初始分配數(shù)組大小置為1024避免頻繁擴(kuò)容,當(dāng)大小超過(guò)1024時(shí),對(duì)數(shù)組進(jìn)行雙倍擴(kuò)容。源碼分析前文有描述。



processSelectedKeysOptimized

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

該過(guò)程分為以下三個(gè)步驟:

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

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

2.處理該channel

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


processSelectedKey

1).對(duì)于boss NioEventLoop來(lái)說(shuō),輪詢到的是基本上就是連接事件,后續(xù)的事情就通過(guò)他的pipeline將連接扔給一個(gè)worker NioEventLoop處理

2).對(duì)于worker NioEventLoop來(lái)說(shuō),輪詢到的基本上都是io讀寫(xiě)事件,后續(xù)的事情就是通過(guò)他的pipeline將讀取到的字節(jié)流傳遞給每個(gè)channelHandler來(lái)處理

3.判斷是否該再來(lái)次輪詢


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

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

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

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

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

也就是說(shuō),對(duì)于每個(gè)NioEventLoop而言,每隔256個(gè)channel從selector上移除的時(shí)候,就標(biāo)記 needsToSelectAgain 為true,我們還是跳回到上面這段代碼


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


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

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



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

runAllTasks(longtimeoutNanos);

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

被調(diào)用情況:

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

分析源碼runAllTasks:


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

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

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

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

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

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

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

總結(jié):

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

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

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

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

參考:

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


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

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

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

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

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

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


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

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