netty cpu 占用率 高_Netty 是如何支撑高性能网络通信的?
作為一個高性能的 NIO 通信框架,Netty 被廣泛應用于大數據處理、互聯網消息中間件、游戲和金融行業等。大多數應用場景對底層的通信框架都有很高的性能要求,作為綜合性能最高的 NIO 框架 之一,Netty 可以完全滿足不同領域對高性能通信的需求。本文我們將從架構層對 Netty 的高性能設計和關鍵代碼實現進行剖析,看 Netty 是如何支撐高性能網絡通信的。
RPC 調用性能模型分析
傳統 RPC 調用性能差的原因
1.網絡傳輸方式問題
傳統的 RPC 框架或者基于 RMI 等方式的遠程過程調用采用了同步阻塞 I/O,當客戶端的并發壓力或者網絡時延增大之后,同步阻塞 I/O 會由于頻繁的 wait 導致 I/O 線程經常性的阻塞,由于線程無法高效的工作,I/O 處理能力自然下降。
采用 BIO 通信模型的服務端,通常由一個獨立的 Acceptor 線程負責監聽客戶端的連接,接收到客戶端連接之后,為其創建一個新的線程處理請求消息,處理完成之后,返回應答消息給客戶端,線程銷毀,這就是典型的 “ 一請求,一應答 ” 模型。該架構最大的問題就是不具備彈性伸縮能力,當并發訪問量增加后,服務端的線程個數和并發訪問數成線性正比,由于線程是 Java 虛擬機 非常寶貴的系統資源,當線程數膨脹之后,系統的性能急劇下降,隨著并發量的繼續增加,可能會發生句柄溢出、線程堆棧溢出等問題,并導致服務器最終宕機。
2.序列化性能差
Java 序列化存在如下幾個典型問題:
1.Java 序列化機制是 Java 內部的一 種對象編解碼技術,無法跨語言使用。例如對于異構系統之間的對接,Java 序列化后的碼流需要能夠通過其他語言反序列化成原始對象,這很難支持。2.相比于其他開源的序列化框架,Java 序列化后的碼流太大,無論是網絡傳輸還是持久化到磁盤,都會導致額外的資源占用。3.序列化性能差,資源占用率高 ( 主要是 CPU 資源占用高 )。
3.線程模型問題
由于采用同步阻塞 I/O,這會導致每個 TCP 連接 都占用 1 個線程,由于線程資源是 JVM 虛擬機 非常寶貴的資源,當 I/O 讀寫阻塞導致線程無法及時釋放時,會導致系統性能急劇下降,嚴重的甚至會導致虛擬機無法創建新的線程。
http://4.IO 通信性能三原則
盡管影響 I/O 通信性能的因素非常多,但是從架構層面看主要有三個要素。
1.傳輸:用什么樣的通道將數據發送給對方。可以選擇 BIO、NIO 或者 AIO,I/O 模型 在很大程度上決定了通信的性能;2.協議:采用什么樣的通信協議,HTTP 等公有協議或者內部私有協議。協議的選擇不同,性能也不同。相比于公有協議,內部私有協議的性能通常可以被設計得更優;3.線程模型:數據報如何讀取?讀取之后的編解碼在哪個線程進行,編解碼后的消息如何派發,Reactor 線程模型的不同,對性能的影響也非常大。
5.異步非阻塞通信
在 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.toutiao.com/i6860457783901291019/總結
以上是生活随笔為你收集整理的netty cpu 占用率 高_Netty 是如何支撑高性能网络通信的?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java中为什么要用注解_java中的注
- 下一篇: oracle查被锁存储过程,oracle