Java NIO與Netty

2020-09-24 15:01:25

在這裡插入圖片描述

Java 網路IO模型(BIO NIO AIO)

BIO同步並阻塞(傳統阻塞型):一個連線一個執行緒,使用者端有連線請求時伺服器端就需要啟動一個執行緒進行處理。執行緒開銷大。

在這裡插入圖片描述

NIO同步非阻塞:一個請求一個執行緒,但使用者端傳送的連線請求都會註冊到多路複用器上,多路複用器輪詢到連線有 I/O 請求時才啟動一個執行緒進行處理。

在這裡插入圖片描述

AIO非同步非阻塞:一個有效請求一個執行緒,使用者端的 I/O 請求都是由 OS 先完成了再通知伺服器應用去
啟動執行緒進行處理。
在這裡插入圖片描述BIO與NIO的區別
BIO 是面向流的,NIO 是面向緩衝區的或者面向塊的;
BIO 的各種流是阻塞的。而 NIO 是非阻塞的;
BIO的 Stream 是單向的,而 NIO 的 channel 是雙向的。

阻塞和非阻塞
阻塞和非阻塞指的是執行一個操作是等操作結束再返回,還是馬上返回。
比如餐館的服務員為使用者點菜,當有使用者點完菜後,服務員將選單給後臺廚師,此時有兩種方式:
第一種:就在出菜視窗等待,直到廚師炒完菜後將菜送到視窗,然後服務員再將菜送到使用者手中;
第二種:等一會再到視窗來問廚師,某個菜好了沒?如果沒有先處理其他事情,等會再去問一次;
第一種就是阻塞方式,第二種則是非阻塞的。
同步和非同步
同步和非同步又是另外一個概念,它是事件本身的一個屬性。還拿前面點菜為例,服務員直接跟廚師打交道,菜出來沒出來,服務員直接指導,但只有當廚師將菜送到服務員手上,這個過程才算正常完成,這就是同步的事件。同樣是點菜,有些餐館有專門的傳菜人員,當廚師炒好菜後,傳菜員將菜送到傳菜視窗,並通知服務員,這就變成非同步的了。其實非同步還可以分為兩種:帶通知的和不帶通知的。前面說的那種屬於帶通知的。有些傳菜員幹活可能主動性不是很夠,不會主動通知你,你就需要時不時的去關注一下狀態。這種就是不帶通知的非同步。

對於同步的事件,你只能以阻塞的方式去做。而對於非同步的事件,阻塞和非阻塞都是可以的。非阻塞又有兩種方式:主動查詢和被動接收訊息。被動不意味著一定不好,在這裡它恰恰是效率更高的,因為在主動查詢裡絕大部分的查詢是在做無用功。對於帶通知的非同步事件,兩者皆可。而對於不帶通知的,則只能用主動查詢。
  但是對於非阻塞和非同步的概念有點混淆,非阻塞只是意味著方法呼叫不阻塞,就是說作為服務員的你不用一直在視窗等,非阻塞的邏輯是"等可以讀(寫)了告訴你",但是完成讀(寫)工作的還是呼叫者(執行緒)服務員的你等菜到視窗了還是要你親自去拿。而非同步意味這你可以不用親自去做讀(寫)這件事,你的工作讓別人(別的執行緒)來做,你只需要發起呼叫,別人把工作做完以後,或許再通知你,它的邏輯是「我做完了 告訴/不告訴 你」,他和非阻塞的區別在於一個是"已經做完"另一個是"可以去做"。

阻塞IO/非阻塞IO
阻塞IO是:拷貝-知道所有資料拷貝到傳送緩衝區。
非阻塞IO是拷貝-返回-再拷貝-再返回
在這裡插入圖片描述read總是在接受快取區有資料的時候直接返回,而不是等到應用程式哥頂的資料充滿才返回。如果此時緩衝區是空的,那麼阻塞模式會等待,非阻塞則會返回-1並有EWOULDBLOCK或EAGAIN錯誤

和read不太一樣的是,在阻塞模式下,write只有在傳送緩衝區足矣容納應用程式的輸出位元組時才會返回。在非阻塞的模式下,能寫入多少則寫入多少,並返回實際寫入的位元組數
在這裡插入圖片描述

I/O多路複用機制

I/O多路複用就是通過一種機制,一個程序可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程式進行相應的讀寫操作。但select,poll,epoll本質上都是同步I/O,因為他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而非同步I/O則無需自己負責進行讀寫,非同步I/O的實現會負責把資料從核心拷貝到使用者空間。

select執行機制
當使用select函數的時候,先通知核心掛起程序,一旦一個或者多個IO事情發生,控制權將返回給應用程式,然後由應用程式進行IO處理。
select()的機制中提供一種fd_set的資料結構,實際上是一個long型別的陣列,每一個陣列元素都能與一開啟的檔案控制程式碼(不管是Socket控制程式碼,還是其他檔案或命名管道或裝置控制程式碼)建立聯絡,建立聯絡的工作由程式設計師完成,當呼叫select()時,由核心根據IO狀態修改fd_set的內容,由此來通知執行了select()的程序哪一Socket或檔案可讀。

從流程上來看,使用select函數進行IO請求和同步阻塞模型沒有太大的區別,甚至還多了新增監視socket,以及呼叫select函數的額外操作,效率更差。但是,使用select以後最大的優勢是使用者可以在一個執行緒內同時處理多個socket的IO請求。使用者可以註冊多個socket,然後不斷地呼叫select讀取被啟用的socket,即可達到在同一個執行緒內同時處理多個IO請求的目的。而在同步阻塞模型中,必須通過多執行緒的方式才能達到這個目的。

select機制的問題

  1. 每次呼叫select,都需要把fd_set集合從使用者態拷貝到核心態,如果fd_set集合很大時,那這個開銷也很大
  2. 同時每次呼叫select都需要在核心遍歷傳遞進來的所有fd_set,如果fd_set集合很大時,那這個開銷也很大
  3. 為了減少資料拷貝帶來的效能損壞,核心對被監控的fd_set集合大小做了限制,並且這個是通過宏控制的,大小不可改變(限制為1024)

Poll執行機制

鑑於select所支援的描述符有限,隨後提出poll解決這個問題

poll和select不同之處在於,在select中,檔案描述符個數隨著fd_set的實現而固定,而在poll函數中,我們可以通過控制pollfd陣列的大小來改變描述符的個數

poll的機制與select類似,與select在本質上沒有多大差別,管理多個描述符也是進行輪詢,根據描述符的狀態進行處理,但是poll沒有最大檔案描述符數量的限制。也就是說,poll只解決了上面的問題3,並沒有解決問題1,2的效能開銷問題。

poll改變了檔案描述符集合的描述方式,使用了連結串列結構而不是select的陣列結構,使得poll支援的檔案描述符集合限制遠大於select的1024。

Epoll執行機制
epoll 通過監控註冊的多個描述字,來進行 I/O 事件的分發處理。不同於 poll 的是,epoll 不僅提供了預設的 level-triggered(條件觸發)機制,還提供了效能更為強勁的edge triggered(邊緣觸發)機制

epoll在Linux2.6核心正式提出,是基於事件驅動的I/O方式,相對於select來說,epoll沒有描述符個數限制,使用一個檔案描述符管理多個描述符,將使用者關心的檔案描述符的事件存放到核心的一個事件表中,這樣在使用者空間和核心空間的copy只需一次。

當我們使用epoll_fd增加一個fd的時候,核心會為我們建立一個epitem範例,講這個範例作為紅黑樹的節點,隨後查詢的每一個fd是否有事件發生就是通過紅黑樹的epitem來操作

epoll維護一個連結串列來記錄就緒事件,核心會當每個檔案有事件發生的時候將自己登記到這個就緒列表,然後通過核心自身的檔案file-eventpoll之間的回撥和喚醒機制,減少對核心描述字的遍歷,大俗事件通知和檢測的效率

elect、poll、epoll 區別總結:
在這裡插入圖片描述

綜上,在選擇select,poll,epoll時要根據具體的使用場合以及這三種方式的自身特點。
1、表面上看epoll的效能最好,但是在連線數少並且連線都十分活躍的情況下,select和poll的效能可能比epoll好,畢竟epoll的通知機制需要很多函數回撥。
2、select低效是因為每次它都需要輪詢。但低效也是相對的,視情況而定,也可通過良好的設計改善。

NIO模型
BIO是基於位元組流或者字元流進行操作,而NIO基於channel通道和buffer緩衝區進行操作,資料總是從通道讀取到緩衝區中,或者從緩衝區寫入到通道中。Selector選擇器用於監聽多個通道的事件(比如:連線請求,資料到達等),因此就可以使用單個執行緒監聽多個使用者端通道。
在這裡插入圖片描述

NIO(JDK1.4提供的新API)

NIO三大核心部分:Channel(通道)、Buffer(緩衝區)、Selector(選擇器)

NIO面向緩衝區,或者面向 塊 程式設計的。資料讀取到一個它稍後處理的緩衝區,需要時可在緩衝區中前後移動,這就增加了處理過程中的靈活性,使用它可以提供非阻塞式的高伸縮性網路。

Java NIO的非阻塞模式(單執行緒處理多工),使一個執行緒從某通道傳送請求或者讀取資料,但是它僅能得到目前可用的資料,如果目前沒有資料可用時,就什麼都不會獲取,而不是保持執行緒阻塞,所以直至資料變的可以讀取之前,該執行緒可以繼續做其他事情。非阻塞寫也是如此,一個執行緒請求寫入一些資料到某通道,但不需要等待它完全寫入,這個執行緒可以去做其他事情。

NIO 的特點:事件驅動模型、單執行緒處理多工、非阻塞 I/O,I/O 讀寫不再阻塞,而是返
回 0、基於 block 的傳輸比基於流的傳輸更高效、更高階的 IO 函數 zero-copy、IO 多路複用大大提高了 Java 網路應用的可伸縮性和實用性。基於 Reactor 執行緒模型。
在 Reactor 模式中,事件分發器等待某個事件或者可應用或個操作的狀態發生,事件分發
器就把這個事件傳給事先註冊的事件處理常式或者回撥函數,由後者來做實際的讀寫操
作。如在 Reactor 中實現讀:註冊讀就緒事件和相應的事件處理器、事件分發器等待事
件、事件到來,啟用分發器,分發器呼叫事件對應的處理器、事件處理器完成實際的讀操
作,處理讀到的資料,註冊新的事件,然後返還控制權。

NIO的組成
在這裡插入圖片描述

Buffer:與 Channel 進行互動,資料是從 Channel 讀入緩衝區,從緩衝區寫入 Channel 中的DirectByteBuffer 可減少一次系統空間到使用者空間的拷貝。但 Buffer 建立和銷燬的成本更
高,不可控,通常會用記憶體池來提高效能。直接緩衝區主要分配給那些易受基礎系統的本
機 I/O 操作影響的大型、持久的緩衝區。如果資料量比較小的中小應用情況下,可以考慮
使用 heapBuffer,由 JVM 進行管理。

Channel:表示 IO 源與目標開啟的連線,是雙向的,但不能直接存取資料,只能與 Buffer
進行互動。通過原始碼可知,FileChannel 的 read 方法和 write 方法都導致資料複製了兩次!

Selector 可使一個單獨的執行緒管理多個 Channel,open 方法可建立 Selector,register 方法向多路複用器器註冊通道,可以監聽的事件型別:讀、寫、連線、accept。註冊事件後會產
生一個 SelectionKey:它表示 SelectableChannel 和 Selector 之間的註冊關係,wakeup 方
法:使尚未返回的第一個選擇操作立即返回,喚醒的原因是:註冊了新的 channel 或者事
件;channel 關閉,取消註冊;優先順序更高的事件觸發(如定時器事件),希望及時處理。

Selector 在 Linux 的實現類是 EPollSelectorImpl,委託給 EPollArrayWrapper 實現,其中三個native 方法是對 epoll 的封裝,而 EPollSelectorImpl. implRegister 方法,通過呼叫 epoll_ctl向 epoll 範例中註冊事件,還將註冊的檔案描述符(fd)與 SelectionKey 的對應關係新增到fdToKey 中,這個 map 維護了檔案描述符與 SelectionKey 的對映。

fdToKey 有時會變得非常大,因為註冊到 Selector 上的 Channel 非常多(百萬連線);過期或失效的 Channel 沒有及時關閉。fdToKey 總是序列讀取的,而讀取是在 select 方法中進行的,該方法是非執行緒安全的。

Pipe:兩個執行緒之間的單向資料連線,資料會被寫到 sink 通道,從 source 通道讀取

NIO 的伺服器端建立過程:Selector.open():開啟一個 Selector;ServerSocketChannel.open():建立伺服器端的 Channel;bind():繫結到某個埠上。並設定非阻塞模式;register():註冊Channel 和關注的事件到 Selector 上;select()輪詢拿到已經就緒的事件

NIO與零拷貝

零拷貝是伺服器網路程式設計的關鍵,任何效能優化都離不開。在 Java 程式設計師的世界,常用的零拷貝有 mmap 和 sendFile。

每一次的使用者態和核心態的上下文切換就相當於一次80中斷(耗時)
在這裡插入圖片描述

傳統IO模型
上半部分表示使用者態和核心態的上下文切換。下半部分表示資料複製操作
在這裡插入圖片描述
步驟:
1.read 呼叫導致使用者態到核心態的一次變化,同時,第一次複製開始:DMA(Direct Memory Access,直接記憶體存取,即不使用 CPU 拷貝資料到記憶體,而是 DMA 引擎傳輸資料到記憶體,用於解放 CPU) 引擎從磁碟讀取 index.html 檔案,並將資料放入到核心緩衝區。
2.發生第二次資料拷貝,即:將核心緩衝區的資料拷貝到使用者緩衝區,同時,發生了一次用核心態到使用者態的上下文切換。
3.發生第三次資料拷貝,我們呼叫 write 方法,系統將使用者緩衝區的資料拷貝到 Socket 緩衝區。此時,又發生了一次使用者態到核心態的上下文切換。
4.第四次拷貝,資料非同步的從 Socket 緩衝區,使用 DMA 引擎拷貝到網路協定引擎。這一段,不需要進行上下文切換。
5.write 方法返回,再次從核心態切換到使用者態。

mmap 優化
mmap 通過記憶體對映,將檔案對映到核心緩衝區,同時,使用者空間可以共用核心空間的資料。這樣,在進行網路傳輸時,就可以減少核心空間到使用者控制元件的拷貝次數。如下圖:
在這裡插入圖片描述只需要從核心緩衝區拷貝到 Socket 緩衝區即可,這將減少一次記憶體拷貝(從 4 次變成了 3 次),但不減少上下文切換次數。

sendFile
資料根本不經過使用者態,直接從核心緩衝區進入到 Socket Buffer,同時,由於和使用者態完全無關,就減少了一次上下文切換。
在這裡插入圖片描述如上圖,我們進行 sendFile 系統呼叫時,資料被 DMA 引擎從檔案複製到核心緩衝區,然後呼叫,然後掉一共 write 方法時,從核心緩衝區進入到 Socket,這時,是沒有上下文切換的,因為在一個使用者空間。最後,資料從 Socket 緩衝區進入到協定棧。
此時,資料經過了 3 次拷貝,3 次上下文切換。

那麼,還能不能再繼續優化呢? 例如直接從核心緩衝區拷貝到網路協定棧
實際上,Linux 在 2.4 版本中,做了一些修改,避免了從核心緩衝區拷貝到 Socket buffer 的操作,直接拷貝到協定棧,從而再一次減少了資料拷貝。具體如下圖:
在這裡插入圖片描述
現在,進入到網路協定棧,只需 2 次拷貝:第一次使用 DMA 引擎從檔案拷貝到核心緩衝區,第二次從核心緩衝區將資料拷貝到網路協定棧;核心快取區只會拷貝一些 offset 和 length 資訊到 SocketBuffer,基本無消耗。

不是說零拷貝嗎?為什麼還是要 2 次拷貝?
答:首先我們說零拷貝,是從作業系統的角度來說的。因為核心緩衝區之間,沒有資料是重複的(只有 kernel buffer 有一份資料,sendFile 2.1 版本實際上有 2 份資料,算不上零拷貝)。例如我們剛開始的例子,核心快取區和 Socket 緩衝區的資料就是重複的。

而零拷貝不僅僅帶來更少的資料複製,還能帶來其他的效能優勢,例如更少的上下文切換,更少的 CPU 快取偽共用以及無 CPU 校驗和計算。

mmap 和 sendFile 的區別
1.mmap 適合小資料量讀寫,sendFile 適合大檔案傳輸。
2.mmap 需要 4 次上下文切換,3 次資料拷貝;sendFile 需要 3 次上下文切換,最少 2 次資料拷貝。
3.sendFile 可以利用 DMA 方式,減少 CPU 拷貝,mmap 則不能(必須從核心拷貝到 Socket 緩衝區)。
在這個選擇上:rocketMQ 在消費訊息時,使用了 mmap。kafka 使用了 sendFile。

Netty 的零拷貝
零拷貝的目的是為了減少IO流程中不必要的拷貝,以及減少使用者程序地址空間和核心地址空間之間因為上下文切換而帶來的開銷。由於虛擬機器器不能直接操作核心,因此它的實現需要作業系統OS的支援,也就是需要kernel核心暴漏API

  1. Direct Buffers:Netty的接收和傳送ByteBuffer採用直接緩衝區(Direct Buffer)實現零拷貝,直接在記憶體區域分配空間,避免了讀寫資料的二次記憶體拷貝,這就實現了讀寫Socket的零拷貝。

如果使用傳統的堆記憶體緩衝區(Heap Buffer)進行Socket讀寫,JVM會將堆記憶體Buffer拷貝到直接記憶體中,然後才寫入Socket中。相比堆外直接記憶體,訊息在傳送過程中多了一次緩衝區的記憶體拷貝。

  1. CompositeByteBuf:它可以將多個ByteBuf封裝成ByteBuf,對外提供統一封裝後的ByteBuf介面。CompositeByteBuf並沒有真正將多個Buffer組合起來,而是儲存了它們的參照,從而避免了資料的拷貝,實現了零拷貝

傳統的ByteBuffer,如果需要將兩個ByteBuffer中的資料組合到一起,我們需要首先建立一個size=size1+size2大小的新的陣列,然後將兩個陣列中的資料拷貝到新的陣列中。但是使用Netty提供的組合ByteBuf,就可以避免這樣的操作。

  1. Netty的檔案傳輸類DefaultFileRegion通過呼叫FileChannel.transferTo()方法實現零拷貝,檔案緩衝區的資料會直接傳送給目標Channel。底層呼叫Linux作業系統中的sendfile()實現的,資料從檔案由DMA引擎拷貝到核心read緩衝區,;DMA從核心read緩衝區將資料拷貝到網路卡介面(硬體)的緩衝區,由網路卡進行網路傳輸。

Netty

Netty是一個非同步的、基於事件驅動的網路應用框架,用以快速開發高效能、高可靠性的網路IO程式。
Netty主要針對在TCP協定下,面向Clinets端的高並行應用,或者Peer-to-Peer場景下的大量資料持續傳輸的應用。
Netty本質是一個NIO框架,適用於伺服器通訊相關的多種應用場景。
在這裡插入圖片描述
Netty 的特點

  • 一個高效能、非同步事件驅動的 NIO 框架,它提供了對 TCP、UDP 和檔案傳輸的支援
  • 使用更高效的 socket 底層,對 epoll 空輪詢引起的 cpu 佔用飆升在內部進行了處理,避免了直接使用 NIO 的陷阱,簡化了 NIO 的處理方式。
  • 採用多種 decoder/encoder 支援,對 TCP 粘包/分包進行自動化處理
  • 可使用接受/處理執行緒池,提高連線效率,對重連、心跳檢測的簡單支援
  • 可設定 IO 執行緒數、TCP 引數, TCP 接收和傳送緩衝區使用直接記憶體代替堆記憶體,通過記憶體池的方式迴圈利用 ByteBuf
  • 通過參照計數器及時申請釋放不再參照的物件,降低了 GC 頻率
  • 使用單執行緒序列化的方式,高效的 Reactor 執行緒模型
  • 大量使用了 volitale、使用了 CAS 和原子類、執行緒安全類的使用、讀寫鎖的使用

Netty 執行緒模型

傳統阻塞IO服務模型
特點:1)採用阻塞IO模式獲取輸入資料
2)每個連線都需要獨立的執行緒完成資料的輸入,業務處理,資料返回
問題:1)當並行數很大,就會建立大量的執行緒,佔用很大系統資源
2)連線建立後,如果當前執行緒暫時沒有資料可讀,該執行緒會阻塞在read操作,造成執行緒資源浪費
在這裡插入圖片描述
Reactor模式:
針對傳統阻塞IO模型的2個缺點的解決方案:
1)基於IO複用模型:多個連線共用一個執行緒,應用程式只需要在一個阻塞物件等待,無需阻塞等待所有連線。當某個連線有新資料需處理時,作業系統通知應用程式,執行緒從阻塞狀態返回,開始進行業務處理。
2)基於執行緒池複用執行緒資源:不必再為每個連線建立執行緒,將連線完成後的業務處理任務分配給執行緒進行處理,一個連線可以處理多個連線的業務。

在這裡插入圖片描述1)Reactor模式,通過一個或者多個輸入同時傳遞給服務處理器的模式(基於事件驅動)
2)伺服器端程式處理傳入的多個請求,並將它們同步分派到相應的處理執行緒,因此Reactor模式也叫dispatcher模式(分發者模式)
3)Reactor模式使用IO複用監聽事件,收到事件後,分發給某個執行緒(程序),這是網路伺服器高並行處理關鍵

Reactor模式組成
1)Reactor:Reactor在一個單獨執行緒中執行,負責監聽和分發事件,分發給適當的處理程式來對IO事件做出反應
2)Handlers:處理程式執行IO事件要完成的實際事件,Reactor通過適度排程適當處理程式來響應IO事件,處理程式執行非阻塞操作。

Reactor模式分類

  • 單Reactor單執行緒

所有 I/O 操作都由一個執行緒完成,即多路複用、事件分發和處理都是在一個Reactor 執行緒上完成的。既要接收使用者端的連線請求,向伺服器端發起連線,又要傳送/讀取請求或應答/響應訊息。一個 NIO 執行緒同時處理成百上千的鏈路,效能上無法支撐,速度慢,若執行緒進入死迴圈,整個程式不可用,對於高負載、大並行的應用場景不合適
在這裡插入圖片描述
1)Select是前面IO複用模型介紹的標準網路程式設計API,可以實現應用程式通過一個阻塞物件監聽多路連線請求
2)Reactor物件是通過Select監控使用者端請求事件,收到事件後通過Dispatch分發
3)如果是建立連線請求事件,則由Acceptor通過Accept處理連線請求,然後建立一個Handler物件處理連線完成後的後續業務處理
4)如果不是建立連線請求事件,則Reactor會分發呼叫連線對應的Handler來響應
5)Handler會完成Read->業務處理->Send的完整業務流程

  • 單Reactor多執行緒

有一個 NIO 執行緒(Acceptor) 只負責監聽伺服器端,接收使用者端的 TCP 連線請求;NIO 執行緒池負責網路 IO 的操作,即訊息的讀取、解碼、編碼和傳送;1 個 NIO 執行緒可以同時處理 N 條鏈路,但是 1 個鏈路只對應 1 個 NIO 執行緒,這是為了防止發生並行操作問題。但在並行百萬使用者端連線或需要安全認證時,一個 Acceptor 執行緒可能會存在效能不足問題。
在這裡插入圖片描述1)Select是前面IO複用模型介紹的標準網路程式設計API,可以實現應用程式通過一個阻塞物件監聽多路連線請求
2)Reactor物件是通過Select監控使用者端請求事件,收到事件後通過Dispatch分發
3)如果是建立連線請求事件,則由Acceptor通過Accept處理連線請求,然後建立一個Handler物件處理連線完成後的後續業務處理
4)Handler只負責響應事件,不做具體的業務處理,通過read讀取資料後,會分發給後面的worker執行緒池的某個執行緒處理業務
5)worker執行緒池會分配獨立執行緒完成真正的業務,並將結果返回給handler
6)handler收到響應後,通過send將結果返回給client

  • 主從Reactor多執行緒

Acceptor 執行緒用於繫結監聽埠,接收使用者端連線,將 SocketChannel從主執行緒池的 Reactor 執行緒的多路複用器上移除,重新註冊到 Sub 執行緒池的執行緒上,用於處理 I/O 的讀寫等操作,從而保證 mainReactor 只負責接入認證、握手等操作;
在這裡插入圖片描述1)Reactor主執行緒MainReactor物件通過select監聽連線事件,收到事件後,通過Acceptor處理連線事件
2)當Acceptor處理連線事件後,MainReactor將連線分配給SubReactor
3)SubReactor將連線加入到連線佇列進行監聽,並建立handler進行各種事件的處理
4)當有新事件發生時,SubReactor就會呼叫對應的handler處理
5)Handler通過read讀取資料後,會分發給後面的worker執行緒處理
6)worker執行緒池會分配獨立執行緒完成真正的業務,並將返回結果
7)handler收到響應後,通過send將結果返回給client
8)Reactor主執行緒可以對應多個Reactor子執行緒,即MainReactor可以關聯多個SubReactor

Netty模型

Netty執行緒模式主要基於主從Reactor多執行緒做了一定的改進,主從Reactor多執行緒模型有多個Reactor

Netty 通過 Reactor 模型基於多路複用器接收並處理使用者請求,內部實現了兩個執行緒池,
boss 執行緒池和 work 執行緒池,其中 boss 執行緒池的執行緒負責處理請求的 accept 事件,當接收到 accept 事件的請求時,把對應的 socket 封裝到一個 NioSocketChannel 中,並交給 work執行緒池,其中 work 執行緒池負責請求的 read 和 write 事件,由對應的 Handler 處理。
在這裡插入圖片描述

1)Netty抽象出兩組執行緒池BossGroup專門負責接收使用者端連線,WorkerGroup專門負責網路的讀寫
2)BossGroup和WorkerGroup型別都是NioEventLoopGroup
3)NioEventLoopGroup相當於一個事件迴圈組,這個組中含有多個事件迴圈,每一個迴圈是NioEventLoop
4)NioEventLoop表示一個不斷迴圈的執行處理任務的執行緒,每個NioEventLoop都有一個selector,用於監聽繫結在其上的socket的網路通訊
5)NioEventLoopGroup可以有多個執行緒,即可以含有多個NioEventLoop
6)每個Boos NioEventLoop迴圈執行步驟有3:

  • 輪詢accept事件
  • 處理accept事件,與client建立連線,生成NioScoketChannel,並將其註冊到某個worker NIOEventLoop上的selector
  • 處理任務佇列的任務,即runAllTasks

7)每個Worker NioEventLoop迴圈執行步驟:

  • 輪詢read、write事件
  • 處理IO事件,即read、write事件,在對應NioScoketChannel處理
  • 處理任務佇列的任務,即runAllTasks

8)每個Worker NioEventLoop處理任務時,會使用pipeline(管道),pipeline包含了channel,即通過pipeline可以獲取對應通道,管道中維護了很多的處理器

Selector BUG:若 Selector 的輪詢結果為空,也沒有 wakeup 或新訊息處理,則發生空輪詢,CPU 使用率 100%,

Netty 的解決辦法:對 Selector 的 select 操作週期進行統計,每完成一次空的 select 操作進行一次計數,若在某個週期內連續發生 N 次空輪詢,則觸發了 epoll 死迴圈 bug。重建
Selector,判斷是否是其他執行緒發起的重建請求,若不是則將原 SocketChannel 從舊的
Selector 上去除註冊,重新註冊到新的 Selector 上,並將原來的 Selector 關閉。

非同步模型
1)與同步相對,當一個非同步過程呼叫發出後,呼叫者不能立刻得到結果,實際上處理這個呼叫的元件在完成後,通過狀態、通知和回撥來通知呼叫者。
2)Netty中的IO操作是非同步的,包括bind、write、connect 等操作會簡單返回一個channelFuture
3)呼叫者不能立刻獲得結果,而是通過Future-Listener機制,使用者可以方便的主動獲取或者通過通知機制獲得IO操作的結果
4)Netty的非同步模型是建立在future和callback的之上的,callback是回撥,重點說future,它的核心思想是:假設一個方法fun,計算過程可能非常耗時,等待fun返回顯然不合適。那麼可以在呼叫fun的時候,立馬返回一個future,後續可以通過future去監控方法fun的處理過程(即Future-Listener機制)

Netty 的高效能

  • 心跳,對伺服器端:會定時清除閒置對談 inactive(netty5),對使用者端:用來檢測對談是否斷開,是否重來,檢測網路延遲,其中 idleStateHandler 類 用來檢測對談狀態
    在這裡插入圖片描述

  • 序列無鎖化設計,即訊息的處理儘可能在同一個執行緒內完成,期間不進行執行緒切換,這樣就避免了多執行緒競爭和同步鎖。表面上看,序列化設計似乎 CPU 利用率不高,並行程度不夠。但是,通過調整 NIO 執行緒池的執行緒引數,可以同時啟動多個序列化的執行緒並行執行,這種區域性無鎖化的序列執行緒設計相比一個佇列-多個工作執行緒模型效能更優。

  • 可靠性,鏈路有效性檢測:鏈路空閒檢測機制,讀/寫空閒超時機制;記憶體保護機制:通過記憶體池重用 ByteBuf;ByteBuf 的解碼保護;優雅停機:不再接收新訊息、退出前的預處理操作、資源的釋放操作。

  • Netty 安全性:支援的安全協定:SSL V2 和 V3,TLS,SSL 單向認證、雙向認證和第三方 CA認證。

  • 高效並行程式設計的體現:volatile 的大量、正確使用;CAS 和原子類的廣泛使用;執行緒安全容器的使用;通過讀寫鎖提升並行效能。IO 通訊效能三原則:傳輸(AIO)、協定(Http)、執行緒(主從多執行緒)

  • 流量整型的作用(變壓器):防止由於上下游網元效能不均衡導致下游網元被壓垮,業務流中斷;防止由於通訊模組接受訊息過快,後端業務執行緒處理不及時導致撐死問題。

  • TCP 引數設定:SO_RCVBUF 和 SO_SNDBUF:通常建議值為 128K 或者 256K;
    SO_TCPNODELAY:NAGLE 演演算法通過將緩衝區內的小封包自動相連,組成較大的封包,阻止大量小封包的傳送阻塞網路,從而提高網路應用效率。但是對於時延敏感的應用場景需要關閉該優化演演算法;

序列化

瞭解哪幾種序列化協定?
序列化(編碼)是將物件序列化為二進位制形式(位元組陣列),主要用於網路傳輸、資料持久
化等;而反序列化(解碼)則是將從網路、磁碟等讀取的位元組陣列還原成原始物件,主要
用於網路傳輸物件的解碼,以便完成遠端呼叫。

影響序列化效能的關鍵因素:序列化後的碼流大小(網路頻寬的佔用)、序列化的效能
(CPU 資源佔用);是否支援跨語言(異構系統的對接和開發語言切換)。

Java 預設提供的序列化:無法跨語言、序列化後的碼流太大、序列化的效能差

XML,優點:人機可讀性好,可指定元素或特性的名稱。缺點:序列化資料只包含資料本
身以及類的結構,不包括型別標識和程式集資訊;只能序列化公共屬性和欄位;不能序列
化方法;檔案龐大,檔案格式複雜,傳輸佔頻寬。適用場景:當做組態檔儲存資料,實
時資料轉換。

JSON,是一種輕量級的資料交換格式,優點:相容性高、資料格式比較簡單,易於讀寫、
序列化後資料較小,可延伸性好,相容性好、與 XML 相比,其協定比較簡單,解析速度比
較快。缺點:資料的描述性比 XML 差、不適合效能要求為 ms 級別的情況、額外空間開銷
比較大。適用場景(可替代XML):跨防火牆存取、可調式性要求高、基於 Web

browser 的 Ajax 請求、傳輸資料量相對小,實時性要求相對低(例如秒級別)的服務。

Fastjson,採用一種「假定有序快速匹配」的演演算法。優點:介面簡單易用、目前 java 語言中
最快的 json 庫。缺點:過於注重快,而偏離了「標準」及功能性、程式碼品質不高,檔案不
全。適用場景:協定互動、Web 輸出、Android 使用者端

Thrift,不僅是序列化協定,還是一個 RPC 框架。優點:序列化後的體積小, 速度快、支援
多種語言和豐富的資料型別、對於資料欄位的增刪具有較強的相容性、支援二進位制壓縮編
碼。缺點:使用者較少、跨防火牆存取時,不安全、不具有可讀性,偵錯程式碼時相對困
難、不能與其他傳輸層協定共同使用(例如 HTTP)、無法支援向持久層直接讀寫資料,即
不適合做資料持久化序列化協定。適用場景:分散式系統的 RPC 解決方案

Avro,Hadoop 的一個子專案,解決了 JSON 的冗長和沒有 IDL 的問題。優點:支援豐富的資料型別、簡單的動態語言結合功能、具有自我描述屬性、提高了資料解析速度、快速可
壓縮的二進位制資料形式、可以實現遠端過程呼叫 RPC、支援跨程式語言實現。缺點:對於
習慣於靜態型別語言的使用者不直觀。適用場景:在 Hadoop 中做 Hive、Pig 和 MapReduce
的持久化資料格式。

Protobuf,將資料結構以.proto 檔案進行描述,通過程式碼生成工具可以生成對應資料結構的
POJO 物件和 Protobuf 相關的方法和屬性。優點:序列化後碼流小,效能高、結構化資料儲存格式(XML JSON 等)、通過標識欄位的順序,可以實現協定的前向相容、結構化的檔案更容易管理和維護。缺點:需要依賴於工具生成程式碼、支援的語言相對較少,官方只支援Java 、C++ 、python。適用場景:對效能要求高的 RPC 呼叫、具有良好的跨防火牆的存取屬性、適合應用層物件的持久化

其它
protostuff 基於 protobuf 協定,但不需要設定 proto 檔案,直接導包即可
Jboss marshaling 可以直接序列化 java 類, 無須實 java.io.Serializable 介面
Message pack 一個高效的二進位制序列化格式
Hessian 採用二進位制協定的輕量級 remoting onhttp 工具
kryo 基於 protobuf 協定,只支援 java 語言,需要註冊(Registration),然後序列化(Output),反序列化(Input)

如何選擇序列化協定?

具體場景
對於公司間的系統呼叫,如果效能要求在 100ms 以上的服務,基於 XML 的 SOAP 協定是一個值得考慮的方案。

基於 Web browser 的 Ajax,以及 Mobile app 與伺服器端之間的通訊,JSON 協定是首選。對於效能要求不太高,或者以動態型別語言為主,或者傳輸資料載荷很小的的運用場景,JSON也是非常不錯的選擇。

對於偵錯環境比較惡劣的場景,採用 JSON 或 XML 能夠極大的提高偵錯效率,降低系統開
發成本。

當對效能和簡潔性有極高要求的場景,Protobuf,Thrift,Avro 之間具有一定的競爭關係。

對於 T 級別的資料的持久化應用場景,Protobuf 和 Avro 是首要選擇。如果持久化後的資料
儲存在 hadoop 子專案裡,Avro 會是更好的選擇。

對於持久層非 Hadoop 專案,以靜態型別語言為主的應用場景,Protobuf 會更符合靜態類
型語言工程師的開發習慣。由於 Avro 的設計理念偏向於動態型別語言,對於動態語言為主
的應用場景,Avro 是更好的選擇。

如果需要提供一個完整的 RPC 解決方案,Thrift 是一個好的選擇。

如果序列化之後需要支援不同的傳輸層協定,或者需要跨防火牆存取的高效能場景,
Protobuf 可以優先考慮。

protobuf 的資料型別有多種:bool、double、float、int32、int64、string、bytes、enum、
message。protobuf 的限定符:required: 必須賦值,不能為空、optional:欄位可以賦值,也
可以不賦值、repeated: 該欄位可以重複任意次數(包括 0 次)、列舉;只能用指定的常數
集中的一個值作為其值;

protobuf 的基本規則:每個訊息中必須至少留有一個 required 型別的欄位、包含 0 個或多
個 optional 型別的欄位;repeated 表示的欄位可以包含 0 個或多個資料;[1,15]之內的標識
號在編碼的時候會佔用一個位元組(常用),[16,2047]之內的標識號則佔用 2 個位元組,標識號一定不能重複、使用訊息型別,也可以將訊息巢狀任意多層,可用巢狀訊息型別來代替組。

protobuf 的訊息升級原則:不要更改任何已有的欄位的數值標識;不能移除已經存在的
required 欄位,optional 和 repeated 型別的欄位可以被移除,但要保留標號不能被重用。
新新增的欄位必須是 optional 或 repeated。因為舊版本程式無法讀取或寫入新增的required 限定符的欄位。

編譯器為每一個訊息型別生成了一個.java 檔案,以及一個特殊的 Builder 類(該類是用來創
建訊息類介面的)。如:UserProto.User.Builder builder =UserProto.User.newBuilder();builder.build();
Netty 中的使用:ProtobufVarint32FrameDecoder 是用於處理半包訊息的解碼類;
ProtobufDecoder(UserProto.User.getDefaultInstance())這是建立的 UserProto.java 檔案中的解碼類;ProtobufVarint32LengthFieldPrepender 對 protobuf 協定的訊息頭上加上一個長度為32 的整形欄位,用於標誌這個訊息的長度的類;ProtobufEncoder 是編碼類將 StringBuilder 轉換為 ByteBuf 型別:copiedBuffer()方法