何為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ù)中,如下圖。
已知: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ì)立即返回。
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
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ù)。
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)
