1 Proactor和Reactor
Proactor和Reactor是兩種經典的多路復用I/O模型,主要用於在高並發、高吞吐量的環境中進行I/O處理。
I/O多路復用機制都依賴於一個事件分發器,事件分離器把接收到的客戶事件分發到不同的事件處理器中,如下圖:
1.1 select,poll,epoll
在操作系統級別select,poll,epoll是3個常用的I/O多路復用機制,簡單了解一下將有助於我們理解Proactor和Reactor。
1.1.1 select
select的原理如下:
用戶程序發起讀操作后,將阻塞查詢讀數據是否可用,直到內核准備好數據后,用戶程序才會真正的讀取數據。
poll與select的原理相似,用戶程序都要阻塞查詢事件是否就緒,但poll沒有最大文件描述符的限制。
1.1.2 epoll
epoll是select和poll的改進,原理圖如下:
epoll使用“事件”的方式通知用戶程序數據就緒,並且使用內存拷貝的方式使用戶程序直接讀取內核准備好的數據,不用再讀取數據
1.2 Proactor
Proactor是一個異步I/O的多路復用模型,原理圖如下:
- 用戶發起IO操作到事件分離器
- 事件分離器通知操作系統進行IO操作
- 操作系統將數據存放到數據緩存區
- 操作系統通知分發器IO完成
- 分離器將事件分發至相應的事件處理器
- 事件處理器直接讀取數據緩存區內的數據進行處理
1.3 Reactor
Reactor是一個同步的I/O多路復用模型,它沒有Proactor模式那么復雜,原理圖如下:
- 用戶發起IO操作到事件分離器
- 事件分離器調用相應的處理器處理事件
- 事件處理完成,事件分離器獲得控制權,繼續相應處理
1.4 Proactor和Reactor的比較
- Reactor模型簡單,Proactor復雜
- Reactor是同步處理方式,Proactor是異步處理方式
- Proactor的IO事件依賴操作系統,操作系統須支持異步IO
- 同步與異步是相對於服務端與IO事件來說的,Proactor通過操作系統異步來完成IO操作,當IO完成后通知事件分離器,而Reactor需要自己完成IO操作
2 Reactor多線程模型
前面已經簡單介紹了Proactor和Reactor模型,在實際中Proactor由於需要操作系統的支持,實現的案例不多,有興趣的可以看一下Boost Asio的實現,我們主要說一下Reactor模型,Netty也是使用Reactor實現的。
但單線程的Reactor模型每一個用戶事件都在一個線程中執行:
- 性能有極限,不能處理成百上千的事件
- 當負荷達到一定程度時,性能將會下降
- 單某一個事件處理器發送故障,不能繼續處理其他事件
2.1 多線程Reactor
使用線程池的技術來處理I/O操作,原理圖如下:
- Acceptor專門用來監聽接收客戶端的請求
- I/O讀寫操作由線程池進行負責
- 每個線程可以同時處理幾個鏈路請求,但一個鏈路請求只能在一個線程中進行處理
2.2 主從多線程Reactor
在多線程Reactor中只有一個Acceptor,如果出現登錄、認證等耗性能的操作,這時就會有單點性能問題,因此產生了主從Reactor多線程模型,原理如下:
- Acceptor不再是一個單獨的NIO線程,而是一個獨立的NIO線程池
- Acceptor處理完后,將事件注冊到IO線程池的某個線程上
- IO線程繼續完成后續的IO操作
- Acceptor僅僅完成登錄、握手和安全認證等操作,IO操作和業務處理依然在后面的從線程中完成
3 Netty中Reactor模型的實現
Netty同時支持Reactor的單線程、多線程和主從多線程模型,在不同的應用中通過啟動參數的配置來啟動不同的線程模型。
通過線程池的線程個數、是否共享線程池方式來切換不同的模型
3.1 Netty中的Reactor模型
Netty中的Reactor模型如下圖:
- Acceptor中的NioEventLoop用於接收TCP連接,初始化參數
- I/O線程池中的NioEventLoop異步讀取通信對端的數據報,發送讀事件到channel
- 異步發送消息到對端,調用channel的消息發送接口
- 執行系統調用Task
- 執行定時Task
3.2 NioEventLoop
NioEventLoop是Netty的Reactor線程,它在Netty Reactor線程模型中的職責如下:
1. 作為服務端Acceptor線程,負責處理客戶端的請求接入
2. 作為客戶端Connecor線程,負責注冊監聽連接操作位,用於判斷異步連接結果
3. 作為IO線程,監聽網絡讀操作位,負責從SocketChannel中讀取報文
4. 作為IO線程,負責向SocketChannel寫入報文發送給對方,如果發生寫半包,會自動注冊監聽寫事件,用於后續繼續發送半包數據,直到數據全部發送完成
如下圖,是一個NioEventLoop的處理鏈:
- 處理鏈中的處理方法是串行化執行的
- 一個客戶端連接只注冊到一個NioEventLoop上,避免了多個IO線程並發操作
3.2.1 Task
Netty Reactor線程模型中有兩種Task:系統Task和定時Task
- 系統Task:創建它們的主要原因是,當IO線程和用戶線程都在操作同一個資源時,為了防止並發操作時鎖的競爭問題,將用戶線程封裝為一個Task,在IO線程負責執行,實現局部無鎖化
- 定時Task:主要用於監控和檢查等定時動作
基於以上原因,NioEventLoop不是一個純粹的IO線程,它還會負責用戶線程的調度
3.2.2 IO線程的分配細節
線程池對IO線程進行資源管理,是通過EventLoopGroup實現的。線程池平均分配channel到所有的線程(循環方式實現,不是100%准確),一個線程在同一時間只會處理一個通道的IO操作,這種方式可以確保我們不需要關心同步問題。
3.2.3 Selector
NioEventLoop是Reactor的核心線程,那么它就就必須實現多路復用。
Selector的過程如下:
- 首先oldWakenUp = wakenUp.getAndSet(false)
- 如果隊列中有任務, selectNow()
- 如果沒有select(),直達channel准備就緒,但此過程中循環次數超過限值也將rebuidSelectoror退出循環
- 執行processSelectedKeys和runAllTasks
epoll-bug的處理
在netty中對java nio的epoll bug進行了處理,就是設置一個閥值,如果超過了就rebuidSelector來避免epoll()死循環
3.2.4 NioEevntLoopGroup
EventExecutorGroup:提供管理EevntLoop的能力,他通過next()來為任務分配執行線程,同時也提供了shutdownGracefully這一優雅下線的接口
EventLoopGroup繼承了EventExecutorGroup接口,並新添了3個方法
- EventLoop next()
- ChannelFuture register(Channel channel)
- ChannelFuture register(Channel channel, ChannelPromise promise)
EventLoopGroup的實現中使用next().register(channel)來完成channel的注冊,即將channel注冊時就綁定了一個EventLoop,然后EvetLoop將channel注冊到EventLoop的Selector上。
NioEventLoopGroup還有幾點需要注意:
- NioEventLoopGroup下默認的NioEventLoop個數為cpu核數 * 2,因為有很多的io處理
- NioEventLoop和java的single線程池在5里差異變大了,它本身不負責線程的創建銷毀,而是由外部傳入的線程池管理
- channel和EventLoop是綁定的,即一旦連接被分配到EventLoop,其相關的I/O、編解碼、超時處理都在同一個EventLoop中,這樣可以確保這些操作都是線程安全的