高性能網絡編程之 Reactor 網絡模型(徹底搞懂)

前言

網絡框架的設計離不開 I/O 線程模型,線程模型的優劣直接決定了系統的吞吐量、可擴展性、安全性等。

目前主流的網絡框架,在網絡 IO 處理層面幾乎都採用了「I/O 多路複用方案」,這是服務端應對高併發的性能利器。

進一步看,當上升到整個網絡模塊時,另一個常常聽說的模式出現了 ---- 「Reactor 模式」,也叫反應器模式,本質是一個事件轉發器,是網絡模塊核心中樞,負責將讀寫事件分發給對應的讀寫事件處理者。

大名鼎鼎的 Java 併發包作者 Doug Lea,在 Scalable I/O in Java 一文中闡述了服務端開發中 I/O 模型的演進過程。netty 中三種 reactor 線程模型也來源於這篇經典文章。

通常情況下,reactor 模式的網絡 IO 層會採用經典的 IO 多路複用方案,強強聯手,妥妥的高性能的代名詞、扛把子!!!是衆多開源項目普遍的解決方案。

一、網絡 IO 進化史

我們先來看看,一個網絡請求在服務端經歷了哪些階段:

可以看到,網絡請求先後經歷 服務器網卡、內核、連接建立、數據讀取、業務處理、數據寫回等一系列過程。

其中,連接建立 (accept)、數據讀取(read)、數據寫回(write) 等操作都需要操作系統內核提供的系統調用,最終由內核與網卡進行數據交互,這些 IO 調用消耗一般是比較高的,比如 IO 等待、數據傳輸等。

最初的處理方式是,每個連接都用獨立的一個線程來處理這一系列的操作,即 建立連接、數據讀寫、業務邏輯處理;這樣一來最大的弊端在於,N 個連接就需要 N 個線程資源,消耗巨大。

所以,在網絡模型演化過程中,不斷的對這幾個階段進行拆分,比如,將建立連接、數據讀寫、業務邏輯處理等關鍵階段分開處理。這樣一來,每個階段都可以考慮使用單線程或者線程池來處理,極大的節約線程資源,又能獲得超高性能。

1. 非阻塞 IO

阻塞 IO:通常是用戶態線程通過系統調用阻塞讀取網卡傳遞的數據,我們知道,在 TCP 三次握手建立連接之後,真正等待數據的到來需要一定時間;

這個時候,在該模式下用戶線程會一直阻塞等待網卡數據準備就緒,直到完成數據讀寫完成;可以看到,用戶線程大部分都在等待 IO 事件就緒,造成資源的急劇浪費。

非阻塞 IO :與阻塞 IO 相反,如果數據未就緒會直接返回,應用層輪詢讀取 / 查詢,直到成功讀取數據。

I/O 多路複用: 是非阻塞 IO 的一種特例,也是目前最經典、最常用的高性能 IO 模型。其具體處理方式是:先查詢 IO 事件是否準備就緒,當 IO 事件準備就緒了,則會真正的通過系統調用實現數據讀寫;

查詢操作,不管是否數據準備就緒都會立即返回,即非阻塞;因此,通常情況下,會通過輪訓來不斷監聽 IO 事件是否準備就緒;因爲操作是非阻塞的,這個過程中通常只需及少量線程(一般一個線程即可)來處理這個輪訓操作,極大的解決阻塞模式下 IO 枯竭問題。

這種一個線程就可以監聽所有網絡連接的 IO 事件是否準備就緒的模式,就是大名鼎鼎的 IO 多路複用。

2. 事件驅動?

前面我們提到:將一個正常的請求分成多段來看待,每一段都可以分別進行優化(看場景需要)。

經典的一種切分方法是將「連接」和「業務線程」分開處理,當「連接層」有事件觸發時提交給「業務線程」,避免了業務線程因「網絡數據處於準備中」導致的長時間等待問題,節省線程資源,這就是大名鼎鼎的事件驅動模型

事件驅動的核心是,以事件爲連接點,當有 IO 事件準備就緒時,以事件的形式通知相關線程進行數據讀寫,進而業務線程可以直接處理這些數據,這一過程的後續操作方,都是被動接收通知,看起來有點像回調操作;

這種模式下,IO 讀寫線程、業務線程工作時,必有數據可操作執行,不會在 IO 等待上浪費資源,這便是事件驅動的核心思想。

舉個簡單例子,10 個士兵接到命令,在接下來將執行祕密任務,但具體時間待定;一種方式時,這 10 個士兵自己掌握主動權,隔一段時間就會自己詢問將軍是否準備執行任務,這種模式比較低下,因爲士兵需要花很多精力自己去確認任務執行時間,同時也會耽擱自己的訓練時間。

另一種方式爲,士兵接到即將執行祕密任務的通知後,會自己做好準備隨時執行,在最終執行命名沒下達之前,會繼續自己的日常訓練;等需要執行任務時,將軍會立刻通知士兵們立即行動;很顯然,這種模式,士兵們的時間資源並沒有浪費。這便是事件驅動的優勢所在。

好了,到此相信你已經明白了什麼是事件驅動了。Reactor 模型的核心便是事件驅動,同時,爲了讓其網絡 IO 層擁有了高性能的能力,一般會採用 IO 多路複用處理方案。

3.Reactor 模型?

有了上文的基礎,理解 Reactor 模型就很容易了,其核心是事件驅動,換句話說,Reactor 是事件驅動模型的一種實現

你可以理解爲 Reactor 模型中的反應器角色,類似於事件轉發器 ---- 承接連接建立、IO 處理以及事件分發,示例圖下:

Reactor 模式由 Reactor 線程、Handlers 處理器兩大角色組成,兩大角色的職責分別如下:

二、大話 Reactor 模型

好了,到了這裏便開始介紹我們的主角:Reactor 模型。

前面我們提到,網絡模型演化過程中,將建立連接、IO 等待 / 讀寫以及事件轉發等操作分階段處理,然後可以對不同階段採用相應的優化策略來提高性能。

也正是如此,Reactor 模型在不同階段都有相關的優化策略,常見的有以下三種方式呈現:

從某些方面來說,其實主要有單線程多線程兩種模型;其中,多線程模型就包含了多線程模型 (Woker 線程池) 和主從多線程模型。多線程 Reactor 的演進分爲兩個方面:

不過,爲了方便表述,還是細分爲三種模型來進行表述。

我們常說的多線程模型一般是指在 Worker 端使用多線程來提升業務上的處理能力。

而主從多線程模型,將 建立連接 和 IO 事件監聽 / 讀寫以及事件分發 兩部分用不同的線程處理,這樣各司其職,能有效利用系統多核資源;同時爲提高事件處理的效率,通常可以使用線程池來處理 IO 事件監聽 / 讀寫以及事件分發這部分操作。

另外,主從多線程模型通常情況下,Worker 端也會採用線程池來處理業務。這樣一看,這三種 Reactor 模型其實是層層遞進,不斷的提升系統的吞吐量。當然,這一系列變換使用都需要結合實際場景考慮,但終究萬變不離其宗。

接下來我們將詳細分析這幾種模型,繼續往下看。

1. 單線程模型

模型圖如下:

上圖描述了 Reactor 的單線程模型結構,在 Reactor 單線程模型中,所有 I/O 操作(包括連接建立、數據讀寫、事件分發等)、業務處理,都是由一個線程完成的。單線程模型邏輯簡單,缺陷也十分明顯:

在單線程 Reactor 模式中,Reactor 和 Handler 都在同一條線程中執行。這樣,帶來了一個問題:當其中某個 Handler 阻塞時,會導致其他所有的 Handler 都得不到執行。

在這種場景下,被阻塞的 Handler 不僅僅負責輸入和輸出處理的傳輸處理器,還包括負責新連接監聽的 Acceptor 處理器,可能導致服務器無響應。這是一個非常嚴重的缺陷,導致單線程反應器模型在生產場景中使用得比較少。

2. 多線程模型

由於單線程模型有性能方面的瓶頸,多線程模型作爲解決方案就應運而生了。

Reactor 多線程模型將業務邏輯交給多個線程進行處理。除此之外,多線程模型其他的操作與單線程模型是類似的,比如連接建立、IO 事件讀寫以及事件分發等都是由一個線程來完成。

當客戶端有數據發送至服務端時,Select 會監聽到可讀事件,數據讀取完畢後提交到業務線程池中併發處理。

一般的請求中,耗時最長的一般是業務處理,所以用一個線程池(worker 線程池)來處理業務操作,在性能上的提升也是非常可觀的。

當然,這種模型也有明顯缺點,連接建立、IO 事件讀取以及事件分發完全有單線程處理;比如當某個連接通過系統調用正在讀取數據,此時相對於其他事件來說,完全是阻塞狀態,新連接無法處理、其他連接的 IO、查詢 IO 讀寫以及事件分發都無法完成。

對於像 Nginx、Netty 這種對高性能、高併發要求極高的網絡框架,這種模式便顯得有些喫力了。因爲,無法及時處理新連接、就緒的 IO 事件以及事件轉發等。

接下來,我們看看主從多線程模型是如何解決這個問題的。

3. 主從多線程模型

主從 Reactor 模型要想解決這個問題,同樣需要從我們前面介紹的幾個階段中的某一個或者多個進行優化處理。

既然是主從模式,那誰主誰從呢?哪個模塊使用主從呢?

在多線程模型中,我們提到,其主要缺陷在於同一時間無法處理大量新連接、IO 就緒事件;因此,將主從模式應用到這一塊,就可以解決這個問題。

主從 Reactor 模式中,分爲了主 Reactor 和 從 Reactor,分別處理 新建立的連接IO讀寫事件/事件分發

簡言之,主從多線程模型由多個 Reactor 線程組成,每個 Reactor 線程都有獨立的 Selector 對象。MainReactor 僅負責處理客戶端連接的 Accept 事件,連接建立成功後將新創建的連接對象註冊至 SubReactor。再由 SubReactor 分配線程池中的 I/O 線程與其連接綁定,它將負責連接生命週期內所有的 I/O 事件。

在海量客戶端併發請求的場景下,主從多線程模式甚至可以適當增加 SubReactor 線程的數量,從而利用多核能力提升系統的吞吐量。

總結

相信到這裏,你已經很清楚 Reactor 模型扮演什麼樣的角色了。其核心是圍繞事件驅動模型

而幾種 Reactor 模型的演進,不過是在這幾個階段中優化升級、層層遞進。

我們再重點回顧多線程模式(多線程模式主從多線程模式),其工作模式大致如下:

Reactor(反應器)模式是高性能網絡編程在設計和架構層面的基礎模式,算是基礎的原理性知識。只有徹底瞭解反應器的原理,才能真正構建好高性能的網絡應用、輕鬆地學習和掌握高併發通信服務器與框架(如 Nginx 服務器)。

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