Netty高性能之道

  1. 異步非阻塞通信

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

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

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

  1. 零拷貝

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方式導致的內存拷貝問題。

  1. 內存池

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

  1. 高效的Reactor線程模型

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

1) Reactor單線程模型;

2) Reactor多線程模型;

3) 主從Reactor多線程模型

Reactor單線程模型:

由於Reactor模式使用的是異步非阻塞IO,所有的IO操作都不會導致阻塞,理論上一個線程可以獨立處理所有IO相關的操作。從架構層面看,一個NIO線程確實可以完成其承擔的職責。例如,通過Acceptor接收客戶端的TCP連接請求消息,鏈路建立成功之後,通過Dispatch將對應的 ByteBuffer派發到指定的Handler上進行消息解碼。用戶Handler可以通過NIO線程將消息發送給客戶端。

對於一些小容量應用場景,可以使用單線程模型。但是對於高負載、大併發的應用卻不合適,主要原因如下:

1) 一個NIO線程同時處理成百上千的鏈路,性能上無法支撐,即便NIO線程的CPU負荷達到100%,也無法滿足海量消息的編碼、解碼、讀取和發送;

2) 當NIO線程負載過重之後,處理速度將變慢,這會導致大量客戶端連接超時,超時之後往往會進行重發,這更加重了NIO線程的負載,最終會導致大量消息積壓和處理超時,NIO線程會成爲系統的性能瓶頸;

3) 可靠性問題:一旦NIO線程意外跑飛,或者進入死循環,會導致整個系統通信模塊不可用,不能接收和處理外部消息,造成節點故障。

Rector多線程模型:

Rector多線程模型與單線程模型最大的區別就是有一組NIO線程處理IO操作,Reactor多線程模型的特點:

1) 有專門一個NIO線程-Acceptor線程用於監聽服務端,接收客戶端的TCP連接請求;

2) 網絡IO操作-讀、寫等由一個NIO線程池負責,線程池可以採用標準的JDK線程池實現,它包含一個任務隊列和N個可用的線程,由這些NIO線程負責消息的讀取、解碼、編碼和發送;

3) 1個NIO線程可以同時處理N條鏈路,但是1個鏈路只對應1個NIO線程,防止發生併發操作問題。

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

主從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線程模型的支持提供了靈活的定製能力,所以可以滿足不同業務場景的性能訴求。

  1. 無鎖化的串行設計理念

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

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

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

  1. 高效的併發編程

Netty的高效併發編程主要體現在如下幾點:

1) volatile的大量、正確使用;

2) CAS和原子類的廣泛使用;

3) 線程安全容器的使用;

4) 通過讀寫鎖提升併發性能。

  1. 高性能的序列化框架

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

1) 序列化後的碼流大小(網絡帶寬的佔用);

2) 序列化&反序列化的性能(CPU資源佔用);

3) 是否支持跨語言(異構系統的對接和開發語言切換)。

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

  1. 靈活的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上,提升網絡並行處理性能。

這裏是程序員祕密聚集地,各位還在架構師的道路上掙扎的小夥伴們速來。“

加QQ羣:611481448

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