1. 程式人生 > 程式設計 >Netty知識筆記

Netty知識筆記

[TOC]

一、簡介

Netty是一個非同步事件驅動的網路應用框架,用於快速開發可維護的高效能伺服器和客戶端。

kcnmJ1.png

Netty是典型的Reactor模型結構,在實現上,Netty中的Boss類充當mainReactor,NioWorker類充當subReactor(預設NioWorker的個數是當前伺服器的可用核數)。

在處理新來的請求時,NioWorker讀完已收到的資料到ChannelBuffer中,之後觸發ChannelPipeline中的ChannelHandler流。

Netty是事件驅動的,可以通過ChannelHandler鏈來控制執行流向。因為ChannelHandler鏈的執行過程是在subReactor中同步的,所以如果業務處理handler耗時長,將嚴重影響可支援的併發數。

kcn8dH.png

二、NIO基礎知識點

1、阻塞與非阻塞

阻塞與非阻塞是描述程式在訪問某個資源時,資料是否準備就緒的的一種處理方式。

  • 阻塞 :當資料沒有準備就緒時,執行緒持續等待資源中資料準備完成,直到返回響應結果。
  • 非阻塞: 執行緒直接返回結果,不會持續等待資源準備資料結束後才響應結果。

2、同步與非同步

  • 同步:一般指主動請求並等待IO操作完成的方式
  • 非同步:指主動請求資料後便可以繼續處理其它任務,隨後等待IO操作完畢的通知。

三、IO模型

1、傳統BIO模型

傳統BIO是一種同步的阻塞IO,IO在進行讀寫時,該執行緒將被阻塞,執行緒無法進行其它操作。

2、偽非同步IO模型

以傳統BIO模型為基礎,通過執行緒池的方式維護所有的IO執行緒,實現相對高效的執行緒開銷及管理。

3、NIO模型

NIO模型是一種同步非阻塞IO,主要有三大核心部分:Channel(通道),Buffer(緩衝區),Selector

傳統IO基於位元組流和字元流進行操作,而NIO基於Channel和Buffer(緩衝區)進行操作,資料總是從通道讀取到緩衝區中,或者從緩衝區寫入到通道中。Selector(選擇區)用於監聽多個通道的事件(比如:連線開啟,資料到達)。因此,單個執行緒可以監聽多個資料通道。

NIO和傳統IO(一下簡稱IO)之間第一個最大的區別是,IO是面向流的,NIO是面向緩衝區的。

Java IO面向流意味著每次從流中讀一個或多個位元組,直至讀取所有位元組,它們沒有被快取在任何地方。此外,它不能前後移動流中的資料。如果需要前後移動從流中讀取的資料,需要先將它快取到一個緩衝區。NIO的緩衝導向方法略有不同。資料讀取到一個它稍後處理的緩衝區,需要時可在緩衝區中前後移動。這就增加了處理過程中的靈活性。但是,還需要檢查是否該緩衝區中包含所有您需要處理的資料。而且,需確保當更多的資料讀入緩衝區時,不要覆蓋緩衝區裡尚未處理的資料。

IO的各種流是阻塞的。這意味著,當一個執行緒呼叫read() 或 write()時,該執行緒被阻塞,直到有一些資料被讀取,或資料完全寫入。該執行緒在此期間不能再幹任何事情了。 NIO的非阻塞模式,使一個執行緒從某通道傳送請求讀取資料,但是它僅能得到目前可用的資料,如果目前沒有資料可用時,就什麼都不會獲取。而不是保持執行緒阻塞,所以直至資料變的可以讀取之前,該執行緒可以繼續做其他的事情。 非阻塞寫也是如此。一個執行緒請求寫入一些資料到某通道,但不需要等待它完全寫入,這個執行緒同時可以去做別的事情。 執行緒通常將非阻塞IO的空閒時間用於在其它通道上執行IO操作,所以一個單獨的執行緒現在可以管理多個輸入和輸出通道(channel)。

四、NIO模型要點

1、Channel(通道)

傳統IO操作對read()或write()方法的呼叫,可能會因為沒有資料可讀/可寫而阻塞,直到有資料響應。也就是說讀寫資料的IO呼叫,可能會無限期的阻塞等待,效率依賴網路傳輸的速度。最重要的是在呼叫一個方法前,無法直到是否會被阻塞。

NIO的Channel抽象了一個重要特徵就是可以通過配置它的阻塞行為,來實現非阻塞式的通道。

Channel是一個雙向通道,與傳統IO操作之允許單向的讀寫不同的是,NIO的Channel允許在一個通道上進行讀和寫的操作。

2、Buffer(緩衝區)

Bufer顧名思義,它是一個緩衝區,實際上是一個容器,一個連續陣列。Channel提供從檔案、網路讀取資料的渠道,但是讀寫的資料都必須經過Buffer。

kcuaN9.jpg

Buffer緩衝區本質上是一塊可以寫入資料,然後可以從中讀取資料的記憶體。這塊記憶體被包裝成NIO Buffer物件,並提供了一組方法,用來方便的訪問該模組記憶體。為了理解Buffer的工作原理,需要熟悉它的三個屬性:capacity、position和limit。

position和limit的含義取決於Buffer處在讀模式還是寫模式。不管Buffer處在什麼模式,capacity的含義總是一樣的.

kcuB1x.png

  • capacity

作為一個記憶體塊,Buffer有固定的大小值,也叫作“capacity”,只能往其中寫入capacity個byte、long、char等型別。一旦Buffer滿了,需要將其清空(通過讀資料或者清楚資料)才能繼續寫資料。

  • position

當你寫資料到Buffer中時,position表示當前的位置。初始的position值為0,當寫入一個位元組資料到Buffer中後,position會向前移動到下一個可插入資料的Buffer單元。position最大可為capacity-1。當讀取資料時,也是從某個特定位置讀,講Buffer從寫模式切換到讀模式,position會被重置為0。當從Buffer的position處讀取一個位元組資料後,position向前移動到下一個可讀的位置。

  • limit

在寫模式下,Buffer的limit表示你最多能往Buffer裡寫多少資料。 寫模式下,limit等於Buffer的capacity。

當切換Buffer到讀模式時, limit表示你最多能讀到多少資料。因此,當切換Buffer到讀模式時,limit會被設定成寫模式下的position值。換句話說,你能讀到之前寫入的所有資料(limit被設定成已寫資料的數量,這個值在寫模式下就是position)

  • Buffer的分配 對Buffer物件的操作必須首先進行分配,Buffer提供一個allocate(int capacity)方法分配一個指定位元組大小的物件。

  • Buffer寫資料

    • 方式一: channel寫到buffer
    • 方式二:通過Buffer的put()方法寫到Buffer

方式1:

int bytes = channel.read(buf); //將channel中的資料讀取到buf中
複製程式碼

方式2:

buf.put(byte); //將資料通過put()方法寫入到buf中
複製程式碼
  • flip()方法

將Buffer從寫模式切換到讀模式,呼叫flip()方法會將position設定為0,並將limit設定為之前的position的值。

  • Buffer讀資料
    • 從buffer讀取到channel
    • 通過buffer的get獲取資料

方式1:

int bytes = channel.write(buf); //將buf中的資料讀取到channel中
複製程式碼

方式2:

byte bt = buf.get(); //從buf中讀取一個byte
複製程式碼
  • rewind()方法

Buffer.rewind()方法將position設定為0,使得可以重讀Buffer中的所有資料,limit保持不變。

  • clear() 與compact() 方法

一旦讀完Buffer中的資料,需要讓Buffer準備好再次被寫入,可以通過clear()或compact()方法完成。

如果呼叫的是clear()方法,position將被設定為0,limit設定為capacity的值。但是Buffer並未被清空,只是通過這些標記告訴我們可以從哪裡開始往Buffer中寫入多少資料。如果Buffer中還有一些未讀的資料,呼叫clear()方法將被"遺忘 "。compact()方法將所有未讀的資料拷貝到Buffer起始處,然後將position設定到最後一個未讀元素的後面,limit屬性依然設定為capacity。可以使得Buffer中的未讀資料還可以在後續中被使用。

  • mark() 與reset()方法

通過呼叫Buffer.mark()方法可以標記一個特定的position,之後可以通過呼叫Buffer.reset()恢復到這個position上。

4.Selector(多路複用器)

Selector與Channel是相互配合使用的,將Channel註冊在Selector上之後,才可以正確的使用Selector,但此時Channel必須為非阻塞模式。Selector可以監聽Channel的四種狀態(Connect、Accept、Read、Write),當監聽到某一Channel的某個狀態時,才允許對Channel進行相應的操作。

五、NIO開發的問題

1、NIO類庫和API複雜,使用麻煩。
2、需要具備Java多執行緒程式設計能力(涉及到Reactor模式)。
3、客戶端斷線重連、網路不穩定、半包讀寫、失敗快取、網路阻塞和異常碼流等問題處理難度非常大
4、存在部分BUG
複製程式碼

NIO進行伺服器開發的步驟:

1、建立ServerSocketChannel,配置為非阻塞模式;
	2、繫結監聽,配置TCP引數;
	3、建立一個獨立的IO執行緒,用於輪詢多路複用器Selector;
	4、建立Selector,將之前建立的ServerSocketChannel註冊到Selector上,監聽Accept事件;
	5、啟動IO執行緒,在迴圈中執行Select.select()方法,輪詢就緒的Channel;
	6、當輪詢到處於就緒狀態的Channel時,需要對其進行判斷,如果是OP_ACCEPT狀態,說明有新的客戶端接入,則呼叫ServerSocketChannel.accept()方法接受新的客戶端;
	7、設定新接入的客戶端鏈路SocketChannel為非阻塞模式,配置TCP引數;
	8、將SocketChannel註冊到Selector上,監聽READ事件;
	9、如果輪詢的Channel為OP_READ,則說明SocketChannel中有新的準備就緒的資料包需要讀取,則構造ByteBuffer物件,讀取資料包;
	10、如果輪詢的Channel為OP_WRITE,則說明還有資料沒有傳送完成,需要繼續傳送。
複製程式碼

六、Netty的有點

    1、API使用簡單,開發門檻低;
	2、功能強大,預置了多種編解碼功能,支援多種主流協議;
	3、定製功能強,可以通過ChannelHandler對通訊框架進行靈活的擴充套件;
	4、效能高,通過與其他業界主流的NIO框架對比,Netty綜合效能最優;
	5、成熟、穩定,Netty修復了已經發現的NIO所有BUG;
	6、社群活躍;
	7、經歷了很多商用專案的考驗。
複製程式碼

七、粘包/拆包問題

TCP是一個“流”協議,所謂流,就是沒有界限的一串資料。可以想象為河流中的水,並沒有分界線。TCP底層並不瞭解上層業務資料的具體含義,它會根據TCP緩衝區的實際情況進行包的劃分,所以在業務上認為,一個完整的包可能會被TCP拆分成多個包進行傳送,也有可能把多個小的包封裝成一個大的資料包傳送,這就是所謂的TCP粘包和拆包問題。

k7zyaF.png

假設客戶端分別傳送了兩個資料包D1和D2給服務端,由於服務端一次讀取到的位元組數是不確定的,可能存在以下4中情況。

 1.服務端分兩次讀取到了兩個獨立的資料包,分別是D1和D2,沒有粘包和拆包;
 2.服務端一次接收到了兩個資料包,D1和D2粘合在一起,被稱為TCP粘包;
 3.服務端分兩次讀取到了兩個資料包,第一次讀取到了完整的D1包和D2包的部分內容,第二次讀取到了D2包的剩餘部分內容,這被稱為TCP拆包;
 4.服務端分兩次讀取到了兩個資料包,第一次讀取到了D1包的部分內容D1_1,第二次讀取到了D1包的剩餘內容D1_1和D2包的完整內容;
複製程式碼

如果此時伺服器TCP接收滑窗非常小,而資料包D1和D2比較大,很有可能發生第五種情況,既服務端分多次才能將D1和D2包接收完全,期間發生多次拆包;

問題的解決策略

由於底層的TCP無法理解上層的業務資料,所以在底層是無法保證資料包不被拆分和重組的,這個問題只能通過上層的應用協議棧設計來解決,根據業界的主流協議的解決方案可歸納如下:

 1.訊息定長,例如每個報文的大小為固定長度200位元組,如果不夠,空位補空格;
 2.在包尾增加回車換行符進行分割,例如FTP協議;
 3.將訊息分為訊息頭和訊息體,訊息頭中包含訊息總長度(或訊息體總長度)的欄位,通常設計思路為訊息頭的第一個欄位使用int32來表示訊息的總程度;
 4.更復雜的應用層協議;
複製程式碼
  • LineBasedFrameDecoder
為瞭解決TCP粘包/拆包導致的半包讀寫問題,Netty預設提供了多種編解碼器用於處理半包。

LinkeBasedFrameDecoder的工作原理是它一次遍歷ByteBuf中的可讀位元組,判斷看是否有“\n”、“\r\n”,如果有,就一次位置為結束位置,從可讀索引到結束位置區間的位元組就組成一行。它是以換行符為結束標誌的編解碼,支援攜帶結束符或者不攜帶結束符兩種解碼方式,同時支援配置單行的最大長度。如果連續讀取到最大長度後任然沒有發現換行符,就會丟擲異常,同時忽略掉之前讀到的異常碼流。
複製程式碼
  • DelimiterBasedFrameDecoder
實現自定義分隔符作為訊息的結束標誌,完成解碼。
複製程式碼
  • FixedLengthFrameDecoder
是固定長度解碼器,能夠按照指定的長度對訊息進行自動解碼,開發者不需要考慮TCP的粘包/拆包問題。
複製程式碼

八、Netty的高效能之道

1.非同步非阻塞通訊

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

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

2、高效的Reactor執行緒模型

常用的Reactor執行緒模型有三種,分別如下:

  • C1、Reactor單執行緒模型

kOzIhT.png

Reactor單執行緒模型,指的是所有的IO操作都在同一個NIO執行緒上面完成,NIO執行緒職責如下:

1、作為NIO服務端,接收客戶端的TCP連線;

2、作為NIO客戶端,向服務端發起TCP連線;

3、讀取通訊對端的請求或者應答訊息;

4、向通訊對端傳送請求訊息或者應答訊息;

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

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

①、一個NIO執行緒同時處理成百上千的鏈路,效能上無法支撐。即便NIO執行緒的CPU負荷達到100%,也無法滿足海量訊息的編碼、解碼、讀取和傳送;

②、當NIO執行緒負載過重後,處理速度將變慢,這會導致大量客戶端連線超時,超時之後往往會進行重發,這更加重了NIO執行緒的負載,最終會導致大量訊息積壓和處理超時,NIO執行緒會成為系統的效能瓶頸;

③、可靠性問題。一旦NIO執行緒意外進入死迴圈,會導致整個系統通訊模組不可用,不能接收和處理外部訊息,造成節點故障。

為瞭解決這些問題,從而演進出了Reactor多執行緒模型。

  • C2、Reactor多執行緒模型
    kXpteA.png

Reactor多執行緒模型與單執行緒模型最大的區別就是有一組NIO執行緒處理IO操作,特點如下:

①有一個專門的NIO執行緒——Acceptor執行緒用於監聽服務端,接收客戶端TCP連線請求;

②網路IO操作——讀、寫等由一個NIO執行緒池負責,執行緒池可以採用標準的JDK執行緒池實現,它包含一個任務佇列和N個可用的執行緒,由這些NIO執行緒負責訊息的讀取、編碼、解碼和傳送;

③1個NIO執行緒可以同時處理N條鏈路,但是1個鏈路只對應1個NIO執行緒,防止發生併發操作問題。

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

  • C3、主從Reactor多執行緒模型
    kXpyLj.png

主從Reactor執行緒模型的特點是:

服務端用於接收客戶端連線的不再是一個單獨的NIO執行緒,而是一個獨立的NIO執行緒池。Acceptor接收到客戶端TCP連線請求處理完成後(可能包含接入認證等),將新建立的SocketChannel註冊到IO執行緒池(subReactor執行緒池)的某個IO執行緒上,由它負責SocketChannel的讀寫和編解碼工作。Acceptor執行緒池只用於客戶端的登入、握手和安全認證,一旦鏈路建立成功,就將鏈路註冊到後端subReactor執行緒池的IO執行緒上,由IO執行緒負責後續的IO操作。
複製程式碼

利用主從NIO執行緒模型,可以解決1個服務端監聽執行緒無法有效處理所有客戶端連線的效能不足問題。Netty官方推薦使用該執行緒模型。它的工作流程總結如下:

①從主執行緒池中隨機選擇一個Reactor執行緒作為Acceptor執行緒,用於繫結監聽埠,接收客戶端連線;

②Acceptor執行緒接收客戶端連線請求之後,建立新的SocketChannel,將其註冊到主執行緒池的其他Reactor執行緒上,由其負責接入認證、IP黑白名單過濾、握手等操作;

③然後也業務層的鏈路正式建立成功,將SocketChannel從主執行緒池的Reactor執行緒的多路複用器上摘除,重新註冊到Sub執行緒池的執行緒上,用於處理IO的讀寫操作。

3、無鎖化的序列設計

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

為了儘可能提升效能,Netty採用了序列無鎖化設計,在IO執行緒內部進行序列操作,避免多執行緒競爭導致的效能下降。表面上看,序列化設計似乎CPU利用率不高,併發程度不夠。但是,通過調整NIO執行緒池的執行緒引數,可以同時啟動多個序列化的執行緒並行執行,這種區域性無鎖化的序列執行緒設計相比一個佇列——多個工作執行緒模型效能更優。

kjGsf0.png

Netty的NioEventLoop讀取到訊息後,直接呼叫ChannelPipeline的fireChannelRead(Object msg),只要使用者不主動切換執行緒,一直會由NioEventLoop呼叫到使用者的Handler,期間不進行執行緒切換。這種序列化處理方式避免了多執行緒導致的鎖競爭,從效能角度看是最優的

4、高效的併發程式設計

Netty中高效併發程式設計主要體現:

  • volatile的大量、正確使用
  • CAS和原子類的廣泛使用
  • 執行緒安全容器的使用
  • 通過讀寫鎖提升併發效能

5、高效的序列化框架

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

  • 序列化後的碼流大小(網路寬頻的佔用)
  • 序列化與反序列化的效能(CPU資源佔用)
  • 是否支援跨語言(異構系統的對接和開發語言切換) Netty預設提供了對GoogleProtobuf的支援,通過擴充套件Netty的編解碼介面,使用者可以實現其他的高效能序列化框架

6、零拷貝

Netty的“零拷貝”主要體現在三個方面:

  • Direct buffers

Netty的接收和傳送ByteBuffer採用DIRECT BUFFERS,使用堆外直接記憶體進行Socket讀寫,不需要進行位元組緩衝區的二次拷貝。如果使用傳統的堆記憶體(HEAP BUFFERS)進行Socket讀寫,JVM會將堆記憶體Buffer拷貝一份到直接記憶體中,然後才寫入Socket中。相比於堆外直接記憶體,訊息在傳送過程中多了一次緩衝區的記憶體拷貝

  • CompositeByteBuf

第二種“零拷貝 ”的實現CompositeByteBuf,它對外將多個ByteBuf封裝成一個ByteBuf,對外提供統一封裝後的ByteBuf介面

  • 檔案傳輸

第三種“零拷貝”就是檔案傳輸,Netty檔案傳輸類DefaultFileRegion通過transferTo方法將檔案傳送到目標Channel中。很多作業系統直接將檔案緩衝區的內容傳送到目標Channel中,而不需要通過迴圈拷貝的方式,這是一種更加高效的傳輸方式,提升了傳輸效能,降低了CPU和記憶體佔用,實現了檔案傳輸的“零拷貝”。

7、記憶體池

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

8、靈活的TCP引數配置能力

Netty在啟動輔助類中可以靈活的配置TCP引數,滿足不同的使用者場景。合理設定TCP引數在某些場景下對於效能的提升可以起到的顯著的效果,總結一下對效能影響比較大的幾個配置項:

    1)、SO_RCVBUF和SO_SNDBUF:通常建議值為128KB或者256KB;
    2)、TCP_NODELAY:NAGLE演演算法通過將緩衝區內的小封包自動相連,組成較大的封包,阻止大量小封包的傳送阻塞網路,從而提高網路應用效率。但是對於時延敏感的應用場景需要關閉該優化演演算法;
    3)、軟中斷:如果Linux核心版本支援RPS(2.6.35以上版本),開啟RPS後可以實現軟中斷,提升網路吞吐量。RPS根據資料包的源地址,目的地址以及目的和源埠,計算出一個hash值,然後根據這個hash值來選擇軟中斷執行的CPU。從上層來看,也就是說將每個連線和CPU繫結,並通過這個hash值,來均衡軟中斷在多個CPU上,提升網路並行處理效能。

複製程式碼

九、專案實踐