天天看點

dubbo通信架構 netty reactor模式

Dubbo使用什麼通信架構

Dubbo使用的是netty,主流通信架構包括netty,mina,Grizzly

以netty為例說明通信架構怎麼工作的

為什麼需要netty

現有系統是個單體的巨型應用,已經無法滿足海量的并發請求,拆分成多個“微服務”以後雖然增加了彈性,但也帶來了一個巨大的挑戰:服務之間互相調用的開銷。

比如說:原來使用者下一個訂單需要登入,浏覽産品詳情,加入購物車,支付,扣庫存等一系列操作,在單體應用的時候它們都在一台機器的同一個程序中,說白了就是子產品之間的函數調用,效率超級高。

現在好了,服務被安置到了不同的伺服器上,一個訂單流程,幾乎每個操作都要越網絡,都是遠端過程調用(RPC), 那執行時間、執行效率可遠遠比不上以前了。

第一版實作使用了HTTP協定,也就是說各個服務對外提供HTTP接口。 小明發現,HTTP協定雖然簡單明了,但是廢話太多,僅僅是給伺服器發個簡單的消息都會附帶一大堆無用資訊,許多頭資訊

想使用Java NIO來實作一個高性能的RPC架構,調用協定,資料的格式和次序都是自己定義的,現有的HTTP根本玩不轉,那使用Netty就是絕佳的選擇。

Reactor的有三種模型(下面有介紹),那麼Netty是哪一種呢?其實Netty的線程模型是Reactor模型的變種,那就是去掉線程池的第三種形式的變種,這也是Netty NIO的預設模式。Netty中Reactor模式的參與者主要有下面一些元件:

Selector

EventLoop

ChannelPipeline/channelandler

1. selector,會不斷在channel上檢測是否有該類型的事件發生,如果沒有,那麼主線程就會被阻塞,否則就會調用相應的事件處理函數即handler來處理。

2.EventLoop

Netty采用了串行化設計理念,從消息的讀取、編碼以及後續Handler的執行,始終都由IO線程EventLoop負責,這就意外着整個流程不會進行線程上下文的切換,資料也不會面臨被并發修改的風險。這也解釋了為什麼Netty線程模型去掉了Reactor主從模型中線程池。EventLoop的實作充當Reactor模式中的分發(Dispatcher)的角色,與channelHandler和channelPipeline

4.ChannelPipeline/channenelHandler

ChannelPipeline:其實是擔任着Reactor模式中的請求處理器(acceptor)這個角色。

channenelHandler:執行具體任務,read decode compute encode send的任務

為了優化對流的處理我們來看看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又做了一些優化,大大提高了性能。

scoket、accept和http連接配接的差別與關系

如果一個程式建立了一個socket,并讓其監聽80端口,其實是向TCP/IP協定棧聲明了其對80端口的占有。以後,所有目标是80端口的TCP資料包都會轉發給該程式(這裡的程式,因為使用的是Socket程式設計接口,是以首先由Socket層來處理)。所謂accept函數,其實抽象的是TCP的連接配接建立過程。accept函數傳回的新socket其實指代的是本次建立的連接配接,而一個連接配接是包括兩部分資訊的,一個是源IP和源端口,另一個是宿IP和宿端口。是以,accept可以産生多個不同的socket,而這些socket裡包含的宿IP和宿端口是不變的,變化的隻是源IP和源端口。這樣的話,這些socket宿端口就可以都是80,而Socket層還是能根據源/宿對來準确地分辨出IP包和socket的歸屬關系,進而完成對TCP/IP協定的操作封裝!

Reactor模式

reactor模式了解:将連接配接建立、IO、計算任務拆分,即連接配接建立放到mainReactor中,連接配接建立好後放到acceptor中,IO事件的響應放到subReactor中,具體的計算與處理放到線程池中。

網絡程式設計思路就是伺服器用一個while循環,不斷監聽端口是否有新的套接字連接配接,如果有,那麼就調用一個處理函數處理,類似:

while(true){ 

socket = accept(); 

handle(socket) 

這種方法的最大問題是無法并發,效率太低,如果目前的請求沒有處理完,那麼後面的請求隻能被阻塞,伺服器的吞吐量太低。

之後,想到了使用多線程,也就是很經典的connection per thread,每一個連接配接用一個線程處理,類似:

while(true){ 

socket = accept(); 

new thread(socket); 

tomcat伺服器的早期版本确實是這樣實作的。多線程的方式确實一定程度上極大地提高了伺服器的吞吐量,因為之前的請求在read阻塞以後,不會影響到後續的請求,因為他們在不同的線程中。這也是為什麼通常會講“一個線程隻能對應一個socket”的原因。最開始對這句話很不了解,線程中建立多個socket不行嗎?文法上确實可以,但是實際上沒有用,每一個socket都是阻塞的,是以在一個線程裡隻能處理一個socket,就算accept了多個也沒用,前一個socket被阻塞了,後面的是無法被執行到的。

缺點在于資源要求太高,系統中建立線程是需要比較高的系統資源的,如果連接配接數太高,系統無法承受,而且,線程的反複建立-銷毀也需要代價。

線程池本身可以緩解線程建立-銷毀的代價,這樣優化确實會好很多,不過還是存在一些問題的,就是線程的粒度太大。每一個線程把一次互動的事情全部做了,包括讀取和傳回,甚至連接配接,表面上似乎連接配接不線上程裡,但是如果線程不夠,有了新的連接配接,也無法得到處理,是以,目前的方案線程裡可以看成要做三件事,連接配接,讀取和寫入。

線程同步的粒度太大了,限制了吞吐量。應該把一次連接配接的操作分為更細的粒度或者過程,這些更細的粒度是更小的線程。整個線程池的數目會翻倍,但是線程更簡單,任務更加單一。這其實就是Reactor出現的原因,在Reactor中,這些被拆分的小線程或者子過程對應的是handler,每一種handler會出處理一種event。這裡會有一個全局的管理者selector,我們需要把channel注冊感興趣的事件,那麼這個selector就會不斷在channel上檢測是否有該類型的事件發生,如果沒有,那麼主線程就會被阻塞,否則就會調用相應的事件處理函數即handler來處理。典型的事件有連接配接,讀取和寫入,當然我們就需要為這些事件分别提供處理器,每一個處理器可以采用線程的方式實作。一個連接配接來了,顯示被讀取線程或者handler處理了,然後再執行寫入,那麼之前的讀取就可以被後面的請求複用,吞吐量就提高了。

幾乎所有的網絡連接配接都會經過讀請求内容——》解碼——》計算處理——》編碼回複——》回複的過程,Reactor模式的的演化過程如下:

dubbo通信架構 netty reactor模式

這種模型由于IO在阻塞時會一直等待,是以在使用者負載增加時,性能下降的非常快。

server導緻阻塞的原因:

1、serversocket的accept方法,阻塞等待client連接配接,直到client連接配接成功。

2、線程從socket inputstream讀入資料,會進入阻塞狀态,直到全部資料讀完。

3、線程向socket outputstream寫入資料,會阻塞直到全部資料寫完。

改進:采用基于事件驅動的設計,當有事件觸發時,才會調用處理器進行資料處理。

dubbo通信架構 netty reactor模式

Reactor:負責響應IO事件,當檢測到一個新的事件,将其發送給相應的Handler去處理。

Handler:負責處理非阻塞的行為,辨別系統管理的資源;同時将handler與事件綁定。

Reactor為單個線程,需要處理accept連接配接,同時發送請求到處理器中。

由于隻有單個線程,是以處理器中的業務需要能夠快速處理完。

改進:使用多線程處理業務邏輯。

dubbo通信架構 netty reactor模式

将處理器的執行放入線程池,多線程進行業務處理。但Reactor仍為單個線程。

繼續改進:對于多個CPU的機器,為充分利用系統資源,将Reactor拆分為兩部分。

Using Multiple Reactors

dubbo通信架構 netty reactor模式

mainReactor負責監聽連接配接,accept連接配接給subReactor處理,為什麼要單獨分一個Reactor來處理監聽呢?因為像TCP這樣需要經過3次握手才能建立連接配接,這個建立連接配接的過程也是要耗時間和資源的,單獨分一個Reactor來處理,可以提高性能。

引用自:https://www.cnblogs.com/doit8791/p/7461479.html

繼續閱讀