Netty异步非阻塞事件驱动及原理详解
? ? ? ?本文基于 Netty 4.1 展開介紹相關理論模型、使用場景、基本組件、整體架構,知其然且知其所以然,希望給大家在實際開發實踐、學習開源項目方面提供參考。
? ? ? ?Netty 是一個異步事件驅動的網絡應用程序框架,用于快速開發可維護的高性能協議服務器和客戶端。
一、JDK 原生 NIO 程序的問題
JDK 原生也有一套網絡應用程序 API,但是存在一系列問題,主要如下:
- NIO 的類庫和 API 繁雜,使用麻煩。你需要熟練掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。
- 需要具備其他的額外技能做鋪墊。例如熟悉 Java 多線程編程,因為 NIO 編程涉及到 Reactor 模式,你必須對多線程和網路編程非常熟悉,才能編寫出高質量的 NIO 程序。
- 可靠性能力補齊,開發工作量和難度都非常大。例如客戶端面臨斷連重連、網絡閃斷、半包讀寫、失敗緩存、網絡擁塞和異常碼流的處理等等。
NIO 編程的特點是功能開發相對容易,但是可靠性能力補齊工作量和難度都非常大。
- JDK NIO 的 Bug。例如臭名昭著的 Epoll Bug,它會導致 Selector 空輪詢,最終導致 CPU 100%。
官方聲稱在 JDK 1.6 版本的 update 18 修復了該問題,但是直到 JDK 1.7 版本該問題仍舊存在,只不過該 Bug 發生概率降低了一些而已,它并沒有被根本解決。
二、Netty 的特點
Netty 對 JDK 自帶的 NIO 的 API 進行封裝,解決上述問題,主要特點有:
- 設計優雅,適用于各種傳輸類型的統一 API 阻塞和非阻塞 Socket;基于靈活且可擴展的事件模型,可以清晰地分離關注點;高度可定制的線程模型 - 單線程,一個或多個線程池;真正的無連接數據報套接字支持(自 3.1 起)。
- 使用方便,詳細記錄的 Javadoc,用戶指南和示例;沒有其他依賴項,JDK 5(Netty 3.x)或 6(Netty 4.x)就足夠了。
- 高性能,吞吐量更高,延遲更低;減少資源消耗;最小化不必要的內存復制。
- 安全,完整的 SSL/TLS 和 StartTLS 支持。
- 社區活躍,不斷更新,社區活躍,版本迭代周期短,發現的 Bug 可以被及時修復,同時,更多的新功能會被加入。
三、Netty 常見使用場景
Netty 常見的使用場景如下:
- 互聯網行業。在分布式系統中,各個節點之間需要遠程服務調用,高性能的 RPC 框架必不可少,Netty 作為異步高性能的通信框架,往往作為基礎通信組件被這些 RPC 框架使用。
? ? ? ?典型的應用有:阿里分布式服務框架 Dubbo 的 RPC 框架使用 Dubbo 協議進行節點間通信,Dubbo 協議默認使用 Netty 作為基礎通信組件,用于實現各進程節點之間的內部通信。
- 游戲行業。無論是手游服務端還是大型的網絡游戲,Java 語言得到了越來越廣泛的應用。Netty 作為高性能的基礎通信組件,它本身提供了 TCP/UDP 和 HTTP 協議棧。
? ? ? ?非常方便定制和開發私有協議棧,賬號登錄服務器,地圖服務器之間可以方便的通過 Netty 進行高性能的通信。
- 大數據領域。經典的 Hadoop 的高性能通信和序列化組件 Avro 的 RPC 框架,默認采用 Netty 進行跨界點通信,它的 Netty Service 基于 Netty 框架二次封裝實現。
三、Netty 高性能設計
? ? ? ?Netty 作為異步事件驅動的網絡,高性能之處主要來自于其 I/O 模型和線程處理模型,前者決定如何收發數據,后者決定如何處理數據。
3.1、I/O 模型
用什么樣的通道將數據發送給對方,BIO、NIO 或者 AIO,I/O 模型在很大程度上決定了框架的性能。
(1)、同步阻塞IO
? ? ? ?阻塞指的是I/O方法調用(比如read)在數據或者狀態還沒有就緒的時候,一直等待,直到就緒才返回。比如阻塞模式下的Socket的read方法,如果沒有接收到數據,read方法將會阻塞,程序就會停在那里,不能做其他的事情。這種模型下,如果服務器想處理多個連接,那么就要為每個Socket連接創建一個單獨的線程,開銷會很大。
傳統阻塞型 I/O(BIO)可以用下圖表示:
特點如下:
- 每個請求都需要獨立的線程完成數據 Read,業務處理,數據 Write 的完整操作問題。
- 當并發數較大時,需要創建大量線程來處理連接,系統資源占用較大。
- 連接建立后,如果當前線程暫時沒有數據可讀,則線程就阻塞在 Read 操作上,造成線程資源浪費。
(2)、同步非阻塞IO?
? ? ? ?非阻塞模型是I/O方法調用(比如read)無論數據有沒有就緒,會馬上返回。如果有數據就會讀到數據,如果沒有數據就返回一個錯誤碼。應用程序需要用輪詢的方式不斷去檢測數據有沒有就緒,但是程序不會被阻塞,除了輪詢程序還可以做其他事情。但是這種輪詢的方式會浪費CPU的時間,效率不夠高。
(3)、IO多路復用
? ? ? ?IO多路復用模型是建立在內核提供的多路分離函數select基礎之上的,使用select函數可以避免同步非阻塞IO模型中輪詢等待的問題。
???????如上圖《多路分離函數select》所示,用戶首先將需要進行IO操作的socket添加到select中,然后阻塞等待select系統調用返回。當數據到達時,socket被激活,select函數返回。用戶線程正式發起read請求,讀取數據并繼續執行。
???????從流程上來看,使用select函數進行IO請求和同步阻塞模型沒有太大的區別,甚至還多了添加監視socket,以及調用select函數的額外操作,效率更差。但是,使用select以后最大的優勢是用戶可以在一個線程內同時處理多個socket的IO請求。用戶可以注冊多個socket,然后不斷地調用select讀取被激活的socket,即可達到在同一個線程內同時處理多個IO請求的目的。而在同步阻塞模型中,必須通過多線程的方式才能達到這個目的。
? ? ? ?然而,使用select函數的優點并不僅限于此。雖然上述方式允許單線程內處理多個IO請求,但是每個IO請求的過程還是阻塞的(在select函數上阻塞),平均時間甚至比同步阻塞IO模型還要長。如果用戶線程只注冊自己感興趣的socket或者IO請求,然后去做自己的事情,等到數據到來時再進行處理,則可以提高CPU的利用率。
? ? ? ?如上圖《Reactor實現多路復用》所示,EventHandler抽象類表示IO事件處理器,它擁有IO文件句柄Handle(通過get_handle獲取),以及對Handle的操作handle_event(讀/寫等)。繼承于EventHandler的子類可以對事件處理器的行為進行定制。Reactor類用于管理EventHandler(注冊、刪除等),并使用handle_events實現事件循環,不斷調用同步事件多路分離器(一般是內核)的多路分離函數select,只要某個文件句柄被激活(可讀/寫等),select就返回(阻塞),handle_events就會調用與文件句柄關聯的事件處理器的handle_event進行相關操作。
? ? ? ?如上圖《Reactor實現多路復用》所示,通過Reactor的方式,可以將用戶線程輪詢IO操作狀態的工作統一交給handle_events事件循環進行處理。用戶線程注冊事件處理器之后可以繼續執行做其他的工作(異步),而Reactor線程負責調用內核的select函數檢查socket狀態。當有socket被激活時,則通知相應的用戶線程(或執行用戶線程的回調函數),執行handle_event進行數據讀取、處理的工作。由于select函數是阻塞的,因此多路IO復用模型也被稱為異步阻塞IO模型。注意,這里的所說的阻塞是指select函數執行時線程被阻塞,而不是指socket。一般在使用IO多路復用模型時,socket都是設置為NONBLOCK的,不過這并不會產生影響,因為用戶發起IO請求時,數據已經到達了,用戶線程一定不會被阻塞。
? ? ? ?IO多路復用是最常使用的IO模型,但是其異步程度還不夠“徹底”,因為它使用了會阻塞線程的select系統調用。因此IO多路復用只能稱為異步阻塞IO,而非真正的異步IO。?
(4)、異步IO?
? ? ? ?異步模型是指用戶給I/O方法調用(比如read)提供一個回調方法,I/O方法調用會立刻返回,等到數據就緒時回調方法會執行。這個看上去很好用,把所有的處理都推給了系統,用戶只需要關心回調方法中的數據處理就行了,但是在高并發情況下,處理好系統I/O程序和用戶程序之間的CPU競爭比較困難。
? ? ? ?如上圖《Proactor設計模式》所示,Proactor模式和Reactor模式在結構上比較相似,不過在用戶(Client)使用方式上差別較大。Reactor模式中,用戶線程通過向Reactor對象注冊感興趣的事件監聽,然后事件觸發時調用事件處理函數。而Proactor模式中,用戶線程將AsynchronousOperation(讀/寫等)、Proactor以及操作完成時的CompletionHandler注冊到AsynchronousOperationProcessor。AsynchronousOperationProcessor使用Facade模式提供了一組異步操作API(讀/寫等)供用戶使用,當用戶線程調用異步API后,便繼續執行自己的任務。AsynchronousOperationProcessor 會開啟獨立的內核線程執行異步操作,實現真正的異步。當異步IO操作完成時,AsynchronousOperationProcessor將用戶線程與AsynchronousOperation一起注冊的Proactor和CompletionHandler取出,然后將CompletionHandler與IO操作的結果數據一起轉發給Proactor,Proactor負責回調每一個異步操作的事件完成處理函數handle_event。雖然Proactor模式中每個異步操作都可以綁定一個Proactor對象,但是一般在操作系統中,Proactor被實現為Singleton模式,以便于集中化分發操作完成事件。
???????如上圖《Proactor實現異步IO》所示,異步IO模型中,用戶線程直接使用內核提供的異步IO API發起read請求,且發起后立即返回,繼續執行用戶線程代碼。不過此時用戶線程已經將調用的AsynchronousOperation和CompletionHandler注冊到內核,然后操作系統開啟獨立的內核線程去處理IO操作。當read請求的數據到達時,由內核負責讀取socket中的數據,并寫入用戶指定的緩沖區中。最后內核將read的數據和用戶線程注冊的CompletionHandler分發給內部Proactor,Proactor將IO完成的信息通知給用戶線程(一般通過調用用戶線程注冊的完成事件處理函數),完成異步IO。
???????相比于IO多路復用模型,異步IO并不十分常用,不少高性能并發服務程序使用IO多路復用模型+多線程任務處理的架構基本可以滿足需求。況且目前操作系統對異步IO的支持并非特別完善,更多的是采用IO多路復用模型模擬異步IO的方式(IO事件觸發時不直接通知用戶線程,而是將數據讀寫完畢后放到用戶指定的緩沖區中)。Java7之后已經支持了異步IO,感興趣的讀者可以嘗試使用。
3.2、Reactor多線程模型
(1)、大部分網絡服務包括以下處理步驟
(2)、Reactor多線程模型圖
值得注意的是Handler中的read和send方法是在Reactor線程而不是worker thread中執行的。這意味著對socket數據的讀取發送數據和對數據的處理是在不同的線程中進行的.
(3)、Reactor多線程模型的主要問題
(4)、實際應用中的多線程模型(改進后的模型圖)
? ? ? ?Reactor線程專門用于接受客戶端連接(通過acceptor);創建多個Event Loop ,組成Event Loop Pool,每個Event Loop都有自己的Selector,并且運行在獨立的線程上;Acceptor對于每一個客戶端的連接從EventLoopPool中選擇一個Event Loop進行處理,并且保證每個客戶端連接在整個生命周期中都是由同一個Event Loop線程來處理,從而使得Handler中的實現-read,decode,process,encode,send-都在同一個線程中執行。整個線程模型除了高效的性能,還有非常重要的一點是Handler的實現不需要加鎖,一方面對性能有幫助,另一方面避免多線程編程的復雜度。
?
總結
以上是生活随笔為你收集整理的Netty异步非阻塞事件驱动及原理详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 综述 | 知识图谱实体链接:一份“由浅入
- 下一篇: 开源开放 | 开源网络通信行业知识图谱(