Netty與Reactor模式

轉載自併發編程網 – ifeve.com (http://ifeve.com/netty-reactor-4/)


一:Netty、NIO、多線程?

時隔很久終於又更新了!之前一直遲遲未動也是因爲積累不夠,後面比較難下手。過年期間@李林鋒hw發佈了一個Netty5.0架構剖析和源碼解讀 ,看完也是收穫不少。前面的文章我們分析了Netty的結構,這次咱們來分析最錯綜複雜的一部分-Netty中的多線程以及NIO的應用。

理清NIO與Netty的關係之前,我們必須先要來看看Reactor模式。Netty是一個典型的多線程的Reactor模式的使用,理解了這部分,在宏觀上理解Netty的NIO及多線程部分就不會有什麼困難了。

本篇文章依然針對Netty 3.7,不過因爲也看過一點Netty 5的源碼,所以會有一點介紹。

 

二: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),剩餘代碼在這裏

01 public void run() {
02     try {
03         while (!Thread.interrupted()) {
04             selector.select();
05             Set selected = selector.selectedKeys();
06             Iterator it = selected.iterator();
07             while (it.hasNext())
08                 dispatch((SelectionKey) (it.next()));
09             selected.clear();
10         }
11     catch (IOException ex) { /* ... */
12     }
13 }

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等待時間。

Multiple Reactors

2、Netty中的Reactor與NIO

好了,瞭解了多線程下的Reactor模式,我們來看看Netty吧(以下部分主要針對NIO,OIO部分更加簡單一點,不重複介紹了)。Netty裏對應mainReactor的角色叫做“Boss”,而對應subReactor的角色叫做”Worker”。Boss負責分配請求,Worker負責執行,好像也很貼切!以TCP的Server端爲例,這兩個對應的實現類分別爲NioServerBossNioWorker(Server和Client的Worker沒有區別,因爲建立連接之後,雙方就是對等的進行傳輸了)。

Netty 3.7中Reactor的EventLoop在AbstractNioSelector.run()中,它實現了Runnable接口。這個類是Netty NIO部分的核心。它的邏輯非常複雜,其中還包括一些對JDK Bug的處理(例如rebuildSelector),剛開始讀的時候不需要深入那麼細節。我精簡了大部分代碼,保留主幹如下:

01 abstract class AbstractNioSelector implements NioSelector {
02  
03     //NIO Selector
04     protected volatile Selector selector;
05  
06     //內部任務隊列
07     private final Queue taskQueue = newConcurrentLinkedQueue();
08  
09     //selector循環
10     public void run() {
11         for (;;) {
12             try {
13                 //處理內部任務隊列
14                 processTaskQueue();
15                 //處理selector事件對應邏輯
16                 process(selector);
17             catch (Throwable t) {
18                 try {
19                     Thread.sleep(1000);
20                 catch (InterruptedException e) {
21                     // Ignore.
22                 }
23             }
24         }
25     }
26  
27     private void processTaskQueue() {
28         for (;;) {
29             final Runnable task = taskQueue.poll();
30             if (task == null) {
31                 break;
32             }
33             task.run();
34         }
35     }
36  
37     protected abstract void process(Selector selector)throws IOException;

其中process是主要的處理事件的邏輯,例如在AbstractNioWorker中,處理邏輯如下:

01 protected void process(Selector selector) throwsIOException {
02     Set selectedKeys = selector.selectedKeys();
03     if (selectedKeys.isEmpty()) {
04         return;
05     }
06     for (Iterator i = selectedKeys.iterator(); i.hasNext();) {
07         SelectionKey k = i.next();
08         i.remove();
09         try {
10             int readyOps = k.readyOps();
11             if ((readyOps & SelectionKey.OP_READ) != 0|| readyOps == 0) {
12                 if (!read(k)) {
13                     // Connection already closed - no need to handle write.
14                     continue;
15                 }
16             }
17             if ((readyOps & SelectionKey.OP_WRITE) !=0) {
18                 writeFromSelectorLoop(k);
19             }
20         catch (CancelledKeyException e) {
21             close(k);
22         }
23  
24         if (cleanUpCancelledKeys()) {
25             break// break the loop to avoid ConcurrentModificationException
26         }
27     }
28 }

這不就是第二部分提到的selector經典用法了麼?

在4.0之後,作者覺得NioSelector這個叫法,以及區分NioBossNioWorker的做法稍微繁瑣了點,乾脆就將這些合併成了NioEventLoop,從此這兩個角色就不做區分了。我倒是覺得新版本的會更優雅一點。

3、Netty中的多線程

下面我們來看Netty的多線程部分。一旦對應的Boss或者Worker啓動,就會分配給它們一個線程去一直執行。對應的概念爲BossPoolWorkerPool。對於每個NioServerSocketChannel,Boss的Reactor有一個線程,而Worker的線程數由Worker線程池大小決定,但是默認最大不會超過CPU核數*2,當然,這個參數可以通過NioServerSocketChannelFactory構造函數的參數來設置。

1 public NioServerSocketChannelFactory(
2         Executor bossExecutor, Executor workerExecutor,
3         int workerCount) {
4     this(bossExecutor, 1, workerExecutor, workerCount);
5 }

最後我們比較關心一個問題,我們之前ChannlePipeline中的ChannleHandler是在哪個線程執行的呢?答案是在Worker線程裏執行的,並且會阻塞Worker的EventLoop。例如,在NioWorker中,讀取消息完畢之後,會觸發MessageReceived事件,這會使得Pipeline中的handler都得到執行。

01 protected boolean read(SelectionKey k) {
02     ....
03  
04     if (readBytes > 0) {
05         // Fire the event.
06         fireMessageReceived(channel, buffer);
07     }
08  
09     return true;
10 }

可以看到,對於處理事件較長的業務,並不太適合直接放到ChannelHandler中執行。那麼怎麼處理呢?我們在Handler部分會進行介紹。

最後附上項目github地址,歡迎交流:https://github.com/code4craft/netty-learning

參考資料:

題圖來自:http://www.worldindustrialreporter.com/france-gives-green-light-to-tokamak-fusion-reactor/

原創文章,轉載請註明: 轉載自併發編程網 – ifeve.com

本文鏈接地址: Netty源碼解讀(四)Netty與Reactor模式


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章