高性能IO設計之Reactor模型

首先,在講述高性能IO編程設計的時候,我們先思考一下何爲“高性能”呢,如果自己來設計一個web體系服務,選擇BIO還是NIO的編程方式呢?其次,我們可以瞭解下構建一個web體系服務中,爲了能夠支撐更多的併發連接數,一般會有兩種web架構設計方案,即線程架構以及事件驅動設計,在Java的IO設計演進文章已對線程架構設計方案進行詳細的闡述,本文主要以事件驅動設計具體實現技術展開討論.

web體系設計

何爲高性能

在epoll技術原理分析文章中講到C10K的優化問題,需要從文件描述符限制,線程資源,內存資源,網絡數據包大小傳輸等方面進行優化,目的是提升web服務的連接調度處理能力,支撐更多的客戶端併發連接響應,因此高性能的IO設計意味着(實現IO高性能的目標)需要考慮以下幾個方面,即:

  • 可以實現併發連接的響應調度,那麼web服務可能需要藉助多線程技術
  • 在上述基礎上可以支撐更多的連接處理,那麼web服務能夠實現可伸縮
  • 充分利用計算機資源並減少資源的空閒浪費,那麼web服務需要儘可能地合理利用CPU/帶寬/內存等資源
BIO與NIO性能區分

爲了達到web服務的高性能設計目標,我們需要考慮技術落地方案的選擇,現有方案有基於’one thread one connection’的BIO以及’one thread one server’的NIO技術實現方式,其次,在這裏需要聲明一點就是BIO視爲單線程的同步操作,NIO視爲單線程的異步操作,同時我們也需要關注兩種不同IO的實現在性能測試中的結果是如何的,纔能有效地幫助我們實現高性能的目標,以下是摘錄《 Thousands of Threads and Blocking I/O》的性能測試結果數據,現分析如下:

異步web與同步web的吞吐量

在這裏插入圖片描述

通過上述可知,在相同的操作系統環境下,同步web的IO吞吐量更高,主要包含以下方面:

  • 同步Web的IO模型吞吐量性能要比NIO高出25%-35%,即使使用多個selector的NIO實現方式也無法比基於Linux的NPLT實現同步操作的性能更快
  • 其次,linux內核使用epoll的技術主要是解決poll本身性能以及可伸縮性問題,epoll在技術實現也將通過創建少量線程的方式來提升性能,增加吞吐量的處理能力

編程方式

  • NIO編程僞代碼
while(true){
    // 調用select()
    int rs = select();
    
    //  如果rs沒有對應的就緒事件個數,繼續select()
    if (rs <= 0){
        continue;
    }
    
    // 獲取可用的key
    Set<Keys> keySet = selectKeys();
    for(Key key: keySet){
        if(key.isAcceptable()){
            client = accept();
            // register and save the key
            client.register(...);
        }else if(key.isReadable()){
            // read();
            // decode();
            // process();
            // encode();
            // write();
        }
    }
}
  • BIO編程僞代碼
while(true){
    client = accept();
    client.read();
    // decode();
    // process();
    // encode();
    // write();
}

通過上述可以看出,BIO是面向單連接處理的編程方式,調用accept以及read方法都需要進行等待就緒狀態才能進行下一步操作,而NIO則是面向單線程處理多連接的編程方式(嚴格意義上是基於事件編程),通過輪詢以及就緒事件的遍歷來處理就緒事件,相比BIO在實現上會更爲複雜些,然而對於實現高性能的IO設計,我們還需要藉助多線程技術來實現,下面針對多線程的同步與異步方式進行對比與分析

多線程環境下同步與異步性能對比

linux在內核2.6版本之後使用NPTL的規範實現線程技術,空閒的線程成本接近爲0,同時線程上下文能夠實現更快切換以及儘可能地運行更多線程,如下圖所示:

在這裏插入圖片描述

通過上述可知,多線程環境下使用同一個類庫進行測試的性能,1000個與1個線程執行的性能效率上相差不大,因此線程上下文切換的成本其實不高
然而對於多線程環境的同步操作如下圖:

在這裏插入圖片描述

通過上述可知,syncHashMap與HashTable隨着增加的線程數,其執行的性能耗時更高,因爲同步操作的hashtable和syncHashMap是在線程級別加鎖實現順序的寫操作,因此需要等待其他線程執行完成才能被喚醒執行,對於具備“異步”特性的類庫則是通過多線程併發方式對容器實現寫操作,即同一個時刻可以有多個線程對容器實現寫操作.
多核環境下的同步與異步性能對比

單核環境

在這裏插入圖片描述

多核環境

在這裏插入圖片描述
通過上述可知,具備‘異步‘的併發類庫不論是在單核還是多核環境下性能基本差不多,但是對於實現同步hashtable的性能在多核環境充分利用cpu核數提升性能,但是在上述我們注意到SyncHashMap執行的性能會更差,爲什麼?個人理解上述的map類庫都是放在相同環境併發執行,而併發環境必然存在資源的競爭,因此對於在激烈的併發競爭環境中,同步操作的成本會更高.

BIO與NIO分析小結

  • BIO在吞吐量性能上比NIO的方式更好
  • BIO編程相比NIO更爲簡單
  • 對於同步與異步操作,無競爭的同步操作性能更好,而存在競爭的同步操作會降低執行的性能,此時進行同步操作成本更高
  • 線程上下文切換的成本其實並不是特別高,但是在多線程的同步環境下性能損耗的成本更高
  • 另外可以看到併發類庫具備更好伸縮性,比如concurrenthashmap與hashtable執行內存IO的寫操作,後者需要通過加鎖實現線程同步,而前者同樣是加鎖,但卻是分片加鎖,使得線程可異步化執行,即同一個對象可以讓不同線程進行寫操作,這個時候性能上的提升並不依賴於線程資源.
  • 同步操作能夠充分利用多核cpu資源來提升性能

簡而言之,高性能IO設計可以運用分散的思想並藉助併發多線程技術以及充分利用計算機資源技術手段來達到目標,同時爲了保證web服務可伸縮性,可以考慮引入中間層的思想來解決現有無法擴展的問題,接下來,我們開始進入web服務設計,爲了能夠支撐更多的併發連接數,一般會有兩種web體系架構設計模式,一種是基於線程的架構,另一種是基於事件驅動架構設計.現針對上述兩種架構展開分析.

基於線程連接架構(TBA)

線程連接架構是基於"每個連接對應每個線程"的設計思想,這樣設計主要有以下幾個方面考慮:

  • 它適用於那些爲了與非線程安全的庫兼容而需要避免線程化的站點,比如每個線程連接可以使用hashmap來處理當前線程的業務數據等操作,避免產生線程安全問題
  • 使用多模塊處理機制隔離每個請求,保證每個請求request之間是相互獨立不干擾的

線程與連接1:1模式

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

  • 上述每一個連接請求都需要創建相應的線程資源來處理對應的每個連接任務
  • 如果需要支撐的連接成千上萬,將會導致創建的線程資源個數達到瓶頸,無法滿足每連接每線程的目標
  • 創建與銷燬線程產生的開銷也將會影響性能,執行期間有可能會導致其他線程處於idle狀態,浪費資源空間

線程與連接N:M模式

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

  • 對於線程池技術,如果創建的線程無法來得及處理連接請求那麼此時將會把還未處理的連接添加到阻塞隊列中,如果是有界隊列,那麼超出的連接怎麼處理,如果是無界隊列,那麼連接堆積,內存資源以及cpu資源都會成爲瓶頸,這些都無法滿足我們對於一個高性能web服務的要求,即阻塞隊列要應該設置多大才合適?
  • 如果線程池所有的線程處理的連接都保持"keep alive"卻沒有任何其他業務操作,這個時候也會造成線程空閒,也會導致阻塞隊列上的連接一直沒有被執行而處於等待狀態,出現"假死"狀態,即線程池調整的線程數量應當設置多大才能保證被充分利用?

基於上述線程架構的問題,引入事件驅動設計方案,接下來我們來看下事件驅動設計是什麼,如何解決上述的問題.

事件驅動架構(EDA)

在講述事件驅動設計之前,可以先通過一個簡單的示例展開.當我們在前端頁面觸發點擊事件的時候,就會調用對應的一個觸發函數來響應對應的點擊事件,也就是說開發人員需要通過以下方式來完成一個點擊事件的註冊與綁定操作:

// 獲取button組件
var btn = document.getElementById("login");
// 綁定點擊事件
login.click(function(){
   // login process
});

對此,我們先梳理下什麼是事件,事件系統有哪些組成,最後再根據上述的點擊事件將整個事件處理流程以時序圖的方式展開.

事件定義與結構組成

  • 什麼是事件:在網絡編程中,一個事件可以被定義爲網絡socket有新的連接,有數據可讀,有數據可寫等狀態的變更,即socket從等待到就緒狀態的變化過程,一個事件結構包含事件header以及事件body
  • 事件header: 主要包含事件信息,比如事件名稱,發生事件的時間戳以及事件類型
  • 事件body: 提供檢測到狀態變更的詳細信息
  • 事件通知: 事件的變更可以讓體系內的其他應用程序知道事件的狀態變化,事件通知在事件產生,發佈事件,傳輸事件,檢測事件以及事件處理等流程進行傳遞,事件通知一般是以異步消息方式進行傳遞

事件流層

  • 事件發射器(event emitters): 負責檢測,收集以及傳輸事件
  • 事件消費者/接收者(event consumer): 負責對產生的事件作出響應(對產生的事件進行處理並響應)或者反應(只負責對產生的事件進行過濾或者驗證並傳遞事件到下一個活動接收者進行處理並響應)
  • 事件通道(event channel): 負責事件傳輸的組件(事件發送器傳輸到接收者的管道),可以是TCP/IP連接通道,也可以是通過消息中間件進行傳輸,還可以是郵件或者是輸入文件等形式

至此,我們對事件的定義有了基本認知之後,那麼對於上述的一個完整的點擊事件流程是如何進行運作的呢,現如下圖所示:

在這裏插入圖片描述

對於EDA的NIO而言,相比上述事件設計是運用相同的思路,但是具體實現的技術方案略有不同,EDA的NIO技術實現是基於Reactor模式,現展開NIO編程的Reactor模式進行分析.

高性能之Reactor設計

Reactor模式

在一個通用的web服務中,一般具備以下的幾方面的特徵:

  • web服務實現可擴展,需要藉助分散設計的思想來實現
  • 大部分web服務具備的通用邏輯有: 讀取請求,對請求數據進行拆包,處理請求業務邏輯,結果返回的數據進行粘包,最後將數據發送到客戶端.
  • 對於web服務而言,不同協議在處理拆包-業務處理-粘包過程的實現方式以及成本都會有所不同

一般地,對於經典的TBA架構的web服務如下圖:
在這裏插入圖片描述

在上述圖中看到每個線程處理每個handler,且不討論先前TBA存在的問題,就可擴展性而言就存在侷限性,尤其是針對部分線程執行decode-compute-encode過程中出現耗時緩慢情況時,很難對其進行優化操作,甚至無法通過服務進行配置調優,沒有達到高性能的可伸縮性要求.

可伸縮web服務目標
  • 一旦負載過多的時候,能夠實現對客戶端的降級操作
  • 可以通過增加資源來改進或者完善現有的web服務性能,比如cpu/內存/網絡帶寬/磁盤IO讀寫能力等
  • 還要滿足低延遲,支撐高峯要求以及服務可用性
  • 可伸縮實現的手段一般採用分而治之的設計思想來解決
IO事件驅動架構

對於一個高性能的IO事件驅動設計,主要包含有以下三個內容:

  • 基於上述的事件驅動架構(EDA)原理
  • 藉助NIO中非阻塞的API
  • 分而治之的設計思想實現web可伸縮性

對於IO事件驅動架構實現的技術主要是使用Reactor模式,現開始進入Reactor模式的分析

Reactor定義

反應器設計模式是一個事件處理模式,用於處理一個或多個輸入併發地傳遞給服務處理程序的服務請求。然後,服務處理程序對傳入請求進行多路複用,並將它們同步分派給相關的請求處理程序.

  • 反應器模式是事件驅動架構的一種實現技術.簡而言之,它使用單線程事件循環對資源發出的事件進行阻塞,並將其分配給相應的處理程序和回調.
  • 只要註冊了事件的處理程序和回調來處理它們,就不需要阻塞IO.事件是指實例,例如新的傳入連接,可以讀取,可以寫入等操作.這些處理程序或者回調函數可以在多核環境中利用線程池方式實現
  • 這種模式將模塊化應用程序級代碼與可重複使用的反應堆實現解耦

Reactor組成結構

  • 請求資源:可以爲系統提供輸入的資源,可以是讀取外部文件,接收的網絡數據報,其他或當前系統輸出資源都可以作爲系統輸入的資源,在網絡編程中請求資源爲發起網絡請求的socket
  • 同步事件多路複用器:所有的請求資源都阻塞於事件輪詢,通過事件輪詢檢測請求資源是否處於就緒狀態,一旦處於就緒狀態,多路複用器就會啓動資源同步操作,將就緒資源發送到調度程序中處理請求
  • 請求轉發器:負責接收多路複用器的就緒資源,並根據請求的資源進行註冊或註銷對應的請求處理器,交由對應的處理器負責處理請求
  • 請求處理器:在應用程序中定義對應請求資源的請求處理器來完成相應的業務請求並給予請求響應

Reactor設計示意圖如下
在這裏插入圖片描述
通過上述示意圖可知,Reactor模式在應用程序級別代碼交由handler進行處理,而對於整個網絡的複用操作交由多路複用器進行處理,實現反應堆的複用與應用程序業務邏輯的解耦,同時可以針對handler處理器進行調優處理以達到handler能夠更快速地響應真正的IO事件並返回給客戶端程序響應結果.

Reactor核心原理

Reactor的事件輪詢

通過上述可知,在事件輪詢中包含以下三個步驟:

  • 查找所有處於活動狀態且未鎖定的處理程序,或將其委託給dispatcher實現
  • 依次執行這些處理程序直到完成或者到達它們被阻塞的點.完成的處理程序將會被停用並允許事件循環繼續.
  • 重複執行第一個步驟

兩個核心參與者

  • Reactor反應器:也可稱爲多路複用器,即在單獨的線程中運行,它是通過將工作分派給適當的處理程序來響應IO事件.
  • Handler處理器:處理程序執行與I/O事件有關的實際工作,反應堆通過分派適當的處理程序來響應I/O事件,即處理程序執行非阻塞操作.

Reactor處理流程

  • Java的Reactor反應器通過調用select()不斷監聽socket事件的變化,通過NIO的SelectionKey保存當前socket事件變化狀態.
  • 當創建服務端socket的時候會將服務端socket進行註冊與端口綁定操作,實現端口的監聽事件
  • 當客戶端與服務端建立連接的時候,服務端socket端口監聽到事件變化,此時將客戶端的socket註冊並保存到SelectionKey中,即Acceptor操作
  • 當客戶端發起請求操作時,服務端保存的客戶端socket監聽到可讀事件,將會在Reactor中添加對應的事件響應處理器Handler並由內部的轉發器分發到對應的Handler進行處理
  • Reactor相當於事件的發起器,SelectionKey相當於事件通道,用於保存和投遞消息通知,Handler相當於事件消費者,也有稱爲事件處理引擎.

下游事件反應器爲可選,主要用於處理返回的結果呈現,可以理解爲前端結果展示的組件.

Reactor技術演進

在文章開頭部分講述到實現高性能的目標,通過對比NIO與BIO的編程設計分析,我們基本上都會基於NIO模式來設計一個高性能的web服務,而一般地,對於NIO服務設計具備高性能的目標,需要藉助以下的技術手輔助段來達到目標.

實現高性能手段

  • 線程池技術:需要關注線程池核數,線程池最大線程數,超時時間,阻塞隊列存儲的策略,連接負載過多處理策略
  • NIO提供非阻塞技術:即保證accept以及read操作爲非阻塞
  • NIO提供的內存優化技術:以字節byte爲單位使用byteBuffer緩存或發送數據
  • 可以使用併發庫技術:在上述中對比異步與同步的性能分析,可以使用併發庫來實現多線程環境下的異步操作

一個單線程NIO服務通用設計

  • 處理select的輪詢調用
  • 讀取request數據
  • 寫出response數據
  • 後臺業務核心數據處理邏輯,即DB數據的讀寫/網絡數據的讀寫/磁盤數據的讀寫/內存數據的讀寫

在上述過程,業務核心數據邏輯具備多樣性,需要針對不同的場景來進行分析,因此影響性能的處理步驟往往在於最後一步,由此可通過Reactor與多線程技術來進一步提升web服務的處理性能

Reactor技術演進

接下來以圖解的方式來查看Reactor與多線程技術的演進過程.以下圖解均來自《Scale IO in Java》以及github上的gnet庫來演示.

  • 單Reactor + 單線程模式

在這裏插入圖片描述

  • 單Reactor + 多線程模式
    在這裏插入圖片描述
  • 多Reactor + 多線程模式

在這裏插入圖片描述

  • gnet庫實現的一種Reactors模式
    在這裏插入圖片描述
    最後,我們單從Reactor技術變化來看,其設計的目的無非包含以下幾個方面:
  • 反應器體系結構模式允許事件驅動的應用程序對來自一個或多個客戶機的服務請求進行多路複用和分派,即支持更多的客戶端連接請求調度.
  • 反應器模式是一種用於同步解複用和事件到達時的順序的設計模式,通過輪詢不斷尋找就緒事件,並在事件觸發時通知相應的事件處理程序來處理它,引入新的對象組件Reactor與Handler,實現程序業務邏輯與socket的IO複用事件處理邏輯解耦.
  • 它接收來自多個併發客戶機的消息、請求和連接,並使用事件處理程序順序處理這些帖子.反應器設計模式的目的是避免爲每個消息、請求和連接創建線程的常見問題
  • 它從一組處理程序接收事件,並將它們按順序分發到相應的事件處理程序,同時可以看到採用分而治之的設計思路來實現可web服務的伸縮性.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章