請戳GitHub原文: github.com/wangzhiwubi…
更多文章關注:多執行緒/集合/分散式/Netty/NIO/RPC
- Java高階特性增強-集合
- Java高階特性增強-多執行緒
- Java高階特性增強-Synchronized
- Java高階特性增強-volatile
- Java高階特性增強-併發集合框架
- Java高階特性增強-分散式
- Java高階特性增強-Zookeeper
- Java高階特性增強-JVM
- Java高階特性增強-NIO
- RPC
- zookeeper
- JVM
- NIO
- 其他更多
請戳GitHub原文: https://github.com/wangzhiwubigdata/God-Of-BigData
關注公眾號,內推,面試,資源下載,關注更多大資料技術~
大資料成神之路~預計更新500+篇文章,已經更新60+篇~
複製程式碼
一:Netty、NIO、多執行緒?
理清NIO與Netty的關係之前,我們必須先要來看看Reactor模式。Netty是一個典型的多執行緒的Reactor模式的使用,理解了這部分,在巨集觀上理解Netty的NIO及多執行緒部分就不會有什麼困難了。
二:Reactor
1、Reactor的由來
Reactor是一種廣泛應用在伺服器端開發的設計模式。Reactor中文大多譯為“反應堆”,我當初接觸這個概念的時候,就感覺很厲害,是不是它的原理就跟“核反應”差不多?後來才知道其實沒有什麼關係,從Reactor的兄弟“Proactor”(多譯為前攝器)就能看得出來,這兩個詞的中文翻譯其實都不是太好,不夠形象。實際上,Reactor模式又有別名“Dispatcher”或者“Notifier”,我覺得這兩個都更加能表明它的本質。
那麼,Reactor模式究竟是個什麼東西呢?這要從事件驅動的開發方式說起。我們知道,對於應用伺服器,一個主要規律就是,CPU的處理速度是要遠遠快於IO速度的,如果CPU為了IO操作(例如從Socket讀取一段資料)而阻塞顯然是不划算的。好一點的方法是分為多程式或者執行緒去進行處理,但是這樣會帶來一些程式切換的開銷,試想一個程式一個資料讀了500ms,期間程式切換到它3次,但是CPU卻什麼都不能幹,就這麼切換走了,是不是也不划算?
這時先驅們找到了事件驅動,或者叫回撥的方式,來完成這件事情。這種方式就是,應用業務向一箇中間人註冊一個回撥(event handler),當IO就緒後,就這個中間人產生一個事件,並通知此handler進行處理。這種回撥的方式,也體現了“好萊塢原則”(Hollywood principle)-“Don't call us, we'll call you”,在我們熟悉的IoC中也有用到。看來軟體開發真是互通的!
好了,我們現在來看Reactor模式。在前面事件驅動的例子裡有個問題:我們如何知道IO就緒這個事件,誰來充當這個中間人?Reactor模式的答案是:由一個不斷等待和迴圈的單獨程式(執行緒)來做這件事,它接受所有handler的註冊,並負責先作業系統查詢IO是否就緒,在就緒後就呼叫指定handler進行處理,這個角色的名字就叫做Reactor。
2、Reactor與NIO
Java中的NIO可以很好的和Reactor模式結合。關於NIO中的Reactor模式,我想沒有什麼資料能比Doug Lea大神(不知道Doug Lea?看看JDK集合包和併發包的作者吧)在《Scalable IO in Java》解釋的更簡潔和全面了。NIO中Reactor的核心是Selector
,我寫了一個簡單的Reactor示例,這裡我貼一個核心的Reactor的迴圈(這種迴圈結構又叫做EventLoop
),剩餘程式碼在learning-src目錄下。
public void run() {
try {
while (!Thread.interrupted()) {
selector.select();
Set selected = selector.selectedKeys();
Iterator it = selected.iterator();
while (it.hasNext())
dispatch((SelectionKey) (it.next()));
selected.clear();
}
} catch (IOException ex) { /* ... */
}
}
複製程式碼
3、與Reactor相關的其他概念
前面提到了Proactor模式,這又是什麼呢?簡單來說,Reactor模式裡,作業系統只負責通知IO就緒,具體的IO操作(例如讀寫)仍然是要在業務程式裡阻塞的去做的,而Proactor模式則更進一步,由作業系統將IO操作執行好(例如讀取,會將資料直接讀到記憶體buffer中),而handler只負責處理自己的邏輯,真正做到了IO與程式處理非同步執行。所以我們一般又說Reactor是同步IO,Proactor是非同步IO。
關於阻塞和非阻塞、非同步和非非同步,以及UNIX底層的機制,大家可以看看這篇文章IO - 同步,非同步,阻塞,非阻塞 (亡羊補牢篇),以及陶輝(《深入理解nginx》的作者)《高效能網路程式設計》的系列。
三:由Reactor出發來理解Netty
1、多執行緒下的Reactor
講了一堆Reactor,我們回到Netty。在《Scalable IO in Java》中講到了一種多執行緒下的Reactor模式。在這個模式裡,mainReactor只有一個,負責響應client的連線請求,並建立連線,它使用一個NIO Selector;subReactor可以有一個或者多個,每個subReactor都會在一個獨立執行緒中執行,並且維護一個獨立的NIO Selector。
這樣的好處很明顯,因為subReactor也會執行一些比較耗時的IO操作,例如訊息的讀寫,使用多個執行緒去執行,則更加有利於發揮CPU的運算能力,減少IO等待時間。
2、Netty中的Reactor與NIO
好了,瞭解了多執行緒下的Reactor模式,我們來看看Netty吧(以下部分主要針對NIO,OIO部分更加簡單一點,不重複介紹了)。Netty裡對應mainReactor的角色叫做“Boss”,而對應subReactor的角色叫做"Worker"。Boss負責分配請求,Worker負責執行,好像也很貼切!以TCP的Server端為例,這兩個對應的實現類分別為NioServerBoss
和NioWorker
(Server和Client的Worker沒有區別,因為建立連線之後,雙方就是對等的進行傳輸了)。
Netty 3.7中Reactor的EventLoop在AbstractNioSelector.run()
中,它實現了Runnable
介面。這個類是Netty NIO部分的核心。它的邏輯非常複雜,其中還包括一些對JDK Bug的處理(例如rebuildSelector
),剛開始讀的時候不需要深入那麼細節。我精簡了大部分程式碼,保留主幹如下:
abstract class AbstractNioSelector implements NioSelector {
//NIO Selector
protected volatile Selector selector;
//內部任務佇列
private final Queue<Runnable> taskQueue = new ConcurrentLinkedQueue<Runnable>();
//selector迴圈
public void run() {
for (;;) {
try {
//處理內部任務佇列
processTaskQueue();
//處理selector事件對應邏輯
process(selector);
} catch (Throwable t) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// Ignore.
}
}
}
}
private void processTaskQueue() {
for (;;) {
final Runnable task = taskQueue.poll();
if (task == null) {
break;
}
task.run();
}
}
protected abstract void process(Selector selector) throws IOException;
}
複製程式碼
其中process是主要的處理事件的邏輯,例如在AbstractNioWorker
中,處理邏輯如下:
protected void process(Selector selector) throws IOException {
Set<SelectionKey> selectedKeys = selector.selectedKeys();
if (selectedKeys.isEmpty()) {
return;
}
for (Iterator<SelectionKey> i = selectedKeys.iterator(); i.hasNext();) {
SelectionKey k = i.next();
i.remove();
try {
int readyOps = k.readyOps();
if ((readyOps & SelectionKey.OP_READ) != 0 || readyOps == 0) {
if (!read(k)) {
// Connection already closed - no need to handle write.
continue;
}
}
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
writeFromSelectorLoop(k);
}
} catch (CancelledKeyException e) {
close(k);
}
if (cleanUpCancelledKeys()) {
break; // break the loop to avoid ConcurrentModificationException
}
}
}
複製程式碼
這不就是第二部分提到的selector經典用法了麼?
在Netty 4.0之後,作者覺得NioSelector
這個叫法,以及區分NioBoss
和NioWorker
的做法稍微繁瑣了點,乾脆就將這些合併成了NioEventLoop
,從此這兩個角色就不做區分了。我倒是覺得新版本的會更優雅一點。
3、Netty中的多執行緒
下面我們來看Netty的多執行緒部分。一旦對應的Boss或者Worker啟動,就會分配給它們一個執行緒去一直執行。對應的概念為BossPool
和WorkerPool
。對於每個NioServerSocketChannel
,Boss的Reactor有一個執行緒,而Worker的執行緒數由Worker執行緒池大小決定,但是預設最大不會超過CPU核數*2,當然,這個引數可以通過NioServerSocketChannelFactory
建構函式的引數來設定。
public NioServerSocketChannelFactory(
Executor bossExecutor, Executor workerExecutor,
int workerCount) {
this(bossExecutor, 1, workerExecutor, workerCount);
}
複製程式碼
最後我們比較關心一個問題,我們之前ChannlePipeline
中的ChannleHandler是在哪個執行緒執行的呢?答案是在Worker執行緒裡執行的,並且會阻塞Worker的EventLoop。例如,在NioWorker
中,讀取訊息完畢之後,會觸發MessageReceived
事件,這會使得Pipeline中的handler都得到執行。
protected boolean read(SelectionKey k) {
....
if (readBytes > 0) {
// Fire the event.
fireMessageReceived(channel, buffer);
}
return true;
}
複製程式碼
可以看到,對於處理事件較長的業務,並不太適合直接放到ChannelHandler中執行。那麼怎麼處理呢?我們在Handler部分會進行介紹。
參考資料:
- Scalable IO in Java gee.cs.oswego.edu/dl/cpjslide…
- Netty5.0架構剖析和原始碼解讀 vdisk.weibo.com/s/C9LV9iVqH…
- Reactor pattern en.wikipedia.org/wiki/Reacto…
- Reactor - An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events www.cs.wustl.edu/~schmidt/PD…
- 高效能網路程式設計6--reactor反應堆與定時器管理 blog.csdn.net/russell_tao…
- IO - 同步,非同步,阻塞,非阻塞 (亡羊補牢篇)blog.csdn.net/historyasam…
題圖來自:www.worldindustrialreporter.com/france-give…
請戳GitHub原文: https://github.com/wangzhiwubigdata/God-Of-BigData
關注公眾號,內推,面試,資源下載,關注更多大資料技術~
大資料成神之路~預計更新500+篇文章,已經更新60+篇~
複製程式碼