Netty 是如何支撐高性能網絡通訊的?

前言

做爲一個高性能的 NIO 通訊框架,Netty 被普遍應用於大數據處理、互聯網消息中間件、遊戲和金融行業等。大多數應用場景對底層的通訊框架都有很高的性能要求,做爲綜合性能最高的 NIO 框架 之一,Netty 能夠徹底知足不一樣領域對高性能通訊的需求。本文咱們將從架構層對 Netty 的高性能設計和關鍵代碼實現進行剖析,看 Netty 是如何支撐高性能網絡通訊的。react

RPC 調用性能模型分析

傳統 RPC 調用性能差的緣由

1.網絡傳輸方式問題編程

傳統的 RPC 框架或者基於 RMI 等方式的遠程過程調用採用了同步阻塞 I/O,當客戶端的併發壓力或者網絡時延增大以後,同步阻塞 I/O 會因爲頻繁的 wait 致使 I/O 線程常常性的阻塞,因爲線程沒法高效的工做,I/O 處理能力天然降低。bootstrap

採用 BIO 通訊模型的服務端,一般由一個獨立的 Acceptor 線程負責監聽客戶端的鏈接,接收到客戶端鏈接以後,爲其建立一個新的線程處理請求消息,處理完成以後,返回應答消息給客戶端,線程銷燬,這就是典型的 「 一請求,一應答 」 模型。該架構最大的問題就是不具有彈性伸縮能力,當併發訪問量增長後,服務端的線程個數和併發訪問數成線性正比,因爲線程是 Java 虛擬機 很是寶貴的系統資源,當線程數膨脹以後,系統的性能急劇降低,隨着併發量的繼續增長,可能會發生句柄溢出、線程堆棧溢出等問題,並致使服務器最終宕機。安全

2.序列化性能差服務器

Java 序列化存在以下幾個典型問題:網絡

1.Java 序列化機制是 Java 內部的一 種對象編解碼技術,沒法跨語言使用。例如對於異構系統之間的對接,Java 序列化後的碼流須要可以經過其餘語言反序列化成原始對象,這很難支持。2.相比於其餘開源的序列化框架,Java 序列化後的碼流太大,不管是網絡傳輸仍是持久化到磁盤,都會致使額外的資源佔用。3.序列化性能差,資源佔用率高 ( 主要是 CPU 資源佔用高 )。多線程

3.線程模型問題架構

因爲採用同步阻塞 I/O,這會致使每一個 TCP 鏈接 都佔用 1 個線程,因爲線程資源是 JVM 虛擬機 很是寶貴的資源,當 I/O 讀寫阻塞致使線程沒法及時釋放時,會致使系統性能急劇降低,嚴重的甚至會致使虛擬機沒法建立新的線程。併發

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 是如何支撐高性能網絡通訊的?

 

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 提供了多種內存管理策略,經過在啓動輔助類中配置相關參數,能夠實現差別化的定製。

相關文章
相關標籤/搜索