作為一個高性能的 NIO 通信框架,Netty 被廣泛應用于大資料處理、互聯網訊息中間件、游戲和金融行業等,大多數應用場景對底層的通信框架都有很高的性能要求,作為綜合性能最高的 NIO 框架 之一,Netty 可以完全滿足不同領域對高性能通信的需求,本文我們將從架構層對 Netty 的高性能設計和關鍵代碼實作進行剖析,看 Netty 是如何支撐高性能網路通信的,
RPC 呼叫性能模型分析
傳統 RPC 呼叫性能差的原因
網路傳輸方式問題
傳統的 RPC 框架或者基于 RMI 等方式的遠程程序呼叫采用了同步阻塞 I/O,當客戶端的并發壓力或者網路時延增大之后,同步阻塞 I/O 會由于頻繁的 wait 導致 I/O 執行緒經常性的阻塞,由于執行緒無法高效的作業,I/O 處理能力自然下降,
采用 BIO 通信模型的服務端,通常由一個獨立的 Acceptor 執行緒負責監聽客戶端的連接,接收到客戶端連接之后,為其創建一個新的執行緒處理請求訊息,處理完成之后,回傳應答訊息給客戶端,執行緒銷毀,這就是典型的 “ 一請求,一應答 ” 模型,該架構最大的問題就是不具備彈性伸縮能力,當并發訪問量增加后,服務端的執行緒個數和并發訪問數成線性正比,由于執行緒是 Java 虛擬機 非常寶貴的系統資源,當執行緒數膨脹之后,系統的性能急劇下降,隨著并發量的繼續增加,可能會發生句柄溢位、執行緒堆疊溢位等問題,并導致服務器最終宕機,
序列化性能差
Java 序列化存在如下幾個典型問題:
1.Java 序列化機制是 Java 內部的一 種物件編解碼技術,無法跨語言使用,例如對于異構系統之間的對接,Java 序列化后的碼流需要能夠通過其他語言反序列化成原始物件,這很難支持,2.相比于其他開源的序列化框架,Java 序列化后的碼流太大,無論是網路傳輸還是持久化到磁盤,都會導致額外的資源占用,3.序列化性能差,資源占用率高 ( 主要是 CPU 資源占用高 ),
執行緒模型問題
由于采用同步阻塞 I/O,這會導致每個 TCP 連接 都占用 1 個執行緒,由于執行緒資源是 JVM 虛擬機 非常寶貴的資源,當 I/O 讀寫阻塞導致執行緒無法及時釋放時,會導致系統性能急劇下降,嚴重的甚至會導致虛擬機無法創建新的執行緒,
IO 通信性能三原則
盡管影響 I/O 通信性能的因素非常多,但是從架構層面看主要有三個要素,
1.傳輸:用什么樣的通道將資料發送給對方,可以選擇 BIO、NIO 或者 AIO,I/O 模型 在很大程度上決定了通信的性能;2.協議:采用什么樣的通信協議,HTTP 等公有協議或者內部私有協議,協議的選擇不同,性能也不同,相比于公有協議,內部私有協議的性能通常可以被設計得更優;3.執行緒模型:資料報如何讀取?讀取之后的編解碼在哪個執行緒進行,編解碼后的訊息如何派發,Reactor 執行緒模型的不同,對性能的影響也非常大,
異步非阻塞通信
在 I/O 編程程序中,當需要同時處理多個客戶端接入請求時,可以利用多執行緒或者 I/O 多路復用技術進行處理,I/O 多路復用技術通過把多個 I/O 的阻塞復用到同一個 select 的阻塞上,從而使得系統在單執行緒的情況下可以同時處理多個客戶端請求, 與傳統的多執行緒 / 多行程模型比,I/O 多路復用的最大優勢是系統開銷小,系統不需要創建新的額外行程或者執行緒,也不需要維護這些行程和執行緒的運行,降低了系統的維護作業量,節省了系統資源,
JDK1.4 提供了對非阻塞 I/O 的支持,JDK1.5 使用 epoll 替代了傳統的 select / poll,極大地提升了 NIO 通信 的性能,
與 Socket 和 ServerSocket 類相對應,NIO 也提供了 SocketChannel 和 ServerSocketChannel 兩種不同的套接字通道實作,這兩種新增的通道都支持阻塞和非阻塞兩種模式, 阻塞模式使用非常簡單,但是性能和可靠性都不好,非阻塞模式則正好相反,開發人員一般可以根據自己的需要來選擇合適的模式,一般來說,低負載、低并發的應用程式可以選擇同步阻塞 I/O 以降低編程復雜度,但是對于高負載、高并發的網路應用,需要使用 NIO 的非阻塞模式進行開發,
Netty 的 I/O 執行緒 NioEventLoop 由于聚合了多路復用器 Selector,可以同時并發處理成百上千個客戶端 SocketChannel,由于讀寫操作都是非阻塞的,這就可以充分提升 I/O 執行緒 的運行效率,避免由頻繁的 I/O 阻塞 導致的執行緒掛起,另外,由于 Netty 采用了異步通信模式,一個 I/O 執行緒 可以并發處理 N 個客戶端連接和讀寫操作,這從根本上解決了傳統 同步阻塞 I/O “ 一連接,一執行緒 ” 模型,架構的性能、彈性伸縮能力和可靠性都得到了極大的提升,
高效的 Reactor 執行緒模型
常用的 Reactor 執行緒模型有三種,分別如下:
1.Reactor 單執行緒模型;2.Reactor 多執行緒模型;3.主從 Reactor 多執行緒模型,
Reactor 單執行緒模型,指的是所有的 I/O 操作都在同一個 NIO 執行緒上面完成,NIO 執行緒的職責如下:
1.作為 NIO 服務端,接收客戶端的 TCP 連接;2.作為 NIO 客戶端,向服務端發起 TCP 連接;3.讀取通信對端的請求或者應答訊息;4.向通信對端發送訊息請求或者應答訊息,
由于 Reactor 模式使用的是異步非阻塞 I/O,所有的 I/O 操作 都不會導致阻塞,理論上一個執行緒可以獨立處理所有 I/O 相關的操作,從架構層面看,一個 NIO 執行緒確實可以完成其承擔的職責,例如,通過 Acceptor 接收客戶端的 TCP 連接請求訊息,鏈路建立成功之后,通過 Dispatch 將對應的 ByteBuffer 派發到指定的 Handler 上進行訊息解碼,用戶 Handler 可以通過 NIO 執行緒 將訊息發送給客戶端,
對于一些小容量應用場景,可以使用單執行緒模型,但是對于高負載、大并發的應用卻不合適,主要原因如下,
1.一個 NIO 執行緒 同時處理成百上千的鏈路,性能上無法支撐, 即便 NIO 執行緒 的 CPU 負荷 達到 100%,也無法滿足海量訊息的編碼,解碼、讀取和發送;2.當 NIO 執行緒 負載過重之后,處理速度將變慢,這會導致大量客戶端連接超時,超時之后往往會進行重發,這更加重了 NIO 執行緒 的負載,最侄訓導致大量訊息積壓和處理超時,NIO 執行緒會成為系統的性能瓶頸;3.可靠性問題,一旦 NIO 執行緒意外跑飛,或者進入死回圈,會導致整個系統通信模塊不可用,不能接收和處理外部訊息,造成節點故障,
為了解決這些問題,演進出了 Reactor 多執行緒模型,下面我們看一下 Reactor 多執行緒模型,
Rector 多執行緒模型與單執行緒模型最大的區別就是有一組 NIO 執行緒 處理 I/O 操作,它的特點如下,
1.有一個專門的 NIO 執行緒 —— Acceptor 執行緒 用于監聽服務埠,接收客戶端的 TCP 連接請求;2.網路 IO 操作 —— 讀、寫等由一個 NIO 執行緒池 負責,執行緒池可以采用標準的 JDK 執行緒池 實作,它包含一個任務佇列和 N 個可用的執行緒,由這些 NIO 執行緒 負責訊息的讀取、解碼、編碼和發送;3.1 個 NIO 執行緒 可以同時處理 N 條鏈路,但是 1 個鏈路只對應 1 個 NIO 執行緒,以防止發生并發操作問題,
在絕大多數場景下,Reactor 多執行緒模型 都可以滿足性能需求,但是,在極特殊應用場景中,一個 NIO 執行緒負責監聽和處理所有的客戶端連接可能會存在性能問題,例如百萬客戶端并發連接,或者服務端需要對客戶端的握手訊息進行安全認證,認證本身非常損耗性能,在這類場景下,單獨一個 Acceptor 執行緒 可能會存在性能不足問題,為了解決性能問題,產生了第三種 Reactor 執行緒模型 —— 主從 Reactor 多執行緒模型,
主從 Reactor 執行緒模型的特點是,服務端用于接收客戶端連接的不再是個單執行緒的連接處理 Acceptor,而是一個獨立的 Acceptor 執行緒池,Acceptor 接收到客戶端 TCP 連接請求 處理完成后 ( 可能包含接入認證等 ),將新創建的 SocketChannel 注冊到 I/O 處理執行緒池 的某個 I/O 執行緒 上,由它負責 SocketChannel 的讀寫和編解碼作業,Acceptor 執行緒池 只用于客戶端的登錄、握手和安全認證,一旦鏈路建立成功,就將鏈路注冊到 I/O 處理執行緒池的 I/O 執行緒 上,每個 I/O 執行緒 可以同時監聽 N 個鏈路,對鏈路產生的 IO 事件 進行相應的 訊息讀取、解碼、編碼及訊息發送等操作,
利用主從 Reactor 執行緒模型,可以解決 1 個 Acceptor 執行緒 無法有效處理所有客戶端連接的性能問題,因此,Netty 官方也推薦使用該執行緒模型,
事實上,Netty 的執行緒模型并非固定不變,通過在啟動輔助類中創建不同的 EventLoopGroup 實體 并進行適當的引數配置,就可以支持上述三種 Reactor 執行緒模型,可以根據業務場景的性能訴求,選擇不同的執行緒模型,
Netty 單執行緒模型服務端代碼示例如下:
EventLoopGroup reactor = new NioEventLoopGroup(1); ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(reactor, reactor) .channel(NioServerSocketChannel.class) ......
Netty 多執行緒模型代碼示例如下:
EventLoopGroup acceptor = new NioEventLoopGroup(1); EventLoopGroup ioGroup = new NioEventLoopGroup(); ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(acceptor, ioGroup) .channel(NioServerSocketChannel.class) ......
Netty 主從多執行緒模型代碼示例如下:
EventLoopGroup acceptorGroup = new NioEventLoopGroup(); EventLoopGroup ioGroup = new NioEventLoopGroup(); ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(acceptorGroup, ioGroup) .channel(NioServerSocketChannel.class) ......
無鎖化的串行設計
在大多數場景下,并行多執行緒處理可以提升系統的并發性能,但是,如果對于共享資源的并發訪問處理不當,會帶來嚴重的鎖競爭,這最侄訓導致性能的下降,為了盡可能地避免鎖競爭帶來的性能損耗,可以通過串行化設計,即訊息的處理盡可能在同一個執行緒內完成,期間不進行執行緒切換,這樣就避免了多執行緒競爭和同步鎖,
為了盡可能提升性能,Netty 對訊息的處理采用了串行無鎖化設計,在 I/O 執行緒 內部進行串行操作,避免多執行緒競爭導致的性能下降,Netty 的串行化設計作業原理圖如下圖所示,
Netty 的 NioEventLoop 讀取到訊息之后,直接呼叫 ChannelPipeline 的 fireChannelRead(Object msg),只要用戶不主動切換執行緒,一直會由 NioEventLoop 呼叫到 用戶的 Handler,期間不進行執行緒切換,這種串行化處理方式避免了多執行緒操作導致的鎖的競爭,從性能角度看是最優的,
零拷貝
Netty 的“ 零拷貝 ”主要體現在如下三個方面,
第一種情況,Netty 的接收和發送 ByteBuffer 采用堆外直接記憶體 (DIRECT BUFFERS) 進行 Socket 讀寫,不需要進行位元組緩沖區的二次拷貝,如果使用傳統的 堆記憶體(HEAP BUFFERS) 進行 Socket 讀寫,JVM 會將 堆記憶體 Buffer 拷貝一份到 直接記憶體 中,然后才寫入 Socket,相比于堆外直接記憶體,訊息在發送程序中多了一次緩沖區的記憶體拷貝,
下面我們繼續看第二種“ 零拷貝 ” 的實作 CompositeByteBuf,它對外將多個 ByteBuf 封裝成一個 ByteBuf,對外提供統一封裝后的 ByteBuf 介面,CompositeByteBuf 實際就是個 ByteBuf 的裝飾器,它將多個 ByteBuf 組合成一個集合,然后對外提供統一的 ByteBuf 介面,添加 ByteBuf,不需要做記憶體拷貝,
第三種 “ 零拷貝 ” 就是檔案傳輸,Netty 檔案傳輸類 DefaultFileRegion 通過 transferTo() 方法 將檔案發送到目標 Channel 中,很多作業系統直接將檔案緩沖區的內容發送到目標 Channel 中,而不需要通過回圈拷貝的方式,這是一種更加高效的傳輸方式,提升了傳輸性能,降低了 CPU 和記憶體占用,實作了檔案傳輸的 “ 零拷貝 ” ,
記憶體池
隨著 JVM 虛擬機 和 JIT 即時編譯技術 的發展,物件的分配和回收是個非常輕量級的作業,但是對于緩沖區 Buffer,情況卻稍有不同,特別是對于堆外直接記憶體的分配和回收,是一件耗時的操作,為了盡量重用緩沖區,Netty 提供了基于記憶體池的緩沖區重用機制, ByteBuf 的子類中提供了多種 PooledByteBuf 的實作,基于這些實作 Netty 提供了多種記憶體管理策略,通過在啟動輔助類中配置相關引數,可以實作差異化的定制,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/80810.html
標籤:Java
上一篇:SpringMVC大威天龍
