天天看點

1萬2千字長文助力春招 | Netty面試篇

本文大部分内容是作者ThinkWon發表在部落格上的,位址:http://rrd.me/gfHnF,我個人結合自己平時面試時的經驗增加了部分内容。希望對大家面試有幫助。

1.Netty 是什麼?

Netty是 一個異步事件驅動的網絡應用程式架構,用于快速開發可維護的高性能協定伺服器和用戶端。Netty是基于nio的,它封裝了jdk的nio,讓我們使用起來更加方法靈活。

2.Netty 的特點是什麼?

  • 高并發:Netty 是一款基于 NIO(Nonblocking IO,非阻塞IO)開發的網絡通信架構,對比于 BIO(Blocking I/O,阻塞IO),他的并發性能得到了很大提高。
  • 傳輸快:Netty 的傳輸依賴于零拷貝特性,盡量減少不必要的記憶體拷貝,實作了更高效率的傳輸。
  • 封裝好:Netty 封裝了 NIO 操作的很多細節,提供了易于使用調用接口。

3.Netty 的優勢有哪些?

  • 使用簡單:封裝了 NIO 的很多細節,使用更簡單。
  • 功能強大:預置了多種編解碼功能,支援多種主流協定。
  • 定制能力強:可以通過 ChannelHandler 對通信架構進行靈活地擴充。
  • 性能高:通過與其他業界主流的 NIO 架構對比,Netty 的綜合性能最優。
  • 穩定:Netty 修複了已經發現的所有 NIO 的 bug,讓開發人員可以專注于業務本身。
  • 社群活躍:Netty 是活躍的開源項目,版本疊代周期短,bug 修複速度快。

4.Netty 的應用場景有哪些?

典型的應用有:阿裡分布式服務架構 Dubbo,預設使用 Netty 作為基礎通信元件,還有 RocketMQ 也是使用 Netty 作為通訊的基礎。

5.Netty 高性能表現在哪些方面?

  • IO 線程模型:同步非阻塞,用最少的資源做更多的事。
  • 記憶體零拷貝:盡量減少不必要的記憶體拷貝,實作了更高效率的傳輸。
  • 記憶體池設計:申請的記憶體可以重用,主要指直接記憶體。内部實作是用一顆二叉查找樹管理記憶體配置設定情況。
  • 串形化處理讀寫:避免使用鎖帶來的性能開銷。
  • 高性能序列化協定:支援 protobuf 等高性能序列化協定。

6.BIO、NIO和AIO的差別?

BIO:一個連接配接一個線程,用戶端有連接配接請求時伺服器端就需要啟動一個線程進行處理。線程開銷大。

僞異步IO:将請求連接配接放入線程池,一對多,但線程還是很寶貴的資源。

NIO:一個請求一個線程,但用戶端發送的連接配接請求都會注冊到多路複用器上,多路複用器輪詢到連接配接有I/O請求時才啟動一個線程進行處理。

AIO:一個有效請求一個線程,用戶端的I/O請求都是由OS先完成了再通知伺服器應用去啟動線程進行處理,

BIO是面向流的,NIO是面向緩沖區的;BIO的各種流是阻塞的。而NIO是非阻塞的;BIO的Stream是單向的,而NIO的channel是雙向的。

NIO的特點:事件驅動模型、單線程處理多任務、非阻塞I/O,I/O讀寫不再阻塞,而是傳回0、基于block的傳輸比基于流的傳輸更高效、更進階的IO函數zero-copy、IO多路複用大大提高了Java網絡應用的可伸縮性和實用性。基于Reactor線程模型。

在Reactor模式中,事件分發器等待某個事件或者可應用或個操作的狀态發生,事件分發器就把這個事件傳給事先注冊的事件處理函數或者回調函數,由後者來做實際的讀寫操作。如在Reactor中實作讀:注冊讀就緒事件和相應的事件處理器、事件分發器等待事件、事件到來,激活分發器,分發器調用事件對應的處理器、事件處理器完成實際的讀操作,處理讀到的資料,注冊新的事件,然後返還控制權。

7.NIO的組成?

Buffer:與Channel進行互動,資料是從Channel讀入緩沖區,從緩沖區寫入Channel中的

flip方法 :反轉此緩沖區,将position給limit,然後将position置為0,其實就是切換讀寫模式

clear方法 :清除此緩沖區,将position置為0,把capacity的值給limit。

rewind方法 :重繞此緩沖區,将position置為0

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()輪詢拿到已經就緒的事件

8.Netty的線程模型?

Netty通過Reactor模型基于多路複用器接收并處理使用者請求,内部實作了兩個線程池,boss線程池和work線程池,其中boss線程池的線程負責處理請求的accept事件,當接收到accept事件的請求時,把對應的socket封裝到一個NioSocketChannel中,并交給work線程池,其中work線程池負責請求的read和write事件,由對應的Handler處理。

單線程模型:所有I/O操作都由一個線程完成,即多路複用、事件分發和處理都是在一個Reactor線程上完成的。既要接收用戶端的連接配接請求,向服務端發起連接配接,又要發送/讀取請求或應答/響應消息。一個NIO 線程同時處理成百上千的鍊路,性能上無法支撐,速度慢,若線程進入死循環,整個程式不可用,對于高負載、大并發的應用場景不合适。

多線程模型:有一個NIO 線程(Acceptor) 隻負責監聽服務端,接收用戶端的TCP 連接配接請求;NIO 線程池負責網絡IO 的操作,即消息的讀取、解碼、編碼和發送;1 個NIO 線程可以同時處理N 條鍊路,但是1 個鍊路隻對應1 個NIO 線程,這是為了防止發生并發操作問題。但在并發百萬用戶端連接配接或需要安全認證時,一個Acceptor 線程可能會存在性能不足問題。

主從多線程模型:Acceptor 線程用于綁定監聽端口,接收用戶端連接配接,将SocketChannel 從主線程池的Reactor 線程的多路複用器上移除,重新注冊到Sub 線程池的線程上,用于處理I/O 的讀寫等操作,進而保證mainReactor隻負責接入認證、握手等操作;

9.TCP 粘包/拆包的原因及解決方法?

TCP是以流的方式來處理資料,一個完整的包可能會被TCP拆分成多個包進行發送,也可能把小的封裝成一個大的資料包發送。

TCP粘包/分包的原因:

應用程式寫入的位元組大小大于套接字發送緩沖區的大小,會發生拆包現象,而應用程式寫入資料小于套接字緩沖區大小,網卡将應用多次寫入的資料發送到網絡上,這将會發生粘包現象;

進行MSS大小的TCP分段,當TCP封包長度-TCP頭部長度>MSS的時候将發生拆包

以太網幀的payload(淨荷)大于MTU(1500位元組)進行ip分片。

解決方法

消息定長:FixedLengthFrameDecoder類

包尾增加特殊字元分割:

  • 行分隔符類:LineBasedFrameDecoder
  • 或自定義分隔符類 :DelimiterBasedFrameDecoder

将消息分為消息頭和消息體:LengthFieldBasedFrameDecoder類。分為有頭部的拆包與粘包、長度字段在前且有頭部的拆包與粘包、多擴充頭部的拆包與粘包。

10.什麼是 Netty 的零拷貝?

Netty 的零拷貝主要包含三個方面:

  • Netty 的接收和發送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接記憶體進行 Socket 讀寫,不需要進行位元組緩沖區的二次拷貝。如果使用傳統的堆記憶體(HEAP BUFFERS)進行 Socket 讀寫,JVM 會将堆記憶體 Buffer 拷貝一份到直接記憶體中,然後才寫入 Socket 中。相比于堆外直接記憶體,消息在發送過程中多了一次緩沖區的記憶體拷貝。
  • Netty 提供了組合 Buffer 對象,可以聚合多個 ByteBuffer 對象,使用者可以像操作一個 Buffer 那樣友善的對組合 Buffer 進行操作,避免了傳統通過記憶體拷貝的方式将幾個小 Buffer 合并成一個大的 Buffer。
  • Netty 的檔案傳輸采用了 transferTo 方法,它可以直接将檔案緩沖區的資料發送到目标 Channel,避免了傳統通過循環 write 方式導緻的記憶體拷貝問題。

11.Netty 中有哪種重要元件?

  • Channel:Netty 網絡操作抽象類,它除了包括基本的 I/O 操作,如 bind、connect、read、write 等。
  • EventLoop:主要是配合 Channel 處理 I/O 操作,用來處理連接配接的生命周期中所發生的事情。
  • ChannelFuture:Netty 架構中所有的 I/O 操作都為異步的,是以我們需要 ChannelFuture 的 addListener()注冊一個 ChannelFutureListener 監聽事件,當操作執行成功或者失敗時,監聽就會自動觸發傳回結果。
  • ChannelHandler:充當了所有處理入站和出站資料的邏輯容器。ChannelHandler 主要用來處理各種事件,這裡的事件很廣泛,比如可以是連接配接、資料接收、異常、資料轉換等。
  • ChannelPipeline:為 ChannelHandler 鍊提供了容器,當 channel 建立時,就會被自動配置設定到它專屬的 ChannelPipeline,這個關聯是永久性的。

12.Netty 發送消息有幾種方式?

Netty 有兩種發送消息的方式:

  • 直接寫入 Channel 中,消息從 ChannelPipeline 當中尾部開始移動;
  • 寫入和 ChannelHandler 綁定的 ChannelHandlerContext 中,消息從 ChannelPipeline 中的下一個 ChannelHandler 中移動。

13.預設情況 Netty 起多少線程?何時啟動?

Netty 預設是 CPU 處理器數的兩倍,bind 完之後啟動。

14.了解哪幾種序列化協定?

序列化(編碼)是将對象序列化為二進制形式(位元組數組),主要用于網絡傳輸、資料持久化等;而反序列化(解碼)則是将從網絡、磁盤等讀取的位元組數組還原成原始對象,主要用于網絡傳輸對象的解碼,以便完成遠端調用。

影響序列化性能的關鍵因素:序列化後的碼流大小(網絡帶寬的占用)、序列化的性能(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)

15.如何選擇序列化協定?

具體場景

對于公司間的系統調用,如果性能要求在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()方法

16.Netty 支援哪些心跳類型設定?

  • readerIdleTime:為讀逾時時間(即測試端一定時間内未接受到被測試端消息)。
  • writerIdleTime:為寫逾時時間(即測試端一定時間内向被測試端發送消息)。
  • allIdleTime:所有類型的逾時時間。

17.Netty 和 Tomcat 的差別?

  • 作用不同:Tomcat 是 Servlet 容器,可以視為 Web 伺服器,而 Netty 是異步事件驅動的網絡應用程式架構和工具用于簡化網絡程式設計,例如TCP和UDP套接字伺服器。
  • 協定不同:Tomcat 是基于 http 協定的 Web 伺服器,而 Netty 能通過程式設計自定義各種協定,因為 Netty 本身自己能編碼/解碼位元組流,所有 Netty 可以實作,HTTP 伺服器、FTP 伺服器、UDP 伺服器、RPC 伺服器、WebSocket 伺服器、Redis 的 Proxy 伺服器、MySQL 的 Proxy 伺服器等等。

18.NIOEventLoopGroup源碼?

NioEventLoopGroup(其實是MultithreadEventExecutorGroup) 内部維護一個類型為 EventExecutor children [], 預設大小是處理器核數 * 2, 這樣就構成了一個線程池,初始化EventExecutor時NioEventLoopGroup重載newChild方法,是以children元素的實際類型為NioEventLoop。

線程啟動時調用SingleThreadEventExecutor的構造方法,執行NioEventLoop類的run方法,首先會調用hasTasks()方法判斷目前taskQueue是否有元素。如果taskQueue中有元素,執行 selectNow() 方法,最終執行selector.selectNow(),該方法會立即傳回。如果taskQueue沒有元素,執行 select(oldWakenUp) 方法

select ( oldWakenUp) 方法解決了 Nio 中的 bug,selectCnt 用來記錄selector.select方法的執行次數和辨別是否執行過selector.selectNow(),若觸發了epoll的空輪詢bug,則會反複執行selector.select(timeoutMillis),變量selectCnt 會逐漸變大,當selectCnt 達到門檻值(預設512),則執行rebuildSelector方法,進行selector重建,解決cpu占用100%的bug。

rebuildSelector方法先通過openSelector方法建立一個新的selector。然後将old selector的selectionKey執行cancel。最後将old selector的channel重新注冊到新的selector中。rebuild後,需要重新執行方法selectNow,檢查是否有已ready的selectionKey。

接下來調用processSelectedKeys 方法(處理I/O任務),當selectedKeys != null時,調用processSelectedKeysOptimized方法,疊代 selectedKeys 擷取就緒的 IO 事件的selectkey存放在數組selectedKeys中, 然後為每個事件都調用 processSelectedKey 來處理它,processSelectedKey 中分别處理OP_READ;OP_WRITE;OP_CONNECT事件。

最後調用runAllTasks方法(非IO任務),該方法首先會調用fetchFromScheduledTaskQueue方法,把scheduledTaskQueue中已經超過延遲執行時間的任務移到taskQueue中等待被執行,然後依次從taskQueue中取任務執行,每執行64個任務,進行耗時檢查,如果已執行時間超過預先設定的執行時間,則停止執行非IO任務,避免非IO任務太多,影響IO任務的執行。

每個NioEventLoop對應一個線程和一個Selector,NioServerSocketChannel會主動注冊到某一個NioEventLoop的Selector上,NioEventLoop負責事件輪詢。

Outbound 事件都是請求事件, 發起者是 Channel,處理者是 unsafe,通過 Outbound 事件進行通知,傳播方向是 tail到head。Inbound 事件發起者是 unsafe,事件的處理者是 Channel, 是通知事件,傳播方向是從頭到尾。

記憶體管理機制,首先會預申請一大塊記憶體Arena,Arena由許多Chunk組成,而每個Chunk預設由2048個page組成。Chunk通過AVL樹的形式組織Page,每個葉子節點表示一個Page,而中間節點表示記憶體區域,節點自己記錄它在整個Arena中的偏移位址。當區域被配置設定出去後,中間節點上的标記位會被标記,這樣就表示這個中間節點以下的所有節點都已被配置設定了。大于8k的記憶體配置設定在poolChunkList中,而PoolSubpage用于配置設定小于8k的記憶體,它會把一個page分割成多段,進行記憶體配置設定。

ByteBuf的特點:支援自動擴容(4M),保證put方法不會抛出異常、通過内置的複合緩沖類型,實作零拷貝(zero-copy);不需要調用flip()來切換讀/寫模式,讀取和寫入索引分開;方法鍊;引用計數基于AtomicIntegerFieldUpdater用于記憶體回收;PooledByteBuf采用二叉樹來實作一個記憶體池,集中管理記憶體的配置設定和釋放,不用每次使用都建立一個緩沖區對象。UnpooledHeapByteBuf每次都會建立一個緩沖區對象。

JDK原生NIO程式的問題

JDK原生也有一套網絡應用程式API,但是存在一系列問題,主要如下:

  • NIO的類庫和API繁雜,使用麻煩,你需要熟練掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等
  • 需要具備其它的額外技能做鋪墊,例如熟悉Java多線程程式設計,因為NIO程式設計涉及到Reactor模式,你必須對多線程和網路程式設計非常熟悉,才能編寫出高品質的NIO程式
  • 可靠性能力補齊,開發工作量和難度都非常大。例如用戶端面臨斷連重連、網絡閃斷、半包讀寫、失敗緩存、網絡擁塞和異常碼流的處理等等,NIO程式設計的特點是功能開發相對容易,但是可靠性能力補齊工作量和難度都非常大
  • JDK NIO的BUG,例如臭名昭著的epoll bug,它會導緻Selector空輪詢,最終導緻CPU 100%。官方聲稱在JDK1.6版本的update18修複了該問題,但是直到JDK1.7版本該問題仍舊存在,隻不過該bug發生機率降低了一些而已,它并沒有被根本解決

Netty的特點

Netty的對JDK自帶的NIO的API進行封裝,解決上述問題,主要特點有:

  • 設計優雅 适用于各種傳輸類型的統一API - 阻塞和非阻塞Socket 基于靈活且可擴充的事件模型,可以清晰地分離關注點 高度可定制的線程模型 - 單線程,一個或多個線程池 真正的無連接配接資料報套接字支援(自3.1起)
  • 使用友善 詳細記錄的Javadoc,使用者指南和示例 沒有其他依賴項,JDK 5(Netty 3.x)或6(Netty 4.x)就足夠了
  • 高性能 吞吐量更高,延遲更低 減少資源消耗 最小化不必要的記憶體複制
  • 安全 完整的SSL / TLS和StartTLS支援
  • 社群活躍,不斷更新 社群活躍,版本疊代周期短,發現的BUG可以被及時修複,同時,更多的新功能會被加入

Netty常見使用場景

Netty常見的使用場景如下:

  • 網際網路行業 在分布式系統中,各個節點之間需要遠端服務調用,高性能的RPC架構必不可少,Netty作為異步高新能的通信架構,往往作為基礎通信元件被這些RPC架構使用。典型的應用有:阿裡分布式服務架構Dubbo的RPC架構使用Dubbo協定進行節點間通信,Dubbo協定預設使用Netty作為基礎通信元件,用于實作各程序節點之間的内部通信。
  • 遊戲行業 無論是手遊服務端還是大型的網絡遊戲,Java語言得到了越來越廣泛的應用。Netty作為高性能的基礎通信元件,它本身提供了TCP/UDP和HTTP協定棧。非常友善定制和開發私有協定棧,賬号登入伺服器,地圖伺服器之間可以友善的通過Netty進行高性能的通信
  • 大資料領域 經典的Hadoop的高性能通信和序列化元件Avro的RPC架構,預設采用Netty進行跨界點通信,它的Netty Service基于Netty架構二次封裝實作

Netty高性能設計

Netty作為異步事件驅動的網絡,高性能之處主要來自于其I/O模型和線程處理模型,前者決定如何收發資料,後者決定如何處理資料

I/O模型

用什麼樣的通道将資料發送給對方,BIO、NIO或者AIO,I/O模型在很大程度上決定了架構的性能

阻塞I/O

傳統阻塞型I/O(BIO)可以用下圖表示:

1萬2千字長文助力春招 | Netty面試篇

特點

  • 每個請求都需要獨立的線程完成資料read,業務處理,資料write的完整操作

問題

  • 當并發數較大時,需要建立大量線程來處理連接配接,系統資源占用較大
  • 連接配接建立後,如果目前線程暫時沒有資料可讀,則線程就阻塞在read操作上,造成線程資源浪費

I/O複用模型

1萬2千字長文助力春招 | Netty面試篇

在I/O複用模型中,會用到select,這個函數也會使程序阻塞,但是和阻塞I/O所不同的的,這兩個函數可以同時阻塞多個I/O操作,而且可以同時對多個讀操作,多個寫操作的I/O函數進行檢測,直到有資料可讀或可寫時,才真正調用I/O操作函數

Netty的非阻塞I/O的實作關鍵是基于I/O複用模型,這裡用Selector對象表示:

1萬2千字長文助力春招 | Netty面試篇

Netty的IO線程NioEventLoop由于聚合了多路複用器Selector,可以同時并發處理成百上千個用戶端連接配接。當線程從某用戶端Socket通道進行讀寫資料時,若沒有資料可用時,該線程可以進行其他任務。線程通常将非阻塞 IO 的空閑時間用于在其他通道上執行 IO 操作,是以單獨的線程可以管理多個輸入和輸出通道。

由于讀寫操作都是非阻塞的,這就可以充分提升IO線程的運作效率,避免由于頻繁I/O阻塞導緻的線程挂起,一個I/O線程可以并發處理N個用戶端連接配接和讀寫操作,這從根本上解決了傳統同步阻塞I/O一連接配接一線程模型,架構的性能、彈性伸縮能力和可靠性都得到了極大的提升。

基于buffer

傳統的I/O是面向位元組流或字元流的,以流式的方式順序地從一個Stream 中讀取一個或多個位元組, 是以也就不能随意改變讀取指針的位置。

在NIO中, 抛棄了傳統的 I/O流, 而是引入了Channel和Buffer的概念. 在NIO中, 隻能從Channel中讀取資料到Buffer中或将資料 Buffer 中寫入到 Channel。

基于buffer操作不像傳統IO的順序操作, NIO 中可以随意地讀取任意位置的資料

線程模型

資料報如何讀取?讀取之後的編解碼在哪個線程進行,編解碼後的消息如何派發,線程模型的不同,對性能的影響也非常大。

事件驅動模型

通常,我們設計一個事件處理模型的程式有兩種思路

  • 輪詢方式 線程不斷輪詢通路相關事件發生源有沒有發生事件,有發生事件就調用事件處理邏輯。
  • 事件驅動方式 發生事件,主線程把事件放入事件隊列,在另外線程不斷循環消費事件清單中的事件,調用事件對應的處理邏輯處理事件。事件驅動方式也被稱為消息通知方式,其實是設計模式中觀察者模式的思路。

以GUI的邏輯處理為例,說明兩種邏輯的不同:

  • 輪詢方式 線程不斷輪詢是否發生按鈕點選事件,如果發生,調用處理邏輯
  • 事件驅動方式 發生點選事件把事件放入事件隊列,在另外線程消費的事件清單中的事件,根據事件類型調用相關事件處理邏輯

這裡借用O’Reilly 大神關于事件驅動模型解釋圖

1萬2千字長文助力春招 | Netty面試篇

主要包括4個基本元件:

  • 事件隊列(event queue):接收事件的入口,存儲待處理事件
  • 分發器(event mediator):将不同的事件分發到不同的業務邏輯單元
  • 事件通道(event channel):分發器與處理器之間的聯系管道
  • 事件處理器(event processor):實作業務邏輯,處理完成後會發出事件,觸發下一步操作

可以看出,相對傳統輪詢模式,事件驅動有如下優點:

  • 可擴充性好,分布式的異步架構,事件處理器之間高度解耦,可以友善擴充事件處理邏輯
  • 高性能,基于隊列暫存事件,能友善并行異步處理事件

Reactor線程模型

Reactor是反應堆的意思,Reactor模型,是指通過一個或多個輸入同時傳遞給服務處理器的服務請求的事件驅動處理模式。服務端程式處理傳入多路請求,并将它們同步分派給請求對應的處理線程,Reactor模式也叫Dispatcher模式,即I/O多了複用統一監聽事件,收到事件後分發(Dispatch給某程序),是編寫高性能網絡伺服器的必備技術之一。

Reactor模型中有2個關鍵組成:

  • Reactor Reactor在一個單獨的線程中運作,負責監聽和分發事件,分發給适當的處理程式來對IO事件做出反應。它就像公司的電話接線員,它接聽來自客戶的電話并将線路轉移到适當的聯系人
  • Handlers 處理程式執行I/O事件要完成的實際事件,類似于客戶想要與之交談的公司中的實際官員。Reactor通過排程适當的處理程式來響應I/O事件,處理程式執行非阻塞操作
1萬2千字長文助力春招 | Netty面試篇

取決于Reactor的數量和Hanndler線程數量的不同,Reactor模型有3個變種

  • 單Reactor單線程
  • 單Reactor多線程
  • 主從Reactor多線程

可以這樣了解,Reactor就是一個執行while (true) { selector.select(); …}循環的線程,會源源不斷的産生新的事件,稱作反應堆很貼切。

Netty線程模型

Netty主要基于主從Reactors多線程模型(如下圖)做了一定的修改,其中主從Reactor多線程模型有多個Reactor:MainReactor和SubReactor:

  • MainReactor負責用戶端的連接配接請求,并将請求轉交給SubReactor
  • SubReactor負責相應通道的IO讀寫請求
  • 非IO請求(具體邏輯處理)的任務則會直接寫入隊列,等待worker threads進行處理

這裡引用Doug Lee大神的Reactor介紹:Scalable IO in Java裡面關于主從Reactor多線程模型的圖

1萬2千字長文助力春招 | Netty面試篇

特别說明的是:雖然Netty的線程模型基于主從Reactor多線程,借用了MainReactor和SubReactor的結構,但是實際實作上,SubReactor和Worker線程在同一個線程池中:

1萬2千字長文助力春招 | Netty面試篇

上面代碼中的bossGroup 和workerGroup是Bootstrap構造方法中傳入的兩個對象,這兩個group均是線程池

  • bossGroup線程池則隻是在bind某個端口後,獲得其中一個線程作為MainReactor,專門處理端口的accept事件,每個端口對應一個boss線程
  • workerGroup線程池會被各個SubReactor和worker線程充分利用

異步處理

異步的概念和同步相對。當一個異步過程調用發出後,調用者不能立刻得到結果。實際處理這個調用的部件在完成後,通過狀态、通知和回調來通知調用者。

Netty中的I/O操作是異步的,包括bind、write、connect等操作會簡單的傳回一個ChannelFuture,調用者并不能立刻獲得結果,通過Future-Listener機制,使用者可以友善的主動擷取或者通過通知機制獲得IO操作結果。

當future對象剛剛建立時,處于非完成狀态,調用者可以通過傳回的ChannelFuture來擷取操作執行的狀态,注冊監聽函數來執行完成後的操,常見有如下操作:

  • 通過isDone方法來判斷目前操作是否完成
  • 通過isSuccess方法來判斷已完成的目前操作是否成功
  • 通過getCause方法來擷取已完成的目前操作失敗的原因
  • 通過isCancelled方法來判斷已完成的目前操作是否被取消
  • 通過addListener方法來注冊監聽器,當操作已完成(isDone方法傳回完成),将會通知指定的監聽器;如果future對象已完成,則了解通知指定的監聽器

例如下面的的代碼中綁定端口是異步操作,當綁定操作處理完,将會調用相應的監聽器處理邏輯

1萬2千字長文助力春招 | Netty面試篇

相比傳統阻塞I/O,執行I/O操作後線程會被阻塞住, 直到操作完成;異步處理的好處是不會造成線程阻塞,線程在I/O操作期間可以執行别的程式,在高并發情形下會更穩定和更高的吞吐量。

Netty架構設計

前面介紹完Netty相關一些理論介紹,下面從功能特性、子產品元件、運作過程來介紹Netty的架構設計

功能特性

1萬2千字長文助力春招 | Netty面試篇
  • 傳輸服務 支援BIO和NIO
  • 容器內建 支援OSGI、JBossMC、Spring、Guice容器
  • 協定支援 HTTP、Protobuf、二進制、文本、WebSocket等一系列常見協定都支援。還支援通過實行編碼解碼邏輯來實作自定義協定
  • Core核心 可擴充事件模型、通用通信API、支援零拷貝的ByteBuf緩沖對象

子產品元件

Bootstrap、ServerBootstrap

Bootstrap意思是引導,一個Netty應用通常由一個Bootstrap開始,主要作用是配置整個Netty程式,串聯各個元件,Netty中Bootstrap類是用戶端程式的啟動引導類,ServerBootstrap是服務端啟動引導類。

Future、ChannelFuture

正如前面介紹,在Netty中所有的IO操作都是異步的,不能立刻得知消息是否被正确處理,但是可以過一會等它執行完成或者直接注冊一個監聽,具體的實作就是通過Future和ChannelFutures,他們可以注冊一個監聽,當操作執行成功或失敗時監聽會自動觸發注冊的監聽事件。

Channel

Netty網絡通信的元件,能夠用于執行網絡I/O操作。Channel為使用者提供:

  • 目前網絡連接配接的通道的狀态(例如是否打開?是否已連接配接?)
  • 網絡連接配接的配置參數 (例如接收緩沖區大小)
  • 提供異步的網絡I/O操作(如建立連接配接,讀寫,綁定端口),異步調用意味着任何I / O調用都将立即傳回,并且不保證在調用結束時所請求的I / O操作已完成。調用立即傳回一個ChannelFuture執行個體,通過注冊監聽器到ChannelFuture上,可以I / O操作成功、失敗或取消時回調通知調用方。
  • 支援關聯I/O操作與對應的處理程式

不同協定、不同的阻塞類型的連接配接都有不同的 Channel 類型與之對應,下面是一些常用的 Channel 類型

  • NioSocketChannel,異步的用戶端 TCP Socket 連接配接
  • NioServerSocketChannel,異步的伺服器端 TCP Socket 連接配接
  • NioDatagramChannel,異步的 UDP 連接配接
  • NioSctpChannel,異步的用戶端 Sctp 連接配接
  • NioSctpServerChannel,異步的 Sctp 伺服器端連接配接 這些通道涵蓋了 UDP 和 TCP網絡 IO以及檔案 IO.

Selector

Netty基于Selector對象實作I/O多路複用,通過 Selector, 一個線程可以監聽多個連接配接的Channel事件, 當向一個Selector中注冊Channel 後,Selector 内部的機制就可以自動不斷地查詢(select) 這些注冊的Channel是否有已就緒的I/O事件(例如可讀, 可寫, 網絡連接配接完成等),這樣程式就可以很簡單地使用一個線程高效地管理多個 Channel 。

NioEventLoop

NioEventLoop中維護了一個線程和任務隊列,支援異步送出執行任務,線程啟動時會調用NioEventLoop的run方法,執行I/O任務和非I/O任務:

  • I/O任務 即selectionKey中ready的事件,如accept、connect、read、write等,由processSelectedKeys方法觸發。
  • 非IO任務 添加到taskQueue中的任務,如register0、bind0等任務,由runAllTasks方法觸發。

兩種任務的執行時間比由變量ioRatio控制,預設為50,則表示允許非IO任務執行的時間與IO任務的執行時間相等。

NioEventLoopGroup

NioEventLoopGroup,主要管理eventLoop的生命周期,可以了解為一個線程池,内部維護了一組線程,每個線程(NioEventLoop)負責處理多個Channel上的事件,而一個Channel隻對應于一個線程。

ChannelHandler

ChannelHandler是一個接口,處理I / O事件或攔截I / O操作,并将其轉發到其ChannelPipeline(業務處理鍊)中的下一個處理程式。

ChannelHandler本身并沒有提供很多方法,因為這個接口有許多的方法需要實作,友善使用期間,可以繼承它的子類:

  • ChannelInboundHandler用于處理入站I / O事件
  • ChannelOutboundHandler用于處理出站I / O操作

或者使用以下擴充卡類:

  • ChannelInboundHandlerAdapter用于處理入站I / O事件
  • ChannelOutboundHandlerAdapter用于處理出站I / O操作
  • ChannelDuplexHandler用于處理入站和出站事件

ChannelHandlerContext

儲存Channel相關的所有上下文資訊,同時關聯一個ChannelHandler對象

ChannelPipline

儲存ChannelHandler的List,用于處理或攔截Channel的入站事件和出站操作。ChannelPipeline實作了一種進階形式的攔截過濾器模式,使使用者可以完全控制事件的處理方式,以及Channel中各個的ChannelHandler如何互相互動。

下圖引用Netty的Javadoc4.1中ChannelPipline的說明,描述了ChannelPipeline中ChannelHandler通常如何處理I/O事件。I/O事件由ChannelInboundHandler或ChannelOutboundHandler處理,并通過調用ChannelHandlerContext中定義的事件傳播方法(例如ChannelHandlerContext.fireChannelRead(Object)和ChannelOutboundInvoker.write(Object))轉發到其最近的處理程式。

1萬2千字長文助力春招 | Netty面試篇

入站事件由自下而上方向的入站處理程式處理,如圖左側所示。入站Handler處理程式通常處理由圖底部的I / O線程生成的入站資料。通常通過實際輸入操作(例如SocketChannel.read(ByteBuffer))從遠端讀取入站資料。

出站事件由上下方向處理,如圖右側所示。出站Handler處理程式通常會生成或轉換出站傳輸,例如write請求。I/O線程通常執行實際的輸出操作,例如SocketChannel.write(ByteBuffer)。

在 Netty 中每個 Channel 都有且僅有一個 ChannelPipeline 與之對應, 它們的組成關系如下:

1萬2千字長文助力春招 | Netty面試篇

一個 Channel 包含了一個 ChannelPipeline, 而 ChannelPipeline 中又維護了一個由 ChannelHandlerContext 組成的雙向連結清單, 并且每個 ChannelHandlerContext 中又關聯着一個 ChannelHandler。入站事件和出站事件在一個雙向連結清單中,入站事件會從連結清單head往後傳遞到最後一個入站的handler,出站事件會從連結清單tail往前傳遞到最前一個出站的handler,兩種類型的handler互不幹擾。

工作原理架構

初始化并啟動Netty服務端過程如下:

1萬2千字長文助力春招 | Netty面試篇
  • 基本過程如下:
  • 1 初始化建立2個NioEventLoopGroup,其中boosGroup用于Accetpt連接配接建立事件并分發請求, workerGroup用于處理I/O讀寫事件和業務邏輯
  • 2 基于ServerBootstrap(服務端啟動引導類),配置EventLoopGroup、Channel類型,連接配接參數、配置入站、出站事件handler
  • 3 綁定端口,開始工作

結合上面的介紹的Netty Reactor模型,介紹服務端Netty的工作架構圖:

1萬2千字長文助力春招 | Netty面試篇

server端包含1個Boss NioEventLoopGroup和1個Worker NioEventLoopGroup,NioEventLoopGroup相當于1個事件循環組,這個組裡包含多個事件循環NioEventLoop,每個NioEventLoop包含1個selector和1個事件循環線程。

每個Boss NioEventLoop循環執行的任務包含3步:

  • 1 輪詢accept事件
  • 2 處理accept I/O事件,與Client建立連接配接,生成NioSocketChannel,并将NioSocketChannel注冊到某個Worker NioEventLoop的Selector上 *3 處理任務隊列中的任務,runAllTasks。任務隊列中的任務包括使用者調用eventloop.execute或schedule執行的任務,或者其它線程送出到該eventloop的任務。

每個Worker NioEventLoop循環執行的任務包含3步:

  • 1 輪詢read、write事件;
  • 2 處I/O事件,即read、write事件,在NioSocketChannel可讀、可寫事件發生時進行處理
  • 3 處理任務隊列中的任務,runAllTasks。

其中任務隊列中的task有3種典型使用場景

  • 1 使用者程式自定義的普通任務
1萬2千字長文助力春招 | Netty面試篇
  • 2非目前reactor線程調用channel的各種方法 例如在推送系統的業務線程裡面,根據使用者的辨別,找到對應的channel引用,然後調用write類方法向該使用者推送消息,就會進入到這種場景。最終的write會送出到任務隊列中後被異步消費
  • 3 使用者自定義定時任務
1萬2千字長文助力春招 | Netty面試篇

總結

現在穩定推薦使用的主流版本還是Netty4,Netty5 中使用了 ForkJoinPool,增加了代碼的複雜度,但是對性能的改善卻不明顯,是以這個版本不推薦使用,官網也沒有提供下載下傳連結。

Netty 入門門檻相對較高,其實是因為這方面的資料較少,并不是因為他有多難,大家其實都可以像搞透 Spring 一樣搞透 Netty。在學習之前,建議先了解透整個架構原理結構,運作過程,可以少走很多彎路。