Netty面試題(2020最新版)

轉載自  Netty面試題(2020最新版)

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每次都會新建一個緩衝區對象。

Netty簡介

Netty是 一個異步事件驅動的網絡應用程序框架,用於快速開發可維護的高性能協議服務器和客戶端。

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)可以用下圖表示:

 

 

特點

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

問題

  • 當併發數較大時,需要創建大量線程來處理連接,系統資源佔用較大

  • 連接建立後,如果當前線程暫時沒有數據可讀,則線程就阻塞在read操作上,造成線程資源浪費

I/O複用模型

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

 

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

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 大神關於事件驅動模型解釋圖

主要包括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事件,處理程序執行非阻塞操作

 

 

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

  • 單Reactor單線程

  • 單Reactor多線程

  • 主從Reactor多線程

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

 

篇幅關係,這裏不再具體展開Reactor特性、優缺點比較,有興趣的讀者可以參考我之前另外一篇文章:《理解高性能網絡模型》https://www.jianshu.com/p/2965fca6bb8f

Netty線程模型

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

  • MainReactor負責客戶端的連接請求,並將請求轉交給SubReactor

  • SubReactor負責相應通道的IO讀寫請求

  • 非IO請求(具體邏輯處理)的任務則會直接寫入隊列,等待worker threads進行處理

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

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

  •  
  •  
  •  
  •  
  •  
EventLoopGroup bossGroup = new NioEventLoopGroup();EventLoopGroup workerGroup = new NioEventLoopGroup();ServerBootstrap server = new ServerBootstrap();server.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class)

上面代碼中的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對象已完成,則理解通知指定的監聽器

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

  •  
  •  
  •  
  •  
  •  
  •  
  •  
    serverBootstrap.bind(port).addListener(future -> {        if (future.isSuccess()) {            System.out.println(new Date() + ": 端口[" + port + "]綁定成功!");        } else {            System.err.println("端口[" + port + "]綁定失敗!");        }    });

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

Netty架構設計

前面介紹完Netty相關一些理論介紹,下面從功能特性、模塊組件、運作過程來介紹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))轉發到其最近的處理程序。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
                                                 I/O Request                                            via Channel or                                        ChannelHandlerContext                                                      |  +---------------------------------------------------+---------------+  |                           ChannelPipeline         |               |  |                                                  \|/              |  |    +---------------------+            +-----------+----------+    |  |    | Inbound Handler  N  |            | Outbound Handler  1  |    |  |    +----------+----------+            +-----------+----------+    |  |              /|\                                  |               |  |               |                                  \|/              |  |    +----------+----------+            +-----------+----------+    |  |    | Inbound Handler N-1 |            | Outbound Handler  2  |    |  |    +----------+----------+            +-----------+----------+    |  |              /|\                                  .               |  |               .                                   .               |  | ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()|  |        [ method call]                       [method call]         |  |               .                                   .               |  |               .                                  \|/              |  |    +----------+----------+            +-----------+----------+    |  |    | Inbound Handler  2  |            | Outbound Handler M-1 |    |  |    +----------+----------+            +-----------+----------+    |  |              /|\                                  |               |  |               |                                  \|/              |  |    +----------+----------+            +-----------+----------+    |  |    | Inbound Handler  1  |            | Outbound Handler  M  |    |  |    +----------+----------+            +-----------+----------+    |  |              /|\                                  |               |  +---------------+-----------------------------------+---------------+                  |                                  \|/  +---------------+-----------------------------------+---------------+  |               |                                   |               |  |       [ Socket.read() ]                    [ Socket.write() ]     |  |                                                                   |  |  Netty Internal I/O Threads (Transport Implementation)            |  +-------------------------------------------------------------------+

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

 

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

 

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

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

工作原理架構

初始化並啓動Netty服務端過程如下:

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
    public static void main(String[] args) {        // 創建mainReactor        NioEventLoopGroup boosGroup = new NioEventLoopGroup();        // 創建工作線程組        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        final ServerBootstrap serverBootstrap = new ServerBootstrap();        serverBootstrap                  // 組裝NioEventLoopGroup                 .group(boosGroup, workerGroup)                 // 設置channel類型爲NIO類型                .channel(NioServerSocketChannel.class)                // 設置連接配置參數                .option(ChannelOption.SO_BACKLOG, 1024)                .childOption(ChannelOption.SO_KEEPALIVE, true)                .childOption(ChannelOption.TCP_NODELAY, true)                // 配置入站、出站事件handler                .childHandler(new ChannelInitializer<NioSocketChannel>() {                    @Override                    protected void initChannel(NioSocketChannel ch) {                        // 配置入站、出站事件channel                        ch.pipeline().addLast(...);                        ch.pipeline().addLast(...);                    }    });
        // 綁定端口        int port = 8080;        serverBootstrap.bind(port).addListener(future -> {            if (future.isSuccess()) {                System.out.println(new Date() + ": 端口[" + port + "]綁定成功!");            } else {                System.err.println("端口[" + port + "]綁定失敗!");            }        });}

基本過程如下:

  • 1 初始化創建2個NioEventLoopGroup,其中boosGroup用於Accetpt連接建立事件並分發請求, workerGroup用於處理I/O讀寫事件和業務邏輯

  • 2 基於ServerBootstrap(服務端啓動引導類),配置EventLoopGroup、Channel類型,連接參數、配置入站、出站事件handler

  • 3 綁定端口,開始工作

結合上面的介紹的Netty Reactor模型,介紹服務端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 用戶程序自定義的普通任務

  •  
  •  
  •  
  •  
  •  
  •  
  •  
ctx.channel().eventLoop().execute(new Runnable() {    @Override    public void run() {        //...    }});
  • 2 非當前reactor線程調用channel的各種方法 例如在推送系統的業務線程裏面,根據用戶的標識,找到對應的channel引用,然後調用write類方法向該用戶推送消息,就會進入到這種場景。最終的write會提交到任務隊列中後被異步消費。

  • 3 用戶自定義定時任務

  •  
  •  
  •  
  •  
  •  
  •  
  •  
ctx.channel().eventLoop().schedule(new Runnable() {    @Override    public void run() {
    }}, 60, TimeUnit.SECONDS);

總結

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

 

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

 

面試專場:

Redis面試題(2020最新版)

Tomcat面試題(2020最新版)

消息中間件MQ與RabbitMQ面試題(2020最新版)

Spring Cloud面試題(2020最新版)

Java經典面試題整理及答案詳解(一)

面試官問:爲什麼需要消息隊列?使用消息隊列有什麼好處?

Zookeeper超詳細的面試題

BAT大廠招聘Java 程序員的技術標準,你達到要求了嗎?

別再說你不會分佈式了,面試官能問的都在這了

Spring MVC 面經

Java面試----2018年MyBatis常見實用面試題整理

《圖解HTTP》面試知道這些就差不多了!

Spring經典面試題總結

面試:史上最全多線程面試題 !

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章