Netty框架的 主要線程是IO線程。線程模型的好壞直接決定了系統的吞吐量、並發性和安全性。
Netty的線程模型遵循了Reactor的基礎線程模型。以下我們先一起看下該模型
Reactor線程模型
Reactor 單線程模型
單線程模型中全部的IO操作都在一個NIO線程上操作:
包括接受client的請求,讀取client的消息和應答。因為使用的是異步非堵塞的IO,全部的IO操作不會堵塞。理論上一個線程就能夠處理全部的IO操作。
單線程模型適用小容量的應用。
由於在高並發應用 可導致下面問題
-
一個線程同一時候處理成百上千的鏈路,性能上無法支撐。
即使IO線程cpu 100%也無法滿足要求。
-
當NIO線層負載過重,處理速度將變慢,會導致大量的client超時,重發,會更加重NIO的負載。終於導致系統大量超時
-
一旦IO線程跑飛,會導致整個系統通訊模塊不可用,造成節點故障
Reactor多線程模型
該模型組織了 一組線程進行IO的操作
特點:
1. 有專門的NIO線程---acceptor線程用於監聽server,接受client的TCP請求
2. 網絡操作的讀寫 由一個IO線程池負責 負責消息的讀取 接收 編碼和發送
3. 一個IO線程能夠同一時候處理N條鏈路。可是一條鏈路 僅僅相應一個Io線程。防止並發的操作問題
適合絕大多數場景,可是對於並發百萬或者server須要對client握手進行安全認證,認證很耗性能的情況,會導致性能瓶頸。
主次Reactor多線程模型
接受client的連接 不在是一個單獨的IO線程,而是一個Nio線程池:
Acceptor接受client的請求並處理完畢后,將新建的socketChannel注冊到IO線程池的某個線程上,由
他負責IO的讀寫 接編碼工作。
Acceptor線程池只負責client的登錄 握手 和 安全認證,一旦鏈路成
功,將鏈路注冊到后端的線程池的線程上,有他進行興許的Io操作。
Netty線程模型
public void bind(int port) throws Exception {
// 配置服務端的NIO線程組
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChildChannelHandler());
// 綁定port,同步等待成功
ChannelFuture f = b.bind(port).sync()。
// 等待服務端監聽port關閉
f.channel().closeFuture().sync();
} finally {
// 優雅退出,釋放線程池資源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel arg0) throws Exception {
arg0.pipeline().addLast(new TimeServerHandler());
}
}
nettyserver在啟動的時候,創建了兩個NIOEventLoopGroup 獨立的Reator線程池,一個用於接收client的TCP連接,一個用於處理IO的相關的讀寫操作。
Netty線程模型就是在reactor模型的基礎上建立的,線程模型並非一成不變的,通過啟動參數的配置,能夠在三種中切換。
啟動過程,bossGroup 會選擇一個EventLoop 須要綁定serverSocketChannel 進行接收client連接;處理后,將准備好的socketchnanell順利注冊到workGroup下。
netty服務端的創建過程
Netty 屏蔽NIO通信的底層細節:
-
首先創建ServerBootstrap,他是Netty服務端的啟動輔助類
-
設置並綁定Reactor線程池。
Netty的Reactor線程池是EventLoopGroup,它實際就是EventLoop線 程的數組。
EventLoop的職責是處理全部注冊到本線程多路復用器Selector上的Channel
-
設置NIOserverSocketChannel. Netty通過工廠類,利用反射創建NioServerSocketChannel對象
-
設置TCP參數
-
鏈路建立的時候創建並初始化ChannelPipeline.它本質就是一個負責處理網絡事件的職責鏈,負責管理和運行ChannelHandler。
網絡事件以事件流的形式在ChannelPipeline中流轉,由ChannelPipeline依據ChannelHandler的運行策略調度ChannelHandler的運行
- 綁定並啟動監聽port
- 綁定port,並啟動。將會啟動NioEventLoop負責調度和運行Selector輪詢操作,選擇准備就緒的Channel集合。當輪詢到准備就緒的Channel之后,就由Reactor線程NioEventLoop運行ChannelPipeline的對應方法。終於調度並運行ChannelHandler。
NioEventLoop IO線程淺析
做為Netty的Reactor線程,由於要處理網絡IO讀寫,所以聚合一個多路復用器對象,它通過open獲取一個多路復用器。他的操作主要是在run方法的for循環中運行的。
- 做為bossGroup的線程 他須要綁定NioServerSocketChannel 來監聽client的connet請求,並處理連接和校驗。
- 作為workGroup線層組的線程。須要將連接就緒的SocketChannel綁定到線程中。所以一個client連接至相應一個線程,一個線程能夠綁定多個client連接。
從調度層面看。也不存在在EventLoop線程中 再啟動其他類型的線程用於異步運行其他的任務。這樣就避免了多線程並發操作和鎖競爭,提升了I/O線程的處理和調度性能。
NioEventLoop線程保護
IO操作是線程是的核心,一旦出現問題,導致其上面的多路復用器和多個鏈路無法正常工作。因此他須要特別的保護。
他在下面兩個方面做了保護處理:
- 慎重處理異常
異常可能導致線程跑飛。會導致線程下的全部鏈路不可用,這時採try{}catch(Throwable) 捕獲異常,防止跑飛。出現異常后,能夠恢復運行。netty的原則是 某個消息的異常不會導致整個鏈路的不可用,某個鏈路的不可用。不能導致其它鏈路的不可用。
- 規避NIO BUG
Selector.select 沒有任務運行時,可能觸發JDK的epoll BUG。這就是著名的JDK epoll BUG,JDK1.7早期版本號 號稱攻克了。可是據網上反饋,還有此BUG。
server直接表現為 IO線程的 CPU非常高,可能達到100%,可能會導致節點故障!
!!
為什么會發生epoll Bug
Netty的修復策略為:
-
對Selector的select的操作周期進行統計
-
對每完畢一次空的select操作進行一次計數
-
在某周期內(如100ms)連續N此空輪詢, 說明觸發了epoll死循環BUG
-
檢測到死循環后,重建selector的方式讓系統恢復正常
netty採用此策略,完美避免了此BUG的發生。
參考資料:netty權威指南2