程序員應如何理解 Reactor 模式?

大家好,我是小風哥!今天我們聊聊 reactor 模式。

在設計高併發高性能服務器時,一項關鍵的考慮就是 I/O。

I/O 是一個問題

有的同學可能會有疑問,爲什麼 I/O 會成爲問題?

假設有一個 web server,每分鐘有數百萬次的請求過來,服務器在處理請求時要訪問數據庫,同時該服務器也可能要請求其它的服務,一張典型的後端 server 可能的架構如圖所示:

一個用戶請求過來後 Server 可能需要訪問數據庫,然後再去請求另外幾個 server 後才能得到用戶請求的處理結果,然後將 response 返回給客戶端,從這張圖中數一數涉及到哪些 IO?

其實主要有兩種:

  1. 數據庫操作的磁盤 IO、文件 IO

  2. 網絡 IO

現在你已經知道了涉及哪些 IO,讓我們再來看一張圖:

我們可以看到,磁盤 IO 和網絡 IO 是非常慢的,也就是說我們通常用手機 APP、PC 瀏覽器打開一個頁面點擊一個按鈕到完全得到響應,這其中大部分的時間都消耗在了這兩種 IO 上,真正用在處理數據的 CPU 時間反而不是很多。

這告訴了我們一個道理,那就是高效處理 IO 對於高併發高性能服務器來說至關重要。

兩類經典設計模式

有兩類處理網絡請求的經典模型:

其中第一種模式我們在之前的_文章_中已經多次講解過了,這種模式會爲每個請求都創建一個線程或者進程:

但這種模式的一個問題就在於當併發數較多時需要創建很多的線程,創建線程過多會有性能問題。

第二種基於事件的模式我們在之前的文章中也講解過,在這種模式下我們只需要一個線程就能同時處理多個用戶請求:

在基於事件的併發編程中有一種叫做 Reactor 的模式非常流行,Node.js 以及 Nginx 就使用 Reactor,在這篇文章中我們詳細的講解一下高性能高併發服務器中的 Reactor 模式。

當然,在瞭解 Reactor 之前我們先來看一下咖啡館是怎樣運作的。

咖啡館是怎樣運作的

假設你有一家咖啡館,作爲老闆的你在前臺接待喝咖啡的顧客,你的生意不錯,來這裏喝咖啡的人絡繹不絕。

有時,有的人點的東西很簡單,比如來一杯咖啡或者牛奶之類,但也有一些顧客會點一些複雜的比如來一份意大利麪等,作爲前臺的你,如果這是停止接待顧客而且製作意大利麪的話那麼後續到來的所有顧客都要等待。

幸好,作爲老闆的你還有幾位大廚來幫忙,因此你只需要簡單的把製作意大利麪的命令交代下去就好了,“張三去煮麪條,李四去製作醬料,製作好後通知我”。

就這樣,即便前臺只有你一個人也能快速接待顧客的點餐,其實這背後本質上就是 Reactor 模式。

Reactor 模式

實際上每個你可以把咖啡館這個例子中每個顧客理解爲服務器接收的請求,前臺的服務員理解爲一個單線程的 while 循環,這個 while 循環有一個很形象的名字,event loop,這個 event loop 要做的事情非常簡單,那就是接收用戶請求,然後讓 handler,或者回調函數去處理,這裏的 handler 或者回調函數就好比大廚張三和李四去,handler 或者回調函數可以和 event loop 運行在同一個線程中,也可以和 event loop 各自運行在各自的線程中。

既然該模式是基於事件驅動,那麼都有哪些事件呢?

我們需要關心的典型事件這樣幾種:

看到了吧,這幾種 event 都是和 IO 相關的,涉及網絡和文件。

有的同學可能會問,那麼這個 event loop 是怎麼知道有這些 event 到來呢?

這是涉及到了 IO 多路複用技術,典型的像 Linux 中的 select、poll、epoll。

通過 IO 多路複用技術,我們可以一次監控一堆的文件描述符,當這些文件描述符對應的 IO 事件發生時會收到操作系統的通知,這時我們獲取到該 event 並交給相應的 handler 或者回調函數來處理。

總結下來,Reactor 的核心組成部分就是 event loop + IO 多路複用 + 回調函數

單線程 or 多線程

我們在上文提到過,處理 event 的 handler 可以和 event loop 運行在同一個線程中,也可以運行在不同的線程中。

如果是運行在同一個線程中那麼我們無需面對複雜的多線程問題,但在當前的多核時代,單線程無法充分利用多核資源,此外如果某個請求比較複雜需要佔用的 CPU 資源較多,那麼在單線程下其它所有的用戶請求都要等待,基於以上考慮我們可以使用線程池 (多線程) 技術。

event loop 在接收到 event 後,將 event 和處理 event 的 handler(回調函數) 打包發給線程池,線程池中的線程接收到打包後的任務後調用 handler(回調函數) 來處理相應的 event。

這樣我們的組合就成了 event loop + IO 多路複用 + 回調函數 + 線程池

把協程也加進來

回調函數的一大缺點在於如果處理用戶請求的邏輯比較複雜可能會導致回調地獄,關於回調地獄你可以參考_這裏_,協程這種技術在一定程度上解決了這一問題,讓我們可以用同步的方式來進行異步編程,關於協程你可以參考_這裏_和_這裏_。

最終我們的組合就成了 event loop + IO 多路複用 + 協程 + 線程池

接下來讓我們以 Node.js 來講解一下 Reactor 模式。

Node.js 與 Reactor 模式

我們來看一下 Node.js 的架構圖:

這張架構圖已經無比清晰的展示了 Reactor 模式是如何運行的。

1, 當用戶請求到來後需要將其放到一個隊列當中,因爲 event loop 是運行在單線程中的。

2,接下來 event loop 不斷檢測 event queue 中是否有 event 到來,如果隊列中有請求,那麼根據隊列的 “先來先服務” 原則,event loop 取出相應的 event,並將其交給線程池。

3,該線程池不斷檢測是否有 task 到來,這裏的 task 也就是將 event 和相應的回調函數打包後形成的。

4,線程接收到 task 後,線程池中的線程開始工作,比如查詢數據庫、讀取文件等等。

5,當線程處理完一個請求後調用 task 相應的回調函數,並將該處理結果 response 發送給 event loop。

6,event loop 在接收到處理結果後發送給客戶端。

怎樣,這是不是像極了上文中的咖啡館以及這裏的核反應堆。

這就是 Reactor 模式。

此外,Node.js 中的協程叫做 Fiber,都是用來以同步的方式來進行異步編程的,這裏就不詳細講解了。

Reactor vs Proactor

Reactor 模式中使用的 IO 都是同步 IO,什麼是同步 IO 呢?

就是說調用方在 IO 完成之前會被阻塞等待,這種 IO 更具體的就叫做同步阻塞式 IO。

但我們知道 event loop 是運行在一個線程中的,如果在 event loop 中調用同步阻塞式 IO 的話,那麼整個線程會被暫停運行,由於 event loop 就像咖啡廳前臺,非常關鍵,如果 event loop 所在線程被阻塞那麼所有的用戶請求都必須等待。

因此,在 event loop 中的 IO 不能是阻塞式的。

有同步阻塞式 IO 就有同步非阻塞式 IO。

什麼是同步非阻塞式 IO 呢?意思是當我們調用同步非阻塞式 IO 相關函數時,函數會立刻返回,並告訴我們文件是否可讀或者可寫,如果可讀或者可寫的話我們再真正的進行文件讀寫,這就是同步非阻塞式 IO。

Reactor 模式都是採用的同步非阻塞式 IO。

與同步 IO 相對應的是異步 IO。

在異步 IO 下我們需要將接收或者寫入數據的地址告訴操作系統,操作系統會將數據從進程地址空間寫入文件或者將文件內容寫到進程地址空間中,操作系統完成 IO 後會通知我們,這就是異步 IO。

執行異步 IO 同樣不會阻塞調用線程。

關於同步以及異步的概念你可以參考這裏。

而採用異步 IO 的事件驅動編程被稱爲 Proactor。

也就是說 Reactor 和 Proactor 的區別就在於一個採用同步 IO 一個採用異步 IO。

接下來我們用一個讀文件的例子來講解這兩者的差異。

Reactor 中的讀:

而 Proactor 的讀是這樣的:

現在你應該明白 Reactor 和 Proactor 的差異了吧。

總結

在這篇文章中我們詳細講解了高性能高併發目前流行的 Reactor 模式,其實其本質和咖啡館沒什麼區別,如果你善於觀察和思考的話那麼你會發現其實很多技術問題都能在現實生活中找到相似的場景。

希望這篇能對大家理解 Reactor 模式有幫助。

如果你喜歡小風哥的寫作風格並且對操作系統感興趣那麼小風哥自己寫的書《深入理解操作系統》絕不能錯過

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/kyEYNLtpsLZHK9n3j1pO0w