Netty爲什麼高性能

Netty作爲異步事件驅動的網絡,高性能之處主要來自於其I/O模型和線程處理模型,前者決定如何收發數據,後者決定如何處理數據

異步非阻塞通信

Netty的非阻塞I/O的實現關鍵是基於I/O複用模型,這裏用Selector對象表示:



Netty的IO線程NioEventLoop由於聚合了多路複用器Selector,可以同時併發處理成百上千個客戶端連接。當線程從某客戶端Socket通道進行讀寫數據時,若沒有數據可用時,該線程可以進行其他任務。線程通常將非阻塞 IO 的空閒時間用於在其他通道上執行 IO 操作,所以單獨的線程可以管理多個輸入和輸出通道。

由於讀寫操作都是非阻塞的,這就可以充分提升IO線程的運行效率,避免由於頻繁I/O阻塞導致的線程掛起,一個I/O線程可以併發處理N個客戶端連接和讀寫操作,這從根本上解決了傳統同步阻塞I/O一連接一線程模型,架構的性能、彈性伸縮能力和可靠性都得到了極大的提升。

零拷貝

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

  1. Netty的接收和發送ByteBuffer採用DIRECT BUFFERS,使用堆外直接內存進行Socket讀寫,不需要進行字節緩衝區的二次拷貝。如果使用傳統的堆內存(HEAP BUFFERS)進行Socket讀寫,JVM會將堆內存Buffer拷貝一份到直接內存中,然後才寫入Socket中。相比於堆外直接內存,消息在發送過程中多了一次緩衝區的內存拷貝。
  2. Netty提供了組合Buffer對象,可以聚合多個ByteBuffer對象,用戶可以像操作一個Buffer那樣方便的對組合Buffer進行操作,避免了傳統通過內存拷貝的方式將幾個小Buffer合併成一個大的Buffer。
  3. Netty的文件傳輸採用了transferTo方法,它可以直接將文件緩衝區的數據發送到目標Channel,避免了傳統通過循環write方式導致的內存拷貝問題。

基於buffer

傳統的I/O是面向字節流或字符流的,以流式的方式順序地從一個Stream 中讀取一個或多個字節, 因此也就不能隨意改變讀取指針的位置。
在NIO中, 拋棄了傳統的 I/O流, 而是引入了Channel和Buffer的概念. 在NIO中, 只能從Channel中讀取數據到Buffer中或將數據 Buffer 中寫入到 Channel。
基於buffer操作不像傳統IO的順序操作, NIO 中可以隨意地讀取任意位置的數據

內存池

隨着JVM虛擬機和JIT即時編譯技術的發展,對象的分配和回收是個非常輕量級的工作。但是對於緩衝區Buffer,情況卻稍有不同,特別是對於堆外直接內存的分配和回收,是一件耗時的操作。爲了儘量重用緩衝區,Netty提供了基於內存池的緩衝區重用機制(PooledByteBuf)。

無鎖化的串行設計理念

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

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

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

事件驅動模型

詳細請看Netty背後的事件驅動機制

Netty線程模型

詳細請看Netty線程模型

異步處理

異步的概念和同步相對。當一個異步過程調用發出後,調用者不能立刻得到結果。實際處理這個調用的部件在完成後,通過狀態、通知和回調來通知調用者。

Netty中的I/O操作是異步的,包括bind、write、connect等操作會簡單的返回一個ChannelFuture,調用者並不能立刻獲得結果,通過Future-Listener機制,用戶可以方便的主動獲取或者通過通知機制獲得IO操作結果。

當future對象剛剛創建時,處於非完成狀態,調用者可以通過返回的ChannelFuture來獲取操作執行的狀態,註冊監聽函數來執行完成後的操,常見有如下操作:

通過isDone方法來判斷當前操作是否完成
通過isSuccess方法來判斷已完成的當前操作是否成功
通過getCause方法來獲取已完成的當前操作失敗的原因
通過isCancelled方法來判斷已完成的當前操作是否被取消
通過addListener方法來註冊監聽器,當操作已完成(isDone方法返回完成),將會通知指定的監聽器;如果future對象已完成,則理解通知指定的監聽器

例如下面的的代碼中綁定端口是異步操作,當綁定操作處理完,將會調用相應的監聽器處理邏輯

serverBootstrap.bind(port).addListener(future -> {
       if (future.isSuccess()) {
           System.out.println(new Date() + ": 端口[" + port + "]綁定成功!");
       } else {
           System.err.println("端口[" + port + "]綁定失敗!");
       }
   });

相比傳統阻塞I/O,執行I/O操作後線程會被阻塞住, 直到操作完成;異步處理的好處是不會造成線程阻塞,線程在I/O操作期間可以執行別的程序,在高併發情形下會更穩定和更高的吞吐量。

高效的併發編程

  1. volatile的大量、正確使用;
  2. CAS和原子類的廣泛使用;
  3. 線程安全容器的使用;
  4. 通過讀寫鎖提升併發性能。

高性能的序列化框架

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

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

Netty默認提供了對Google Protobuf的支持,通過擴展Netty的編解碼接口,用戶可以實現其它的高性能序列化框架,例如Thrift的壓縮二進制編解碼框架。

靈活的TCP參數配置能力

合理設置TCP參數在某些場景下對於性能的提升可以起到顯著的效果,例如SO_RCVBUF和SO_SNDBUF。如果設置不當,對性能的影響是非常大的。下面總結下對性能影響比較大的幾個配置項:

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

參考:
Netty高性能之道
Netty的“零拷貝”


技術討論 & 疑問建議 & 個人博客

版權聲明: 本博客所有文章除特別聲明外,均採用 CC BY-NC-SA 3.0 許可協議,轉載請註明出處!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章