天天看點

Netty高性能之道

  • 一 Netty高性能之道
    • 傳統RPC調用性能問題
      • 問題1網絡傳輸方式問題
      • 問題2序列化方式問題
      • 問題3線程模型問題
  • 二高性能的三個主題
  • 三Netty高性能之道
    • 異步非阻塞通信
    • 零拷貝
    • 記憶體池
    • 高效的Reactor線程模型
      • Reactor單線程模型
      • Reactor多線程模型
      • 主從Reactor多線程模型
    • 無鎖化的串行設計理念
    • 高效的并發程式設計
    • 高性能的序列化架構
    • 靈活的TCP參數配置能力

一、 Netty高性能之道

傳統的RPC架構或者基于RMI等方式的遠端服務(過程)調用采用了同步阻塞IO,當用戶端的并發壓力或者網絡時延增大之後,同步阻塞IO會由于頻繁的wait導緻IO線程經常性的阻塞,由于線程無法高效的工作,IO處理能力自然下降。

下面,我們通過BIO通信模型圖看下BIO通信的弊端:

Netty高性能之道

采用BIO通信模型的服務端,通常由一個獨立的Acceptor線程負責監聽用戶端的連接配接,接收到用戶端連接配接之後為用戶端連接配接建立一個新的線程處理請求消息,處理完成之後,傳回應答消息給用戶端,線程銷毀,這就是典型的一請求一應答模型。該架構最大的問題就是不具備彈性伸縮能力,當并發通路量增加後,服務端的線程個數和并發通路數成線性正比,由于線程是JAVA虛拟機非常寶貴的系統資源,當線程數膨脹之後,系統的性能急劇下降,随着并發量的繼續增加,可能會發生句柄溢出、線程堆棧溢出等問題,并導緻伺服器最終當機。

Java序列化存在如下幾個典型問題:

  • Java序列化機制是Java内部的一種對象編解碼技術,無法跨語言使用;例如對于異構系統之間的對接,Java序列化後的碼流需要能夠通過其它語言反序列化成原始對象(副本),目前很難支援;
  • 相比于其它開源的序列化架構,Java序列化後的碼流太大,無論是網絡傳輸還是持久化到磁盤,都會導緻額外的資源占用;
  • 序列化性能差(CPU資源占用高)。

由于采用同步阻塞IO,這會導緻每個TCP連接配接都占用1個線程,由于線程資源是JVM虛拟機非常寶貴的資源,當IO讀寫阻塞導緻線程無法及時釋放時,會導緻系統性能急劇下降,嚴重的甚至會導緻虛拟機無法建立新的線程。

二、高性能的三個主題

  • 傳輸:用什麼樣的通道将資料發送給對方,BIO、NIO或者AIO,IO模型在很大程度上決定了架構的性能。
  • 協定:采用什麼樣的通信協定,HTTP或者内部私有協定。協定的選擇不同,性能模型也不同。相比于公有協定,内部私有協定的性能通常可以被設計的更優。
  • 線程:資料報如何讀取?讀取之後的編解碼在哪個線程進行,編解碼後的消息如何派發,Reactor線程模型的不同,對性能的影響也非常大。
Netty高性能之道

三、Netty高性能之道

在IO程式設計過程中,當需要同時處理多個用戶端接入請求時,可以利用多線程或者IO多路複用技術進行處理。IO多路複用技術通過把多個IO的阻塞複用到同一個select的阻塞上,進而使得系統在單線程的情況下可以同時處理多個用戶端請求。與傳統的多線程/多程序模型比,I/O多路複用的最大優勢是系統開銷小,系統不需要建立新的額外程序或者線程,也不需要維護這些程序和線程的運作,降低了系統的維護工作量,節省了系統資源。

JDK1.4提供了對非阻塞IO(NIO)的支援,JDK1.5_update10版本使用epoll替代了傳統的select/poll,極大的提升了NIO通信的性能。

與Socket類和ServerSocket類相對應,NIO也提供了SocketChannel和ServerSocketChannel兩種不同的套接字通道實作。這兩種新增的通道都支援阻塞和非阻塞兩種模式。阻塞模式使用非常簡單,但是性能和可靠性都不好,非阻塞模式正好相反。開發人員一般可以根據自己的需要來選擇合适的模式,一般來說,低負載、低并發的應用程式可以選擇同步阻塞IO以降低程式設計複雜度。但是對于高負載、高并發的網絡應用,需要使用NIO的非阻塞模式進行開發。

Netty架構按照Reactor模式設計和實作,它的服務端通信序列圖如下:

Netty高性能之道

用戶端通信序列圖如下:

Netty高性能之道

Netty的IO線程NioEventLoop由于聚合了多路複用器Selector,可以同時并發處理成百上千個用戶端Channel,由于讀寫操作都是非阻塞的,這就可以充分提升IO線程的運作效率,避免由于頻繁IO阻塞導緻的線程挂起。另外,由于Netty采用了異步通信模式,一個IO線程可以并發處理N個用戶端連接配接和讀寫操作,這從根本上解決了傳統同步阻塞IO一連接配接一線程模型,架構的性能、彈性伸縮能力和可靠性都得到了極大的提升。

很多使用者都聽說過Netty具有“零拷貝”功能,但是具體展現在哪裡又說不清楚,本小節就詳細對Netty的“零拷貝”功能進行講解。

Netty的“零拷貝”主要展現在如下三個方面:

  • Netty的接收和發送ByteBuffer采用DIRECT BUFFERS,使用堆外直接記憶體進行Socket讀寫,不需要進行位元組緩沖區的二次拷貝。如果使用傳統的堆記憶體(HEAP BUFFERS)進行Socket讀寫,JVM會将堆記憶體Buffer拷貝一份到直接記憶體中,然後才寫入Socket中。相比于堆外直接記憶體,消息在發送過程中多了一次緩沖區的記憶體拷貝。
  • Netty提供了組合Buffer對象,可以聚合多個ByteBuffer對象,使用者可以像操作一個Buffer那樣友善的對組合Buffer進行操作,避免了傳統通過記憶體拷貝的方式将幾個小Buffer合并成一個大的Buffer。
  • Netty的檔案傳輸采用了transferTo方法,它可以直接将檔案緩沖區的資料發送到目标Channel,避免了傳統通過循環write方式導緻的記憶體拷貝問題。

随着JVM虛拟機和JIT即時編譯技術的發展,對象的配置設定和回收是個非常輕量級的工作。但是對于緩沖區Buffer,情況卻稍有不同,特别是對于堆外直接記憶體的配置設定和回收,是一件耗時的操作。為了盡量重用緩沖區,Netty提供了基于記憶體池的緩沖區重用機制。下面我們一起看下Netty ByteBuf的實作:

Netty高性能之道

Netty提供了多種記憶體管理政策,通過在啟動輔助類中配置相關參數,可以實作差異化的定制。

常用的Reactor線程模型有三種,分别如下:

  • Reactor單線程模型;
  • Reactor多線程模型;
  • 主從Reactor多線程模型;

指的是所有的IO操作都在同一個NIO線程上面完成,NIO線程的職責如下:

  • 作為NIO服務端,接收用戶端的TCP連接配接;
  • 作為NIO用戶端,向服務端發起TCP連接配接;
  • 讀取通信對端的請求或者應答消息;
  • 向通信對端發送消息請求或者應答消息。

Reactor單線程模型示意圖如下所示:

Netty高性能之道

由于Reactor模式使用的是異步非阻塞IO,所有的IO操作都不會導緻阻塞,理論上一個線程可以獨立處理所有IO相關的操作。從架構層面看,一個NIO線程确實可以完成其承擔的職責。例如,通過Acceptor接收用戶端的TCP連接配接請求消息,鍊路建立成功之後,通過Dispatch将對應的ByteBuffer派發到指定的Handler上進行消息解碼。使用者Handler可以通過NIO線程将消息發送給用戶端。對于一些小容量應用場景,可以使用單線程模型。但是對于高負載、大并發的應用卻不合适,主要原因如下:

  • 一個NIO線程同時處理成百上千的鍊路,性能上無法支撐,即便NIO線程的CPU負荷達到100%,也無法滿足海量消息的編碼、解碼、讀取和發送;
  • 當NIO線程負載過重之後,處理速度将變慢,這會導緻大量用戶端連接配接逾時,逾時之後往往會進行重發,這更加重了NIO線程的負載,最終會導緻大量消息積壓和處理逾時,NIO線程會成為系統的性能瓶頸;
  • 可靠性問題:一旦NIO線程意外跑飛,或者進入死循環,會導緻整個系統通信子產品不可用,不能接收和處理外部消息,造成節點故障。

為了解決這些問題,演進出了Reactor多線程模型。Rector多線程模型與單線程模型最大的差別就是有一組NIO線程處理IO操作,它的原理圖如下:

Netty高性能之道

Reactor多線程模型的特點:

  • 有專門一個NIO線程-Acceptor線程用于監聽服務端,接收用戶端的TCP連接配接請求;
  • 網絡IO操作-讀、寫等由一個NIO線程池負責,線程池可以采用标準的JDK線程池實作,它包含一個任務隊列和N個可用的線程,由這些NIO線程負責消息的讀取、解碼、編碼和發送;
  • 1個NIO線程可以同時處理N條鍊路,但是1個鍊路隻對應1個NIO線程,防止發生并發操作問題。

在絕大多數場景下,Reactor多線程模型都可以滿足性能需求;但是,在極特殊應用場景中,一個NIO線程負責監聽和處理所有的用戶端連接配接可能會存在性能問題。例如百萬用戶端并發連接配接,或者服務端需要對用戶端的握手消息進行安全認證,認證本身非常損耗性能。在這類場景下,單獨一個Acceptor線程可能會存在性能不足問題。

為了解決性能問題,産生了第三種Reactor線程模型-主從Reactor多線程模型。

它的線程模型如下圖所示:

Netty高性能之道

主從Reactor線程模型的特點是:

  • 服務端用于接收用戶端連接配接的不再是個1個單獨的NIO線程,而是一個獨立的NIO線程池。
  • Acceptor接收到用戶端TCP連接配接請求處理完成後(可能包含接入認證等),将新建立的SocketChannel注冊到IO線程池(sub reactor線程池)的某個IO線程上,由它負責SocketChannel的讀寫和編解碼工作。
  • Acceptor線程池僅僅隻用于用戶端的登陸、握手和安全認證,一旦鍊路建立成功,就将鍊路注冊到後端subReactor線程池的IO線程上,由IO線程負責後續的IO操作。

利用主從NIO線程模型,可以解決1個服務端監聽線程無法有效處理所有用戶端連接配接的性能不足問題。是以,在Netty的官方demo中,推薦使用該線程模型。事實上,Netty的線程模型并非固定不變,通過在啟動輔助類中建立不同的EventLoopGroup執行個體并通過适當的參數配置,就可以支援上述三種Reactor線程模型。正是因為Netty 對Reactor線程模型的支援提供了靈活的定制能力,是以可以滿足不同業務場景的性能訴求。

在大多數場景下,并行多線程處理可以提升系統的并發性能。但是,如果對于共享資源的并發通路處理不當,會帶來嚴重的鎖競争,這最終會導緻性能的下降。為了盡可能的避免鎖競争帶來的性能損耗,可以通過串行化設計,即消息的處理盡可能在同一個線程内完成,期間不進行線程切換,這樣就避免了多線程競争和同步鎖。

為了盡可能提升性能,Netty采用了串行無鎖化設計,在IO線程内部進行串行操作,避免多線程競争導緻的性能下降。表面上看,串行化設計似乎CPU使用率不高,并發程度不夠。但是,通過調整NIO線程池的線程參數,可以同時啟動多個串行化的線程并行運作,這種局部無鎖化的串行線程設計相比一個隊列-多個工作線程模型性能更優。

Netty的串行化設計工作原理圖如下:

Netty高性能之道

圖2-25 Netty串行化工作原理圖

Netty的NioEventLoop讀取到消息之後,直接調用ChannelPipeline的fireChannelRead(Object msg),隻要使用者不主動切換線程,一直會由NioEventLoop調用到使用者的Handler,期間不進行線程切換,這種串行化處理方式避免了多線程操作導緻的鎖的競争,從性能角度看是最優的。

Netty的高效并發程式設計主要展現在如下幾點:

  • volatile的大量、正确使用;
  • CAS和原子類的廣泛使用;
  • 線程安全容器的使用;
  • 通過讀寫鎖提升并發性能。

影響序列化性能的關鍵因素總結如下:

  • 序列化後的碼流大小(網絡帶寬的占用);
  • 序列化&反序列化的性能(CPU資源占用);
  • 是否支援跨語言(異構系統的對接和開發語言切換)。

Netty預設提供了對Google Protobuf的支援,通過擴充Netty的編解碼接口,使用者可以實作其它的高性能序列化架構,例如Thrift的壓縮二進制編解碼架構。

下面我們一起看下不同序列化&反序列化架構序列化後的位元組數組對比:

Netty高性能之道

從上圖可以看出,Protobuf序列化後的碼流隻有Java序列化的1/4左右。正是由于Java原生序列化性能表現太差,才催生出了各種高性能的開源序列化技術和架構(性能差隻是其中的一個原因,還有跨語言、IDL定義等其它因素)。了解更詳細的測試結果

  • SO_RCVBUF和SO_SNDBUF:通常建議值為128K或者256K;
  • SO_TCPNODELAY:NAGLE算法通過将緩沖區内的小封包自動相連,組成較大的封包,阻止大量小封包的發送阻塞網絡,進而提高網絡應用效率。但是對于時延敏感的應用場景需要關閉該優化算法;
  • 軟中斷:如果Linux核心版本支援RPS(2.6.35以上版本),開啟RPS後可以實作軟中斷,提升網絡吞吐量。RPS根據資料包的源位址,目的位址以及目的和源端口,計算出一個hash值,然後根據這個hash值來選擇軟中斷運作的cpu,從上層來看,也就是說将每個連接配接和cpu綁定,并通過這個hash值,來均衡軟中斷在多個cpu上,提升網絡并行處理性能。

繼續閱讀