何為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ù)中,如下圖。
已知: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(),該方法會立即返回。
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
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ù)。
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)注公眾號
