Reactor 典型的 NIO 編程模型

Doug Lea 在 Scalable IO in Java 的 PPT 中描述了 Reactor 編程模型的思想,大部分 NIO 框架和一些中間件的 NIO 編程都與它一樣或是它的變體。本文結合 PPT 按照自己的理解整理而來,最終編寫了一個簡單的 NIO 回顯服務。

Reactor 之所以高效是因爲採用了分而治之事件驅動的設計。大部分網絡服務像 Web 服務器、分佈式對象的通信等大多數具有相同的基本處理流程:

  • 讀取請求數據 - read
  • 按協議解析請求 - decode
  • 業務處理 - process
  • 按協議編碼內容生成響應 - encode
  • 發送響應 - send

傳統的服務器設計是:一連接一處理線程,也就是我們常說的 BIO 編程。BIO 在讀取和發送時是阻塞的,在請求的整個生命週期內,不管有沒有數據可讀或待發送,都綁定和佔用了這個處理線程。

傳統模型中,線程的處理單元是一次完整的請求,爲了把線程解放出來,Reactor 對這個處理單元進行了分解。

1. 分而治之

Reactor 模式將處理過程分爲多個小任務,每個任務執行一個非阻塞的操作,任務通常由一個 IO 事件觸發執行。這種機制在 java.nio 中提供了支持:

  • 非阻塞的讀和寫
  • 調度與發生的 IO 事件關聯的任務

BIO 線程是以 read->decode->process->encode->send 的順序串行處理,NIO 將其分成了三個執行單元:讀取、業務處理和發送,處理過程如下:

  • 讀取:如果無數據可讀,線程返回線程池;發生讀IO事件,申請一個線程處理讀取,讀取結束後處理業務
  • 業務處理:線程同步處理完業務後,生成響應內容並編碼,返回線程池
  • 發送:發生寫IO事件,申請一個線程進行發送

可以看出一個明顯的區別就是,一次請求的處理過程是由多個不同的線程完成的,感覺和指令的串行執行並行執行有點類似。

分而治之的關鍵在於非阻塞,這樣就能充分利用線程,壓榨 CPU,提高系統的吞吐能力。

2. 事件驅動

通常比其他模型更高效,它使用的資源更少,不用針對每個請求啓用一條線程,減少了上下文切換,減少阻塞。但任務調度可能會慢,必須手動將事件和處理動作綁定。

通常編程比較困難,它必須爲服務設計多個邏輯狀態,以便跟蹤和中斷恢復,這也是在非阻塞編程中有大量狀態機運用的原因。

NIO 中總共設計了 4 種事件,每個事件發生都會調度關聯的任務,分別是:

  • OP_ACCEPT: 服務端監聽到了一個連接,準備接收
  • OP_CONNECT: 客戶端與服務器連接建立成功
  • OP_READ: 讀事件就緒,通道有數據可讀
  • OP_WRITE: 寫事件就緒,可以向通道寫入數據

java.nio 對事件驅動也提供了支持:

  • Channels: 連接到文件、Socket 等,支持非阻塞讀取
  • Buffers: 類似數組的對象,可由通道直接讀取或寫入
  • Selectors: 通知哪組通道有 IO 事件
  • SelectionKeys: 維護 IO 事件的狀態和綁定信息

3. Reactor 模式

Reactor 是一種設計模式,wikipedia 對其定義如下:

Reactor 是一個或多個輸入事件的處理模式,用於處理併發傳遞給服務處理程序的服務請求。服務處理程序判斷傳入請求發生的事件,並將它們同步的分派給關聯的請求處理程序。

Reactor 模式按照職責不同,通常可以把線程分爲 Reactor 線程、IO 線程和業務線程:

  • Reactor 線程:輪詢通知發生IO的通道,並分派合適的 Handler 處理
  • IO 線程:執行實際的讀寫操作
  • 業務線程:執行應用程序的業務邏輯

PPT 中介紹了三種版本的實現。

3.1 單線程版本

單線程版本

單線程版本就是用一個線程完成事件的通知、實際的 I/O 操作和業務處理。Reactor 作用就是要迅速的觸發 Handler ,顯然 Handler 處理的過程會導致 Reactor 變慢,此時可以將 非 IO 操作從 Reactor 線程中分離。

3.2 多線程版本

多線程版本

多線程版本將業務處理和 I/O 操作進行分離,Reactor 線程只關注事件分發和實際的 IO 操作,業務處理如協議的編解碼都分配給線程池處理。可能會有這樣的情況發生,業務處理很快,大部分的 Reactor 線程都在處理 IO,導致 CPU 閒置,降低了響應速度。

3.3 主從 Reactor 版本

主從 Reactor

主從 Reactor 版本設計了一個 主Reactor 用於處理連接接收事件,多個 從Reactor 處理實際的 I/O,分工合作,匹配 CPU 和 IO 速率。

3.4 一個變體

以上的三個版本中,Reactor 線程都參與了 IO 操作,有一種變體是把實際的 IO 操作也轉移到線程池線程中,這樣從數據的讀取到業務的處理都是由一個線程完成,可以避免鎖的競爭。

小結

知易行難,爲了更好的理解,這裏對 PPT 中介紹的3個版本實現了一個簡單的回顯服務,內部有比較詳細的代碼註釋。

源碼地址https://github.com/tonwu/reactor

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