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的“零拷貝”主要體現在如下三個方面:
- Netty的接收和發送ByteBuffer采用DIRECT BUFFERS,使用堆外直接內存進行Socket讀寫,不需要進行字節緩沖區的二次拷貝。如果使用傳統的堆內存(HEAP BUFFERS)進行Socket讀寫,JVM會將堆內存Buffer拷貝一份到直接內存中,然后才寫入Socket中。相比于堆外直接內存,消息在發送過程中多了一次緩沖區的內存拷貝。
- Netty提供了組合Buffer對象,可以聚合多個ByteBuffer對象,用戶可以像操作一個Buffer那樣方便的對組合Buffer進行操作,避免了傳統通過內存拷貝的方式將幾個小Buffer合并成一個大的Buffer。
- 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操作期間可以執行別的程序,在高并發情形下會更穩定和更高的吞吐量。
高效的并發編程
- volatile的大量、正確使用;
- CAS和原子類的廣泛使用;
- 線程安全容器的使用;
- 通過讀寫鎖提升并發性能。
高性能的序列化框架
影響序列化性能的關鍵因素總結如下:
- 序列化后的碼流大?。ňW絡帶寬的占用);
- 序列化&反序列化的性能(CPU資源占用);
- 是否支持跨語言(異構系統的對接和開發語言切換)。
Netty默認提供了對Google Protobuf的支持,通過擴展Netty的編解碼接口,用戶可以實現其它的高性能序列化框架,例如Thrift的壓縮二進制編解碼框架。
靈活的TCP參數配置能力
合理設置TCP參數在某些場景下對于性能的提升可以起到顯著的效果,例如SO_RCVBUF和SO_SNDBUF。如果設置不當,對性能的影響是非常大的。下面總結下對性能影響比較大的幾個配置項:
- SO_RCVBUF和SO_SNDBUF:通常建議值為128K或者256K;
- SO_TCPNODELAY:NAGLE算法通過將緩沖區內的小封包自動相連,組成較大的封包,阻止大量小封包的發送阻塞網絡,從而提高網絡應用效率。但是對于時延敏感的應用場景需要關閉該優化算法;
- 軟中斷:如果Linux內核版本支持RPS(2.6.35以上版本),開啟RPS后可以實現軟中斷,提升網絡吞吐量。RPS根據數據包的源地址,目的地址以及目的和源端口,計算出一個hash值,然后根據這個hash值來選擇軟中斷運行的cpu,從上層來看,也就是說將每個連接和cpu綁定,并通過這個 hash值,來均衡軟中斷在多個cpu上,提升網絡并行處理性能。
技術討論 & 疑問建議 & 個人博客
版權聲明: 本博客所有文章除特別聲明外,均采用 CC BY-NC-SA 3.0 許可協議,轉載請注明出處!