Netty原理剖析

1. Netty簡介

Netty是一個高性能、異步事件驅動的NIO框架,基於JAVA NIO提供的API實現。它提供了對TCP、UDP和文件傳輸的支持,做爲一個異步NIO框架,Netty的全部IO操做都是異步非阻塞的,經過Future-Listener機制,用戶能夠方便的主動獲取或者經過通知機制得到IO操做結果。 做爲當前最流行的NIO框架,Netty在互聯網領域、大數據分佈式計算領域、遊戲行業、通訊行業等得到了普遍的應用,一些業界著名的開源組件也基於Netty的NIO框架構建。react

2. Netty線程模型

在JAVA NIO方面Selector給Reactor模式提供了基礎,Netty結合Selector和Reactor模式設計了高效的線程模型。先來看下Reactor模式:編程

2.1 Reactor模式安全

Wikipedia這麼解釋Reactor模型:「The reactor design pattern is an event handling pattern for handling service requests delivered concurrently by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to associated request handlers.」。首先Reactor模式首先是事件驅動的,有一個或者多個併發輸入源,有一個Server Handler和多個Request Handlers,這個Service Handler會同步的將輸入的請求多路複用的分發給相應的Request Handler。能夠以下圖所示:服務器

這裏寫圖片描述

從結構上有點相似生產者和消費者模型,即一個或多個生產者將事件放入一個Queue中,而一個或者多個消費者主動的從這個隊列中poll事件來處理;而Reactor模式則沒有Queue來作緩衝,每當一個事件輸入到Service Handler以後,該Service Handler會主動根據不一樣的Evnent類型將其分發給對應的Request Handler來處理。網絡

2.2 Reator模式的實現多線程

關於Java NIO 構造Reator模式,Doug lea在《Scalable IO in Java》中給了很好的闡述,這裏截取PPT對Reator模式的實現進行說明架構

1.第一種實現模型以下:
這裏寫圖片描述併發

這是最簡單的Reactor單線程模型,因爲Reactor模式使用的是異步非阻塞IO,全部的IO操做都不會被阻塞,理論上一個線程能夠獨立處理全部的IO操做。這時Reactor線程是個多面手,負責多路分離套接字,Accept新鏈接,並分發請求處處理鏈中。框架

對於一些小容量應用場景,可使用到單線程模型。但對於高負載,大併發的應用卻不合適,主要緣由以下:異步

  1. 當一個NIO線程同時處理成百上千的鏈路,性能上沒法支撐,即便NIO線程的CPU負荷達到100%,也沒法徹底處理消息
  2. 當NIO線程負載太重後,處理速度會變慢,會致使大量客戶端鏈接超時,超時以後每每會重發,更加劇了NIO線程的負載。
  3. 可靠性低,一個線程意外死循環,會致使整個通訊系統不可用

爲了解決這些問題,出現了Reactor多線程模型。

2.Reactor多線程模型:
這裏寫圖片描述

相比上一種模式,該模型在處理鏈部分採用了多線程(線程池)。

在絕大多數場景下,該模型都能知足性能需求。可是,在一些特殊的應用場景下,如服務器會對客戶端的握手消息進行安全認證。這類場景下,單獨的一個Acceptor線程可能會存在性能不足的問題。爲了解決這些問題,產生了第三種Reactor線程模型

3.Reactor主從模型
這裏寫圖片描述

該模型相比第二種模型,是將Reactor分紅兩部分,mainReactor負責監聽server socket,accept新鏈接;並將創建的socket分派給subReactor。subReactor負責多路分離已鏈接的socket,讀寫網絡數據,對業務處理功能,其扔給worker線程池完成。一般,subReactor個數上可與CPU個數等同。

2.3 Netty模型

2.2中說完了Reactor的三種模型,那麼Netty是哪種呢?其實Netty的線程模型是Reactor模型的變種,那就是去掉線程池的第三種形式的變種,這也是Netty NIO的默認模式。Netty中Reactor模式的參與者主要有下面一些組件:

  1. Selector
  2. EventLoopGroup/EventLoop
  3. ChannelPipeline

Selector即爲NIO中提供的SelectableChannel多路複用器,充當着demultiplexer的角色,這裏再也不贅述;下面對另外兩種功能和其在Netty之Reactor模式中扮演的角色進行介紹。

3.EventLoopGroup/EventLoop

當系統在運行過程當中,若是頻繁的進行線程上下文切換,會帶來額外的性能損耗。多線程併發執行某個業務流程,業務開發者還須要時刻對線程安全保持警戒,哪些數據可能會被併發修改,如何保護?這不只下降了開發效率,也會帶來額外的性能損耗。

爲了解決上述問題,Netty採用了串行化設計理念,從消息的讀取、編碼以及後續Handler的執行,始終都由IO線程EventLoop負責,這就意外着整個流程不會進行線程上下文的切換,數據也不會面臨被併發修改的風險。這也解釋了爲何Netty線程模型去掉了Reactor主從模型中線程池。

EventLoopGroup是一組EventLoop的抽象,EventLoopGroup提供next接口,能夠總一組EventLoop裏面按照必定規則獲取其中一個EventLoop來處理任務,對於EventLoopGroup這裏須要瞭解的是在Netty中,在Netty服務器編程中咱們須要BossEventLoopGroup和WorkerEventLoopGroup兩個EventLoopGroup來進行工做。一般一個服務端口即一個ServerSocketChannel對應一個Selector和一個EventLoop線程,也就是說BossEventLoopGroup的線程數參數爲1。BossEventLoop負責接收客戶端的鏈接並將SocketChannel交給WorkerEventLoopGroup來進行IO處理。

EventLoop的實現充當Reactor模式中的分發(Dispatcher)的角色。

4.ChannelPipeline

ChannelPipeline實際上是擔任着Reactor模式中的請求處理器這個角色。

ChannelPipeline的默認實現是DefaultChannelPipeline,DefaultChannelPipeline自己維護着一個用戶不可見的tail和head的ChannelHandler,他們分別位於鏈表隊列的頭部和尾部。tail在更上層的部分,而head在靠近網絡層的方向。在Netty中關於ChannelHandler有兩個重要的接口,ChannelInBoundHandler和ChannelOutBoundHandler。inbound能夠理解爲網絡數據從外部流向系統內部,而outbound能夠理解爲網絡數據從系統內部流向系統外部。用戶實現的ChannelHandler能夠根據須要實現其中一個或多個接口,將其放入Pipeline中的鏈表隊列中,ChannelPipeline會根據不一樣的IO事件類型來找到相應的Handler來處理,同時鏈表隊列是責任鏈模式的一種變種,自上而下或自下而上全部知足事件關聯的Handler都會對事件進行處理。

ChannelInBoundHandler對從客戶端發往服務器的報文進行處理,通常用來執行半包/粘包,解碼,讀取數據,業務處理等;ChannelOutBoundHandler對從服務器發往客戶端的報文進行處理,通常用來進行編碼,發送報文到客戶端。

下圖是對ChannelPipeline執行過程的說明:
這裏寫圖片描述

關於Pipeline的更多知識可參考:淺談管道模型(Pipeline)

5.Buffer

Netty提供的通過擴展的Buffer相對NIO中的有個許多優點,做爲數據存取很是重要的一塊,咱們來看看Netty中的Buffer有什麼特色。

1.ByteBuf讀寫指針

  • 在ByteBuffer中,讀寫指針都是position,而在ByteBuf中,讀寫指針分別爲readerIndex和writerIndex,直觀看上去ByteBuffer僅用了一個指針就實現了兩個指針的功能,節省了變量,可是當對於ByteBuffer的讀寫狀態切換的時候必需要調用flip方法,而當下一次寫以前,必需要將Buffe中的內容讀完,再調用clear方法。每次讀以前調用flip,寫以前調用clear,這樣無疑給開發帶來了繁瑣的步驟,並且內容沒有讀完是不能寫的,這樣很是不靈活。相比之下咱們看看ByteBuf,讀的時候僅僅依賴readerIndex指針,寫的時候僅僅依賴writerIndex指針,不需每次讀寫以前調用對應的方法,並且沒有必須一次讀完的限制。

2.零拷貝

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

3.引用計數與池化技術

  • 在Netty中,每一個被申請的Buffer對於Netty來講均可能是很寶貴的資源,所以爲了得到對於內存的申請與回收更多的控制權,Netty本身根據引用計數法去實現了內存的管理。Netty對於Buffer的使用都是基於直接內存(DirectBuffer)實現的,大大提升I/O操做的效率,然而DirectBuffer和HeapBuffer相比之下除了I/O操做效率高以外還有一個天生的缺點,即對於DirectBuffer的申請相比HeapBuffer效率更低,所以Netty結合引用計數實現了PolledBuffer,即池化的用法,當引用計數等於0的時候,Netty將Buffer回收致池中,在下一次申請Buffer的沒某個時刻會被複用。

總結

Netty其實本質上就是Reactor模式的實現,Selector做爲多路複用器,EventLoop做爲轉發器,Pipeline做爲事件處理器。可是和通常的Reactor不一樣的是,Netty使用串行化實現,並在Pipeline中使用了責任鏈模式。

Netty中的buffer相對有NIO中的buffer又作了一些優化,大大提升了性能。

相關文章
相關標籤/搜索