在高性能的I/O設計中,有兩個著名的模型:Reactor模型和Proactor模型,其中Reactor模型用于同步I/O,而Proactor模型運用于異步I/O操作。
想要了解兩種模型,需要了解一些IO、同步異步的基礎知識,徹底搞懂JAVA的網絡IO
服務端的線程模型
無論是Reactor模型還是Proactor模型,對于支持多連接的服務器,一般可以總結為2種fd和3種事件,如下圖:
2種fd
- listenfd:一般情況,只有一個。用來監聽一個特定的端口(如80)。
- connfd:每個連接都有一個connfd。用來收發數據。
3種事件
- listenfd進行accept阻塞監聽,創建一個connfd
- 用戶態/內核態copy數據。每個connfd對應著2個應用緩沖區:readbuf和writebuf。
- 處理connfd發來的數據。業務邏輯處理,準備response到writebuf。
Reactor模型
無論是C++還是Java編寫的網絡框架,大多數都是基于Reactor模型進行設計和開發,Reactor模型基于事件驅動,特別適合處理海量的I/O事件。
Reactor模型中定義的三種角色:
- Reactor:負責監聽和分配事件,將I/O事件分派給對應的Handler。新的事件包含連接建立就緒、讀就緒、寫就緒等。
- Acceptor:處理客戶端新連接,并分派請求到處理器鏈中。
- Handler:將自身與事件綁定,執行非阻塞讀/寫任務,完成channel的讀入,完成處理業務邏輯后,負責將結果寫出channel。可用資源池來管理。
Reactor處理請求的流程:
讀取操作:
- 應用程序注冊讀就緒事件和相關聯的事件處理器
- 事件分離器等待事件的發生
- 當發生讀就緒事件的時候,事件分離器調用第一步注冊的事件處理器
寫入操作類似于讀取操作,只不過第一步注冊的是寫就緒事件。
1.單Reactor單線程模型
Reactor線程負責多路分離套接字,accept新連接,并分派請求到handler。redis使用單Reactor單進程的模型。
消息處理流程:
- Reactor對象通過select監控連接事件,收到事件后通過dispatch進行轉發。
- 如果是連接建立的事件,則由acceptor接受連接,并創建handler處理后續事件。
- 如果不是建立連接事件,則Reactor會分發調用Handler來響應。
- handler會完成read->業務處理->send的完整業務流程。
單Reactor單線程模型只是在代碼上進行了組件的區分,但是整體操作還是單線程,不能充分利用硬件資源。handler業務處理部分沒有異步。
對于一些小容量應用場景,可以使用單Reactor單線程模型。但是對于高負載、大并發的應用場景卻不合適,主要原因如下:
- 即便Reactor線程的CPU負荷達到100%,也無法滿足海量消息的編碼、解碼、讀取和發送。
- 當Reactor線程負載過重之后,處理速度將變慢,這會導致大量客戶端連接超時,超時之后往往會進行重發,這更加重Reactor線程的負載,最終會導致大量消息積壓和處理超時,成為系統的性能瓶頸。
- 一旦Reactor線程意外中斷或者進入死循環,會導致整個系統通信模塊不可用,不能接收和處理外部消息,造成節點故障。
為了解決這些問題,演進出單Reactor多線程模型。
2.單Reactor多線程模型
該模型在事件處理器(Handler)部分采用了多線程(線程池)。
消息處理流程:
- Reactor對象通過Select監控客戶端請求事件,收到事件后通過dispatch進行分發。
- 如果是建立連接請求事件,則由acceptor通過accept處理連接請求,然后創建一個Handler對象處理連接完成后續的各種事件。
- 如果不是建立連接事件,則Reactor會分發調用連接對應的Handler來響應。
- Handler只負責響應事件,不做具體業務處理,通過Read讀取數據后,會分發給后面的Worker線程池進行業務處理。
- Worker線程池會分配獨立的線程完成真正的業務處理,如何將響應結果發給Handler進行處理。
- Handler收到響應結果后通過send將響應結果返回給Client。
相對于第一種模型來說,在處理業務邏輯,也就是獲取到IO的讀寫事件之后,交由線程池來處理,handler收到響應后通過send將響應結果返回給客戶端。這樣可以降低Reactor的性能開銷,從而更專注的做事件分發工作了,提升整個應用的吞吐。
但是這個模型存在的問題:
- 多線程數據共享和訪問比較復雜。如果子線程完成業務處理后,把結果傳遞給主線程Reactor進行發送,就會涉及共享數據的互斥和保護機制。
- Reactor承擔所有事件的監聽和響應,只在主線程中運行,可能會存在性能問題。例如并發百萬客戶端連接,或者服務端需要對客戶端握手進行安全認證,但是認證本身非常損耗性能。
為了解決性能問題,產生了第三種主從Reactor多線程模型。
3.主從Reactor多線程模型
比起第二種模型,它是將Reactor分成兩部分:
- mainReactor負責監聽server socket,用來處理網絡IO連接建立操作,將建立的socketChannel指定注冊給subReactor。
- subReactor主要做和建立起來的socket做數據交互和事件業務處理操作。通常,subReactor個數上可與CPU個數等同。
Nginx、Swoole、Memcached和Netty都是采用這種實現。
消息處理流程:
- 從主線程池中隨機選擇一個Reactor線程作為acceptor線程,用于綁定監聽端口,接收客戶端連接
- acceptor線程接收客戶端連接請求之后創建新的SocketChannel,將其注冊到主線程池的其它Reactor線程上,由其負責接入認證、IP黑白名單過濾、握手等操作
- 步驟2完成之后,業務層的鏈路正式建立,將SocketChannel從主線程池的Reactor線程的多路復用器上摘除,重新注冊到Sub線程池的線程上,并創建一個Handler用于處理各種連接事件
- 當有新的事件發生時,SubReactor會調用連接對應的Handler進行響應
- Handler通過Read讀取數據后,會分發給后面的Worker線程池進行業務處理
- Worker線程池會分配獨立的線程完成真正的業務處理,如何將響應結果發給Handler進行處理
- Handler收到響應結果后通過Send將響應結果返回給Client
總結
Reactor模型具有如下的優點:
- 響應快,不必為單個同步時間所阻塞,雖然Reactor本身依然是同步的;
- 編程相對簡單,可以最大程度的避免復雜的多線程及同步問題,并且避免了多線程/進程的切換開銷;
- 可擴展性,可以方便地通過增加Reactor實例個數來充分利用CPU資源;
- 可復用性,Reactor模型本身與具體事件處理邏輯無關,具有很高的復用性。
Proactor模型
模塊關系:
- Procator Initiator負責創建Procator和Handler,并將Procator和Handler都通過Asynchronous operation processor注冊到內核。
- Asynchronous operation processor負責處理注冊請求,并完成IO操作。完成IO操作后會通知procator。
- procator根據不同的事件類型回調不同的handler進行業務處理。handler完成業務處理,handler也可以注冊新的handler到內核進程。
消息處理流程:
讀取操作:
- 應用程序初始化一個異步讀取操作,然后注冊相應的事件處理器,此時事件處理器不關注讀取就緒事件,而是關注讀取完成事件,這是區別于Reactor的關鍵。
- 事件分離器等待讀取操作完成事件
- 在事件分離器等待讀取操作完成的時候,操作系統調用內核線程完成讀取操作,并將讀取的內容放入用戶傳遞過來的緩存區中。這也是區別于Reactor的一點,Proactor中,應用程序需要傳遞緩存區。
- 事件分離器捕獲到讀取完成事件后,激活應用程序注冊的事件處理器,事件處理器直接從緩存區讀取數據,而不需要進行實際的讀取操作。
異步IO都是操作系統負責將數據讀寫到應用傳遞進來的緩沖區供應用程序操作。
Proactor中寫入操作和讀取操作,只不過感興趣的事件是寫入完成事件。
Proactor有如下缺點:
- 編程復雜性,由于異步操作流程的事件的初始化和事件完成在時間和空間上都是相互分離的,因此開發異步應用程序更加復雜。應用程序還可能因為反向的流控而變得更加難以Debug;
- 內存使用,緩沖區在讀或寫操作的時間段內必須保持住,可能造成持續的不確定性,并且每個并發操作都要求有獨立的緩存,相比Reactor模型,在Socket已經準備好讀或寫前,是不要求開辟緩存的;
- 操作系統支持,windows下通過IOCP實現了真正的異步 I/O,而在linux系統下,Linux2.6才引入,并且異步I/O使用epoll實現的,所以還不完善。
因此在 Linux 下實現高并發網絡編程都是以Reactor模型為主。
常見架構的進程/線程模型
Netty的線程模型
Netty采用的是主從線程模型。下面是Netty使用中很常見的一段代碼。
public class Server {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NIOServerSocketChannel.class)
.childOption(ChannelOption.TCP_NODELAY, true)
.childAttr(AttributeKey.newInstance("childAttr"), "childAttrValue")
.handler(new ServerHandler())
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
}
});
ChannelFuture f = b.bind(8888).sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
對Netty示例代碼進行分析:
- 定義了兩個EventLoopGroup,其中bossGroup對應的就是主線程池,只接收客戶端的連接(注冊,初始化邏輯),具體的工作由workerGroup這個從線程池來完成。可以理解為老板負責招攬接待,員工負責任務完成。線程池和線程組是一個概念,所以名稱里有group。之后就采用ServerBootstrap啟動類,傳入這兩個主從線程組。
- 客戶端和服務器建立連接后,NIO會在兩者之間建立Channel,所以啟動類調用channel方法就是為了指定建立什么類型的通道。這里指定的是NioServerSocketChannel這個通道類。
- 啟動類還調用了handler()和childHandler()方法,這兩個方法中提及的handler是一個處理類的概念,他負責處理連接后的一個個通道的相應處理。handler()指定的處理類是主線程池中對通道的處理類,childHandler()方法指定的是從線程池中對通道的處理類。
- 執行ServerBootstrap的bind方法進行綁定端口的同時也執行了sync()方法進行同步阻塞調用。
- 關閉通道采用Channel的closeFuture()方法關閉。
- 最終優雅地關閉兩個線程組,執行shutdownGracefully()方法完成關閉線程組。
如果需要在客戶端連接前的請求進行handler處理,則需要配置handler();如果是處理客戶端連接之后的handler,則需要配置在childHandler()。option和childOption也是一樣的道理。
boss線程池作用:
- 接收客戶端的連接,初始化Channel參數。
- 將鏈路狀態變更時間通知給ChannelPipeline。
worker線程池作用:
- 異步讀取通信對端的數據報,發送讀事件到ChannelPipeline。
- 異步發送消息到通信對端,調用ChannelPipeline的消息發送接口。
- 執行系統調用Task。
- 執行定時任務Task。
通過配置boss和worker線程池的線程個數以及是否共享線程池等方式,Netty的線程模型可以在以上三種Reactor模型之間進行切換。
Tomcat的線程模型
Tomcat支持四種接收請求的處理方式:BIO、NIO、APR和AIO
- NIO
- 同步非阻塞,比傳統BIO能更好的支持大并發,tomcat 8.0 后默認采用該模型。
- 使用方法(配置server.xml):<Connector port="8080" protocol="HTTP/1.1"/> 改為 protocol="org.Apache.coyote.http11.Http11NioProtocol"
- BIO
- 阻塞式IO,tomcat7之前默認,采用傳統的java IO進行操作,該模型下每個請求都會創建一個線程,適用于并發量小的場景。
- 使用方法(配置server.xml):protocol =" org.apache.coyote.http11.Http11Protocol"
- APR
- tomcat 以JNI形式調用http服務器的核心動態鏈接庫來處理文件讀取或網絡傳輸操作,需要編譯安裝APR庫。
- 使用方法(配置server.xml):protocol ="org.apache.coyote.http11.Http11AprProtocol"
- AIO
- 異步非阻塞 (NIO2),tomcat8.0后支持。多用于連接數目多且連接比較長(重操作)的架構,比如相冊服務器,充分調用OS參與并發操作,編程比較復雜,JDK7開始支持。
- 使用方法(配置server.xml):protocol ="org.apache.coyote.http11.Http11Nio2Protocol"
Nginx的進程模型
Nginx采用的是多進程(單線程)&多路IO復用模型。
工作模型:
- Nginx在啟動后,會有一個master進程和多個相互獨立的worker進程。
- 接收來自外界的信號,向所有worker進程發送信號,每個進程都有可能來處理這個連接。
- master進程能監控worker進程的運行狀態,當worker進程退出后(異常情況下),會自動啟動新的worker進程。