高效能網路框架筆記四(IO執行緒模型)
上一文介紹中,我們詳述了網路資料包的接收和傳送過程,並通過介紹5中IO模型瞭解了核心是如何讀取網路資料並通知給使用者執行緒的。
前面的內容都是以核心空間的視角來剖析網路資料的收發模型,本小節我們站在使用者空間的視角來看一下如何對網路資料進行收發。
相對核心來講,使用者空間的IO執行緒模型相對簡單一些。這些使用者空間的IO執行緒模型都是在討論當前多執行緒一起配合工作時誰負責接收連線,誰負責響應IO讀寫,誰負責計算,誰負責傳送和接收,僅僅是使用者IO執行緒的不同分工模式。
1、Reactor
Reactor是利用NIO對IO執行緒進行不同的分工:
- 使用前面我提到的IO多路複用模型,比如select、poll、epoll,kqueue進行IO事件的註冊和監聽。
- 將監聽到就緒的IO事件分發dispatch到各個具體的處理Handler中進行相應的IO事件處理。
通過IO多路複用技術就可以不斷的監聽IO事件,不斷的分發dispatch,就像一個反應堆一樣,看起來像不斷的產生IO事件,因此我們稱這種模式為Reactor模式。
Reactor模型分三類:
1.1 單Reactor單執行緒
Reactor是依賴IO多路複用技術實現監聽IO事件,從而源源不斷的產生IO就緒事件,在Linux系統下我們使用epoll來進行IO多路複用,我們以Linux系統為例:
- 單Reactor意味著只有一個epoll物件,用來監聽所有的事件,比如連線事件,讀寫事件。
- 單執行緒意味著只有一個執行緒來執行epoll_wait獲取IO就緒的socket,然後對這些就緒的socket執行讀寫,以及以後的業務處理也依然是這個執行緒。
單Reactor單執行緒模型就好比我們開了個很小的飯館,作為老闆的我們需要一個人看所有的事情,包括:迎接顧客(accept事件),為顧客介紹選單並等待顧客點菜(IO請求),做菜(業務處理),上菜(IO響應),送客(斷開連線)
1.2單Reactor多執行緒
隨著客人的增多(併發請求),顯然飯館只有一個人(單執行緒)幹活肯定忙不過來,這時招聘一些員工(多執行緒)來幫忙幹上述事情。
於是就有了單Reactor多執行緒模型:
- 這種模式下,也是隻有一個epoll物件來監聽所有的IO事件,一個執行緒來呼叫epoll_wait獲取IO就緒的socket。
- 但是當IO就緒事件產生時,這些IO事件對應處理的業務Handler,我們是通過執行緒池來執行。這樣相比單Reactor單執行緒模型提高了執行效率,充分發揮了多核CPU的優勢。
1.3主從Reactor多執行緒
做任何事情都要區分事件的優先順序,我們應該優先高效的去做優先順序更高的事情,而不是一股腦兒不分優先順序的全部去做。
當我們的小飯館客人越來越多(併發量越來越大),我們就需要擴大飯店的規模,在這個過程中我們發現,迎接客人是飯店最重要的工作,我們要把客人迎進來,不能讓客人一看人多就走掉,只要客人進來了,哪怕菜做的慢一點也沒關係。
- 我們由原來單Reactor變為了多Reactor。主Reactor用來優先專門做優先順序最高的事情,也就是迎接客人(處理連線事件)。
- 建立好連線,簡歷對應的socket後,在acceptor中將要監聽的read事件註冊到從Reactor中,由從Reactor來監聽socket上的讀寫事件。
- 最終將讀寫的業務邏輯交給執行緒池處理。
注意:這裡想從Reactor註冊的只是read事件,並沒有註冊write事件,因為read事件是有epoll核心觸發的,而write事件則是由使用者業務執行緒觸發的(什麼時候傳送資料是由具體業務執行緒決定的),所以write事件理應由使用者業務執行緒去註冊。
使用者執行緒註冊write事件的時機是隻有當使用者傳送的資料無法一次性全部寫入buffer時,才會去註冊write事件,等待buffer重新科協時,繼續寫入剩下的傳送資料,如果用哪個好執行緒一股腦的將傳送資料全部寫入buffer,那麼也就無需註冊write事件到從Reactor中。
主從Reactor多執行緒模型是目前大部分主流網路框架中採用的一種IO執行緒模型。Netty就是用的這種模型。
2、Preactor
Proactor是基於AIO對IO執行緒進行分工的一種模型。前面我們介紹了非同步IO模型,它是作業系統核心支援的一種全非同步程式設計模型,在資料準備階段和資料拷貝階段全程無阻塞。
Proactor執行緒模型將IO事件的監聽,IO操作的執行,IO結果的dispatch統統交給核心來做。
Proactor模型元件介紹:
- completion handler 為使用者程式定義了非同步IO操作回撥函式,在非同步IO操作完成時被核心回撥通知IO結果。
- completion Event Queue 非同步IO操作完成後,會產生對應的IO完成事件,將IO完成事件放到該佇列中。
- Asynchronous Operation Processor負責非同步IO的執行。執行完成後產生IO完成事件放入completion Event Queue佇列中。
- proactor是一個事件迴圈派發器,負責從completion Event Queue中獲取IO完成事件,並回調與IO完成事件關聯的completion handler。
- Initiator初始化非同步操作(asynchronous operation)並通過asynchronous Operation Processor將completion handler和proactor註冊到核心。
Proactor執行過程:
- 使用者執行緒發起aio_read,並告訴核心使用者空間的都讀快取地址,以便核心完成IO操作將結果放入使用者空間的讀緩衝區,使用者執行緒直接可以讀取結果(無任何阻塞)。
- Initiator初始化aio_read非同步讀取操作(asynchronous operation),並將completion handler註冊到核心。
在Proactor中我們關係的IO完成事件:核心已經幫助我們讀好資料並放入我們指定的讀緩衝區,使用者執行緒可以直接讀取。在Reactor中我們中我們關係的是IO就緒事件:資料已經到來,但是需要使用者執行緒自己去核心讀取。
- 此時使用者執行緒就可以做其他的事情了,無需等待IO結果。而核心與此同事開始非同步執行IO操作。當IO操作完成時會產生一個completion event事件,將這個IO完成事件放入completion event queue中。
- Proactor從completion event queue中取出completion event,並回調與IO完成事件關聯的completion handler。
- 在completion handler中完成業務邏輯處理。
3、Reactor與Proactor對比
- Reactor是基於NIO實現的一種IO線性模型,Proactor是基於AIO實現的IO執行緒模型。
- Reactor關心的是IO就緒事件,Proactor關心的是IO完成事件。
- 在Proactor中,使用者程式需要向核心傳遞使用者空間的讀緩衝區地址。Reactor則不需要。這也就導致了Proactor中沒有併發操作都要求有獨立的緩衝區,在記憶體上有一定的開銷。
- Proactor的實現邏輯複雜,編碼成本較Reactor要高的多。
- Proactor在處理高耗時IO時的效能要過於Reactor,但對於低耗時IO的執行效率提升並不明顯。
4、Netty的IO模型
介紹完網路資料包在核心中的收發過程以及五種IO模型和兩種執行緒模型後,執行緒我們來看下Netty中的IO模型是什麼樣的。
在我們介紹Reactor IO執行緒模型的時候提到有三種Reactor模型:單Reactor單執行緒,單Reactor多執行緒,主從Reactor多執行緒。
這三種Reactor模型在netty中都是支援的,但是我們常用的是主從Reactor多執行緒模型。
而我們之前介紹的三種Reactor只是一種模型,是一種設計思想。實際上各種網路框架在實現中並不是嚴格按照模型來實現的,會有一些小的不同,但大體設計思想上是一樣的。
下面我們來看下netty中的主從Reactor多執行緒模型是什麼樣子:
- Reactor在netty中是以group的形式出現的。netty中將Reactor分為兩組,一組是MainReactorGroup也就是我們在編碼中常常看到的EventLoopGroup bossGroup,另一組是SubReactorGroup也就是我們在編碼中常常看到的EventLoopGroup workerGroup。
- MainReactorGroup通常只有一個Reactor,專門負責做重要的事情,也就是監聽連線accept事件。當有連線事件產生時,在對應的處理handler acceptor中建立初始化相應的NioSocketChannel(代表一個Socket連線)。然後以負載均衡的方式在SubReactorGroup中選取一個Reactor,註冊上去,監聽Read事件。
MainReactorGroup中只有一個Reactor的原因是,通常我們的服務端程式只會繫結監聽一個埠,如果繫結監聽多個埠,就會配置多個Reactor。
- SubReactorGroup中有多個Reactor,具體Reactor的個數可以由系統引數 -D io.netty.evnetLoopThreads指定。預設的Reactor的個數為CPU核數*2。SubReactorGroup中的Reactor主要負責監聽讀寫事件,每個Reactor負責監聽一組socket連線。將全部的連線分攤在多個Reactor中。
- 一個Reactor分配一個IO執行緒,這個IO執行緒負責從Reactor中獲取IO就緒事件,執行IO呼叫獲取IO資料,執行PipeLine。
socket連線在建立後就被固定的分配給一個Reactor,所以一個socket連線也只會被一個固定的IO執行緒執行,每個socket連線分配一個獨立的PipeLine例項,用來編排這個socket連線上的IO處理邏輯。這種無鎖序列化的設計目的是為了防止執行緒併發執行同一個socket連線上的IO邏輯處理,防止出現執行緒安全問題。同時使系統吞吐量達到最大化
由於每個Reactor中只有一個IO執行緒,這個IO執行緒叫執行IO活躍的socket連線對應的Pipeline中的ChannelHandler,又要從Reactor中獲取IO就緒事件,執行IO呼叫。所以Pipeline中channelhandler中執行的邏輯不能耗時太長,儘量將耗時的業務邏輯處理放入單獨的業務執行緒池中處理,否則會影響其它連線的IO讀寫,從而近一步影響到整個服務程式的IO吞吐。
當IO請求在業務執行緒中完成相應的業務邏輯處理後,在業務執行緒中利用持有的ChannelHandlerContext引用將響應資料在Pipeline中反向傳播,最終寫回給客戶端。
netty支援的三種Reactor模型:
配置單Reactor單執行緒
EventLoopGroup eventGroup = new NioEventLoopGroup(1); ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(eventGroup);
配置單Reactor多執行緒
EventLoopGroup eventGroup = new NioEventLoopGroup(); ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(eventGroup);
配置主從Reactor多執行緒
EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup, workerGroup);
https://mp.weixin.qq.com/s/Wx8_LeqYPnqSKGQKDhrstg