此前,我們學習了 Java NIO API 的使用,也學習了幾種常見的 IO 模型 以及傳統阻塞 I/O 服務模型和 Reactor 線程模型 。你體會到直接去使用 Java NIO API 去進行網絡編程會非常麻煩,除了要對 Java NIO API 掌握的非常熟練之外,還需要掌握多線程等其他技術。不過這些問題,Netty 都可以幫我們解決。
Netty 是一個 NIO 客戶端服務器框架,可以快速輕鬆地開發協議服務器和客戶端等網絡應用程序。 它極大地簡化了 TCP 和 UDP 套接字服務器等網絡編程的複雜度。
『快速而又簡單』並不意味着最終的應用程序會受到可維護性或性能問題的影響。 Netty 經過精心設計,具有豐富的協議,如 FTP,SMTP,HTTP 以及各種二進制和基於文本的傳統協議。 因此,Netty 成功地找到了一種在不妥協的情況下實現易於開發,性能,穩定性和靈活性的方法。
服務端 IO 編程
傳統的 BIO 編程
網絡編程的基本模型是 Client/Server 模型,也就是兩個進程之間進行相互通信,其中服務端提供位置信息(綁定的 IP 地址和監聽端口),客戶端通過連接操作向服務端監聽的地址發起連接請求,通過三次握手建立連接,如果連接建立成功,雙方就可以通過網絡套接字(Socket)進行通信。
在基於傳統同步阻塞模型開發中,ServerSocket 負責綁定 IP 地址,啓動監聽端口;Socket 負責發起連接操作。連接成功之後,雙方通過輸入和輸出流進行同步阻塞式通信。
首先,我們通過如圖所示的通信模型圖來熟悉下 BIO 的服務端通信模型:採用 BIO 通信模型的服務端,通常由一個獨立的 Acceptor 線程負責監聽客戶端的連接,它接收到客戶端連接請求之後爲每個客戶端創建一個新的線程進行鏈路處理,處理完成之後,通過輸出流返回應答給客戶端,線程銷燬。這就是典型的一請求一應答通信模型”
該模型最大的問題就是缺乏彈性伸縮能力,當客戶端併發訪問量增加後,服務端的線程個數和客戶端併發訪問數呈 1:1 的正比關係,由於線程是 Java 虛擬機非常寶貴的系統資源,當線程數膨脹之後,系統的性能將急劇下降,隨着併發訪問量的繼續增大,系統會發生線程堆棧溢出、創建新線程失敗等問題,並最終導致進程宕機或者僵死,不能對外提供服務。
僞異步 I/O 編程
爲了解決同步阻塞 I/O 面臨的一個鏈路需要一個線程處理的問題,後來有人對它的線程模型進行了優化,後端通過一個線程池來處理多個客戶端的請求接入,形成客戶端個數 M:線程池最大線程數 N 的比例關係,其中 M 可以遠遠大於 N,通過線程池可以靈活的調配線程資源,設置線程的最大值,防止由於海量併發接入導致線程耗盡。
採用線程池和任務隊列可以實現一種叫做僞異步的 I/O 通信框架,它的模型圖如圖 1-2 所示。
當有新的客戶端接入的時候,將客戶端的 Socket 封裝成一個 Task(該任務實現 java.lang.Runnable 接口)投遞到後端的線程池中進行處理,JDK 的線程池維護一個消息隊列和 N 個活躍線程對消息隊列中的任務進行處理。由於線程池可以設置消息隊列的大小和最大線程數,因此,它的資源佔用是可控的,無論多少個客戶端併發訪問,都不會導致資源的耗盡和宕機。
僞異步 I/O 實際上僅僅只是對之前 I/O 線程模型的一個簡單優化,它無法從根本上解決同步 I/O 導致的通信線程阻塞問題。下面我們就簡單分析下如果通信對方返回應答時間過長,會引起的級聯故障。
- 服務端處理緩慢,返回應答消息耗費 60s,平時只需要 10ms。
- 採用僞異步 I/O 的線程正在讀取故障服務節點的響應,由於讀取輸入流是阻塞的,因此,它將會被同步阻塞 60s。
- 假如所有的可用線程都被故障服務器阻塞,那後續所有的 I/O 消息都將在隊列中排隊。
- 由於線程池採用阻塞隊列實現,當隊列積滿之後,後續入隊列的操作將被阻塞。
- 由於前端只有一個 Accptor 線程接收客戶端接入,它被阻塞在線程池的同步阻塞隊列之後,新的客戶端請求消息將被拒絕,客戶端會發生大量的連接超時。
- 由於幾乎所有的連接都超時,調用者會認爲系統已經崩潰,無法接收新的請求消息。
NIO 編程
與 Socket 類和 ServerSocket 類相對應,NIO 也提供了 SocketChannel 和 ServerSocketChannel 兩種不同的套接字通道實現。這兩種新增的通道都支持阻塞和非阻塞兩種模式。阻塞模式使用非常簡單,但是性能和可靠性都不好,非阻塞模式則正好相反。開發人員一般可以根據自己的需要來選擇合適的模式,一般來說,低負載、低併發的應用程序可以選擇同步阻塞 I/O 以降低編程複雜度,但是對於高負載、高併發的網絡應用,需要使用 NIO 的非阻塞模式進行開發。
詳見 Java NIO API
AIO 編程
NIO2.0 引入了新的異步通道的概念,並提供了異步文件通道和異步套接字通道的實現。異步通道提供兩種方式獲取獲取操作結果:
- 通過 java.util.concurrent.Future 類來表示異步操作的結果;
- 在執行異步操作的時候傳入一個 java.nio.channels;
- CompletionHandler 接口的實現類作爲操作完成的回調。
NIO2.0 的異步套接字通道是真正的異步非阻塞 I/O,它對應 UNIX 網絡編程中的事件驅動 I/O(AIO),它不需要通過多路複用器(Selector)對註冊的通道進行輪詢操作即可實現異步讀寫,從而簡化了 NIO 的編程模型。
幾種 IO 模型對比
BIO | 僞異步 IO | NIO | AIO | |
---|---|---|---|---|
客戶端個數:IO 線程數 | 1:1 | M:N (M > N) | M:1(1 個 IO 線程處理多個客戶端連接) | M:0(不需要啓動額外的 I/O 線程,被動回調) |
I/O 類型(同步) | 同步 IO | 同步 IO | 同步 IO | 異步 IO |
I/O 類型 (阻塞) | 阻塞 IO | 阻塞 IO | 非阻塞 IO | 非阻塞 IO |
調試難度 | 簡單 | 簡單 | 負責 | 複雜 |
可靠性 | 非常差 | 非常差 | 高 | 高 |
吞吐量 | 低 | 中 | 高 | 高 |
API 使用難度 | 簡單 | 簡單 | 非常難 | 複雜 |
爲什麼要用 Netty
爲什麼不建議直接使用 JDK 原生 NIO 框架去進行開發?
- NIO 的類庫和 API 繁雜,使用麻煩,你需要熟練掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。
- 需要具備其他的額外技能做鋪墊,例如熟悉 Java 多線程編程。這是因爲 NIO 編程涉及到 Reactor 模式,你必須對多線程和網路編程非常熟悉,才能編寫出高質量的 NIO 程序。
- 可靠性能力補齊,工作量和難度都非常大。例如客戶端面臨斷連重連、網絡閃斷、半包讀寫、失敗緩存、網絡擁塞和異常碼流的處理等問題,NIO 編程的特點是功能開發相對容易,但是可靠性能力補齊的工作量和難度都非常大。
- JDK NIO 的 BUG,例如臭名昭著的 epoll bug,它會導致 Selector 空輪詢,最終導致 CPU 100%。官方聲稱在 JDK1.6 版本的 update18 修復了該問題,但是直到 JDK1.7 版本該問題仍舊存在,只不過該 BUG 發生概率降低了一些而已,它並沒有被根本解決。該 BUG 以及與該 BUG 相關的問題單可以參見以下鏈接內容。
選擇 Netty 的理由
Netty 是業界最流行的 NIO 框架之一,它的健壯性、功能、性能、可定製性和可擴展性在同類框架中都是首屈一指的,它已經得到成百上千的商用項目驗證,例如:Dubbo、RocketMQ、Spark、Spring5、Elasticsearch 等,他具有如下優點:
- 異步事件通知框架,可開發出高性能的服務端和客戶端;
- 封裝了 JDK 底層 BIO、NIO 模型,提高簡單易用的 API,開發門檻低;
- 成熟、穩定,Netty 修復了已經發現的所有 JDK NIO BUG,業務開發人員不需要再爲 NIO 的 BUG 而煩惱;
- 功能強大,預置了多種編解碼功能,解決了拆包粘包問題,支持多種主流協議;
- 定製能力強,可以通過 ChannelHandler 對通信框架進行靈活地擴展;
- 性能高,通過與其他業界主流的 NIO 框架對比,Netty 的綜合性能最優;
- 社區活躍,版本迭代週期短,發現的 BUG 可以被及時修復,同時,更多的新功能會加入;
- 經歷了大規模的商業應用考驗,質量得到驗證。在互聯網、大數據、網絡遊戲、企業應用、電信軟件等衆多行業得到成功商用,證明了它已經完全能夠滿足不同行業的商業應用了。
Netty 架構
功能架構圖
邏輯架構圖
- Reactor 通信調度層:它由一系列輔助類完成,包括 Reactor 線程 NioEventLoop 及其父類,NioSocketChannel / NioServerSocketChannel 及其父類,ByteBuffer 以及由其衍生出來的各種 Buffer,Unsafe 以及其衍生出的各種內部類等。該層的主要職責就是監聽網絡的讀寫和連接操作,負責將網絡層的數據讀取到內存緩衝區中,然後觸發各種網絡事件,例如連接創建、連接激活、讀事件、寫事件等,將這些事件觸發到 PipeLine 中,由 PipeLine 管理的職責鏈來進行後續的處理。
- 職責鏈 ChannelPipeline:它負責事件在職責鏈中的有序傳播,同時負責動態地編排職責鏈。職責鏈可以選擇監聽和處理自己關心的事件,它可以攔截處理和向後 / 向前傳播事件。不同應用的 Handler 節點的功能也不同,通常情況下,往往會開發編解碼 Hanlder 用於消息的編解碼,它可以將外部的協議消息轉換成內部的 POJO 對象,這樣上層業務則只需要關心處理業務邏輯即可,不需要感知底層的協議差異和線程模型差異,實現了架構層面的分層隔離。
- 業務邏輯編排層(Service ChannelHandler):業務邏輯編排層通常有兩類:一類是純粹的業務邏輯編排,還有一類是其他的應用層協議插件,用於特定協議相關的會話和鏈路管理。
Netty Reactor 模型
前面,我們介紹了三種常見的 Reactor 線程模型 ,Netty 是典型的 Reactor 模型結構,下圖是 Netty 常見的主從 Reactor 模型示例圖。
在創建 ServerBootstrap 類實例前,先創建兩個 EventLoopGroup,一個 bossGroup,一個 workerGroup。它們實際上是兩個獨立的 Reactor 線程池,bossGroup 負責接收客戶端的連接,workerGroup 負責處理 IO 相關的讀寫操作,或者執行系統 task、定時 task 等。
用於接收客戶端請求的線程池職責如下:
- 接收客戶端 TCP 連接,初始化 Channel 參數;
- 將鏈路狀態變更事件通知給 ChannelPipeline;
處理 IO 操作的線程池職責如下:
- 異步讀取遠端數據,發送讀事件到 ChannelPipeline;
- 異步發送數據到遠端,調用 ChannelPipeline 的發送消息接口;
- 執行系統調用 Task;
- 執行定時任務 Task,如空閒鏈路檢測和發送心跳消息等。
通過調整兩個 EventLoopGroup 的線程數、是否共享線程池等方式,Netty 的 Reactor 線程模型可以在單線程、多線程和主從多線程間切換,用戶可以根據實際情況靈活配置。