單服務器高性能模式:Reactor與Proactor

我介紹了單服務器高性能的 PPC 和 TPC 模式,它們的優勢是實現簡單,缺點是都沒法支撐高併發的場景,尤爲是互聯網發展到如今,各類海量用戶業務的出現,PPC 和 TPC 徹底無能爲力。今天我將介紹能夠應對高併發場景的單服務器高性能架構模式:Reactor 和 Proactor。編程

Reactor

PPC 模式最主要的問題就是每一個鏈接都要建立進程(爲了描述簡潔,這裏只以 PPC 和進程爲例,實際上換成 TPC 和線程,原理是同樣的),鏈接結束後進程就銷燬了,這樣作實際上是很大的浪費。爲了解決這個問題,一個天然而然的想法就是資源複用,即再也不單獨爲每一個鏈接建立進程,而是建立一個進程池,將鏈接分配給進程,一個進程能夠處理多個鏈接的業務。安全

引入資源池的處理方式後,會引出一個新的問題:進程如何才能高效地處理多個鏈接的業務?當一個鏈接一個進程時,進程能夠採用「read -> 業務處理 -> write」的處理流程,若是當前鏈接沒有數據能夠讀,則進程就阻塞在 read 操做上。這種阻塞的方式在一個鏈接一個進程的場景下沒有問題,但若是一個進程處理多個鏈接,進程阻塞在某個鏈接的 read 操做上,此時即便其餘鏈接有數據可讀,進程也沒法去處理,很顯然這樣是沒法作到高性能的。服務器

解決這個問題的最簡單的方式是將 read 操做改成非阻塞,而後進程不斷地輪詢多個鏈接。這種方式可以解決阻塞的問題,但解決的方式並不優雅。首先,輪詢是要消耗 CPU 的;其次,若是一個進程處理幾千上萬的鏈接,則輪詢的效率是很低的。網絡

爲了可以更好地解決上述問題,很容易能夠想到,只有當鏈接上有數據的時候進程纔去處理,這就是 I/O 多路複用技術的來源。多線程

I/O 多路複用技術概括起來有兩個關鍵實現點:架構

  • 當多條鏈接共用一個阻塞對象後,進程只須要在一個阻塞對象上等待,而無須再輪詢全部鏈接,常見的實現方式有 select、epoll、kqueue 等。併發

  • 當某條鏈接有新的數據能夠處理時,操做系統會通知進程,進程從阻塞狀態返回,開始進行業務處理。運維

I/O 多路複用結合線程池,完美地解決了 PPC 和 TPC 的問題,並且「大神們」給它取了一個很牛的名字:Reactor,中文是「反應堆」。聯想到「核反應堆」,聽起來就很嚇人,實際上這裏的「反應」不是聚變、裂變反應的意思,而是「事件反應」的意思,能夠通俗地理解爲「來了一個事件我就有相應的反應」,這裏的「我」就是 Reactor,具體的反應就是咱們寫的代碼,Reactor 會根據事件類型來調用相應的代碼進行處理。Reactor 模式也叫 Dispatcher 模式(在不少開源的系統裏面會看到這個名稱的類,其實就是實現 Reactor 模式的),更加貼近模式自己的含義,即 I/O 多路複用統一監聽事件,收到事件後分配(Dispatch)給某個進程。異步

Reactor 模式的核心組成部分包括 Reactor 和處理資源池(進程池或線程池),其中 Reactor 負責監聽和分配事件,處理資源池負責處理事件。初看 Reactor 的實現是比較簡單的,但實際上結合不一樣的業務場景,Reactor 模式的具體實現方案靈活多變,主要體如今:編程語言

  • Reactor 的數量能夠變化:能夠是一個 Reactor,也能夠是多個 Reactor。

  • 資源池的數量能夠變化:以進程爲例,能夠是單個進程,也能夠是多個進程(線程相似)。

將上面兩個因素排列組合一下,理論上能夠有 4 種選擇,但因爲「多 Reactor 單進程」實現方案相比「單 Reactor 單進程」方案,既複雜又沒有性能優點,所以「多 Reactor 單進程」方案僅僅是一個理論上的方案,實際沒有應用。

最終 Reactor 模式有這三種典型的實現方案:

  • 單 Reactor 單進程 / 線程。

  • 單 Reactor 多線程。

  • 多 Reactor 多進程 / 線程。

以上方案具體選擇進程仍是線程,更多地是和編程語言及平臺相關。例如,Java 語言通常使用線程(例如,Netty),C 語言使用進程和線程均可以。例如,Nginx 使用進程,Memcache 使用線程。

1. 單 Reactor 單進程 / 線程

單 Reactor 單進程 / 線程的方案示意圖以下(以進程爲例):



注意,select、accept、read、send 是標準的網絡編程 API,dispatch 和「業務處理」是須要完成的操做,其餘方案示意圖相似。

詳細說明一下這個方案:

  • Reactor 對象經過 select 監控鏈接事件,收到事件後經過 dispatch 進行分發。

  • 若是是鏈接創建的事件,則由 Acceptor 處理,Acceptor 經過 accept 接受鏈接,並建立一個 Handler 來處理鏈接後續的各類事件。

  • 若是不是鏈接創建事件,則 Reactor 會調用鏈接對應的 Handler(第 2 步中建立的 Handler)來進行響應。

  • Handler 會完成 read-> 業務處理 ->send 的完整業務流程。

單 Reactor 單進程的模式優勢就是很簡單,沒有進程間通訊,沒有進程競爭,所有都在同一個進程內完成。但其缺點也是很是明顯,具體表現有:

  • 只有一個進程,沒法發揮多核 CPU 的性能;只能採起部署多個系統來利用多核 CPU,但這樣會帶來運維複雜度,原本只要維護一個系統,用這種方式須要在一臺機器上維護多套系統。

  • Handler 在處理某個鏈接上的業務時,整個進程沒法處理其餘鏈接的事件,很容易致使性能瓶頸。

所以,單 Reactor 單進程的方案在實踐中應用場景很少,只適用於業務處理很是快速的場景,目前比較著名的開源軟件中使用單 Reactor 單進程的是 Redis。

須要注意的是,C 語言編寫系統的通常使用單 Reactor 單進程,由於沒有必要在進程中再建立線程;而 Java 語言編寫的通常使用單 Reactor 單線程,由於 Java 虛擬機是一個進程,虛擬機中有不少線程,業務線程只是其中的一個線程而已。

2. 單 Reactor 多線程

爲了克服單 Reactor 單進程 / 線程方案的缺點,引入多進程 / 多線程是顯而易見的,這就產生了第 2 個方案:單 Reactor 多線程。

單 Reactor 多線程方案示意圖是:



我來介紹一下這個方案:

  • 主線程中,Reactor 對象經過 select 監控鏈接事件,收到事件後經過 dispatch 進行分發。

  • 若是是鏈接創建的事件,則由 Acceptor 處理,Acceptor 經過 accept 接受鏈接,並建立一個 Handler 來處理鏈接後續的各類事件。

  • 若是不是鏈接創建事件,則 Reactor 會調用鏈接對應的 Handler(第 2 步中建立的 Handler)來進行響應。

  • Handler 只負責響應事件,不進行業務處理;Handler 經過 read 讀取到數據後,會發給 Processor 進行業務處理。

  • Processor 會在獨立的子線程中完成真正的業務處理,而後將響應結果發給主進程的 Handler 處理;Handler 收到響應後經過 send 將響應結果返回給 client。

單 Reator 多線程方案可以充分利用多核多 CPU 的處理能力,但同時也存在下面的問題:

  • 多線程數據共享和訪問比較複雜。例如,子線程完成業務處理後,要把結果傳遞給主線程的 Reactor 進行發送,這裏涉及共享數據的互斥和保護機制。以 Java 的 NIO 爲例,Selector 是線程安全的,可是經過 Selector.selectKeys() 返回的鍵的集合是非線程安全的,對 selected keys 的處理必須單線程處理或者採起同步措施進行保護。

  • Reactor 承擔全部事件的監聽和響應,只在主線程中運行,瞬間高併發時會成爲性能瓶頸。

你可能會發現,我只列出了「單 Reactor 多線程」方案,沒有列出「單 Reactor 多進程」方案,這是什麼緣由呢?主要緣由在於若是採用多進程,子進程完成業務處理後,將結果返回給父進程,並通知父進程發送給哪一個 client,這是很麻煩的事情。由於父進程只是經過 Reactor 監聽各個鏈接上的事件而後進行分配,子進程與父進程通訊時並非一個鏈接。若是要將父進程和子進程之間的通訊模擬爲一個鏈接,並加入 Reactor 進行監聽,則是比較複雜的。而採用多線程時,由於多線程是共享數據的,所以線程間通訊是很是方便的。雖然要額外考慮線程間共享數據時的同步問題,但這個複雜度比進程間通訊的複雜度要低不少。

3. 多 Reactor 多進程 / 線程

爲了解決單 Reactor 多線程的問題,最直觀的方法就是將單 Reactor 改成多 Reactor,這就產生了第 3 個方案:多 Reactor 多進程 / 線程。

多 Reactor 多進程 / 線程方案示意圖是(以進程爲例):



方案詳細說明以下:

  • 父進程中 mainReactor 對象經過 select 監控鏈接創建事件,收到事件後經過 Acceptor 接收,將新的鏈接分配給某個子進程。

  • 子進程的 subReactor 將 mainReactor 分配的鏈接加入鏈接隊列進行監聽,並建立一個 Handler 用於處理鏈接的各類事件。

  • 當有新的事件發生時,subReactor 會調用鏈接對應的 Handler(即第 2 步中建立的 Handler)來進行響應。

  • Handler 完成 read→業務處理→send 的完整業務流程。

多 Reactor 多進程 / 線程的方案看起來比單 Reactor 多線程要複雜,但實際實現時反而更加簡單,主要緣由是:

  • 父進程和子進程的職責很是明確,父進程只負責接收新鏈接,子進程負責完成後續的業務處理。

  • 父進程和子進程的交互很簡單,父進程只須要把新鏈接傳給子進程,子進程無須返回數據。

  • 子進程之間是互相獨立的,無須同步共享之類的處理(這裏僅限於網絡模型相關的 select、read、send 等無須同步共享,「業務處理」仍是有可能須要同步共享的)。

目前著名的開源系統 Nginx 採用的是多 Reactor 多進程,採用多 Reactor 多線程的實現有 Memcache 和 Netty。

我多說一句,Nginx 採用的是多 Reactor 多進程的模式,但方案與標準的多 Reactor 多進程有差別。具體差別表現爲主進程中僅僅建立了監聽端口,並無建立 mainReactor 來「accept」鏈接,而是由子進程的 Reactor 來「accept」鏈接,經過鎖來控制一次只有一個子進程進行「accept」,子進程「accept」新鏈接後就放到本身的 Reactor 進行處理,不會再分配給其餘子進程,更多細節請查閱相關資料或閱讀 Nginx 源碼。

Proactor

Reactor 是非阻塞同步網絡模型,由於真正的 read 和 send 操做都須要用戶進程同步操做。這裏的「同步」指用戶進程在執行 read 和 send 這類 I/O 操做的時候是同步的,若是把 I/O 操做改成異步就可以進一步提高性能,這就是異步網絡模型 Proactor。

Proactor 中文翻譯爲「前攝器」比較難理解,與其相似的單詞是 proactive,含義爲「主動的」,所以咱們照貓畫虎翻譯爲「主動器」反而更好理解。Reactor 能夠理解爲「來了事件我通知你,你來處理」,而 Proactor 能夠理解爲「來了事件我來處理,處理完了我通知你」。這裏的「我」就是操做系統內核,「事件」就是有新鏈接、有數據可讀、有數據可寫的這些 I/O 事件,「你」就是咱們的程序代碼。

Proactor 模型示意圖是:



詳細介紹一下 Proactor 方案:

  • Proactor Initiator 負責建立 Proactor 和 Handler,並將 Proactor 和 Handler 都經過 Asynchronous Operation Processor 註冊到內核。

  • Asynchronous Operation Processor 負責處理註冊請求,並完成 I/O 操做。

  • Asynchronous Operation Processor 完成 I/O 操做後通知 Proactor。

  • Proactor 根據不一樣的事件類型回調不一樣的 Handler 進行業務處理。

  • Handler 完成業務處理,Handler 也能夠註冊新的 Handler 到內核進程。

理論上 Proactor 比 Reactor 效率要高一些,異步 I/O 可以充分利用 DMA 特性,讓 I/O 操做與計算重疊,但要實現真正的異步 I/O,操做系統須要作大量的工做。目前 Windows 下經過 IOCP 實現了真正的異步 I/O,而在 Linux 系統下的 AIO 並不完善,所以在 Linux 下實現高併發網絡編程時都是以 Reactor 模式爲主。因此即便 Boost.Asio 號稱實現了 Proactor 模型,其實它在 Windows 下采用 IOCP,而在 Linux 下是用 Reactor 模式(採用 epoll)模擬出來的異步模型。

小結

今天我爲你講了單服務器支持高併發的高性能架構模式 Reactor 和 Proactor,但願對你有所幫助。

這就是今天的所有內容,留一道思考題給你吧,針對「前浪微博」消息隊列架構的案例,你以爲採用何種併發模式是比較合適的,爲何?

相關文章
相關標籤/搜索