蘑菇街Netty面試專題及答案詳解

1.BIONIO AIO 的區別?

BIO:一個連接一個線程,客戶端有連接請求時服務器端就需要啓動一個線程進行處理。線 程開銷大。
 
僞異步 IO:將請求連接放入線程池,一對多,但線程還是很寶貴的資源。
 
NIO:一個請求一個線程,但客戶端發送的連接請求都會註冊到多路複用器上,多路複用 器輪詢到連接有 I/O 請求時才啓動一個線程進行處理。
 
AIO:一個有效請求一個線程,客戶端的 I/O 請求都是由 OS 先完成了再通知服務器應用去 啓動線程進行處理,
 
BIO 是面向流的,NIO 是面向緩衝區的;BIO 的各種流是阻塞的。而 NIO 是非阻塞的;BIO 的 Stream 是單向的,而 NIO channel 是雙向的。
 
NIO 的特點:事件驅動模型、單線程處理多任務、非阻塞 I/OI/O 讀寫不再阻塞,而是返 回 0、基於 block 的傳輸比基於流的傳輸更高效、更高級的 IO 函數 zero-copyIO 多路複用 大大提高了 Java 網絡應用的可伸縮性和實用性。基於 Reactor 線程模型。
 
Reactor 模式中,事件分發器等待某個事件或者可應用或個操作的狀態發生,事件分發 器就把這個事件傳給事先註冊的事件處理函數或者回調函數,由後者來做實際的讀寫操 作。如在 Reactor 中實現讀:註冊讀就緒事件和相應的事件處理器、事件分發器等待事 件、事件到來,激活分發器,分發器調用事件對應的處理器、事件處理器完成實際的讀操 作,處理讀到的數據,註冊新的事件,然後返還控制權。
 

2.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 可使一個單獨的線程管理多個 Channelopen 方法可創建 Selectorregister 方法向 多路複用器器註冊通道,可以監聽的事件類型:讀、寫、連接、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():打開一個 SelectorServerSocketChannel.open(): 創建服務端的 Channelbind():綁定到某個端口上。並配置非阻塞模式;register():註冊 Channel 和關注的事件到 Selector 上;select()輪詢拿到已經就緒的事件
 

3.Netty 的特點?

 
一個高性能、異步事件驅動的 NIO 框架,它提供了對 TCPUDP 和文件傳輸的支持
 
使用更高效的 socket 底層,對 epoll 空輪詢引起的 cpu 佔用飆升在內部進行了處理,避免 了直接使用 NIO 的陷阱,簡化了 NIO 的處理方式。
 
採用多種 decoder/encoder 支持,對 TCP 粘包/分包進行自動化處理 可使用接受/處理線程池,提高連接效率,對重連、心跳檢測的簡單支持
 
可配置 IO 線程數、TCP 參數, TCP 接收和發送緩衝區使用直接內存代替堆內存,通過內存 池的方式循環利用 ByteBuf 通過引用計數器及時申請釋放不再引用的對象,降低了 GC 頻率
 
使用單線程串行化的方式,高效的 Reactor 線程模型
 
大量使用了 volitale、使用了 CAS 和原子類、線程安全類的使用、讀寫鎖的使用
 

4.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 只負責接入認證、握手等操作;
 

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

 
TCP 是以流的方式來處理數據,一個完整的包可能會被 TCP 拆分成多個包進行發送,也可 能把小的封裝成一個大的數據包發送。
 
TCP 粘包/分包的原因: 應用程序寫入的字節大小大於套接字發送緩衝區的大小,會發生拆包現象,而應用程序寫 入數據小於套接字緩衝區大小,網卡將應用多次寫入的數據發送到網絡上,這將會發生粘 包現象;
 
進行 MSS 大小的 TCP 分段,當 TCP 報文長度-TCP 頭部長度>MSS 的時候將發生拆包 以太網幀的 payload(淨荷)大於 MTU1500 字節)進行 ip 分片。
 
解決方法
消息定長:FixedLengthFrameDecoder
包尾增加特殊字符分割:行分隔符類:LineBasedFrameDecoder 或自定義分隔符類 : DelimiterBasedFrameDecoder將消息分爲消息頭和消息體:LengthFieldBasedFrameDecoder 類。分爲有頭部的拆包與粘 包、長度字段在前且有頭部的拆包與粘包、多擴展頭部的拆包與粘包。
 

6.瞭解哪幾種序列化協議?

 
序列化(編碼)是將對象序列化爲二進制形式(字節數組),主要用於網絡傳輸、數據持久 化等;而反序列化(解碼)則是將從網絡、磁盤等讀取的字節數組還原成原始對象,主要 用於網絡傳輸對象的解碼,以便完成遠程調用。
 
影響序列化性能的關鍵因素:序列化後的碼流大小(網絡帶寬的佔用)、序列化的性能 (CPU 資源佔用);是否支持跨語言(異構系統的對接和開發語言切換)。
 
Java 默認提供的序列化:無法跨語言、序列化後的碼流太大、序列化的性能差
 
XML,優點:人機可讀性好,可指定元素或特性的名稱。缺點:序列化數據只包含數據本 身以及類的結構,不包括類型標識和程序集信息;只能序列化公共屬性和字段;不能序列 化方法;文件龐大,文件格式複雜,傳輸佔帶寬。適用場景:當做配置文件存儲數據,實 時數據轉換。
 
JSON,是一種輕量級的數據交換格式,優點:兼容性高、數據格式比較簡單,易於讀寫、 序列化後數據較小,可擴展性好,兼容性好、與 XML 相比,其協議比較簡單,解析速度比 較快。缺點:數據的描述性比 XML 差、不適合性能要求爲 ms 級別的情況、額外空間開銷 比較大。適用場景(可替代XML):跨防火牆訪問、可調式性要求高、基於 Web browser 的 Ajax 請求、傳輸數據量相對小,實時性要求相對低(例如秒級別)的服務。
 
Fastjson,採用一種“假定有序快速匹配”的算法。優點:接口簡單易用、目前 java 語言中 最快的 json 庫。缺點:過於注重快,而偏離了“標準”及功能性、代碼質量不高,文檔不 全。適用場景:協議交互、Web 輸出、Android 客戶端Thrift,不僅是序列化協議,還是一個 RPC 框架。優點:序列化後的體積小, 速度快、支持 多種語言和豐富的數據類型、對於數據字段的增刪具有較強的兼容性、支持二進制壓縮編 碼。缺點:使用者較少、跨防火牆訪問時,不安全、不具有可讀性,調試代碼時相對困 難、不能與其他傳輸層協議共同使用(例如 HTTP)、無法支持向持久層直接讀寫數據,即 不適合做數據持久化序列化協議。適用場景:分佈式系統的 RPC 解決方案
 
AvroHadoop 的一個子項目,解決了 JSON 的冗長和沒有 IDL 的問題。優點:支持豐富的 數據類型、簡單的動態語言結合功能、具有自我描述屬性、提高了數據解析速度、快速可壓縮的二進制數據形式、可以實現遠程過程調用 RPC、支持跨編程語言實現。缺點:對於 習慣於靜態類型語言的用戶不直觀。適用場景:在 Hadoop 中做 HivePig 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
 

7.如何選擇序列化協議?

 
具體場景
對於公司間的系統調用,如果性能要求在 100ms 以上的服務,基於 XML SOAP 協議是一 個值得考慮的方案。
 
基於 Web browser Ajax,以及 Mobile app 與服務端之間的通訊,JSON 協議是首選。對於 性能要求不太高,或者以動態類型語言爲主,或者傳輸數據載荷很小的的運用場景,JSON 也是非常不錯的選擇。
 
對於調試環境比較惡劣的場景,採用 JSON XML 能夠極大的提高調試效率,降低系統開 發成本。
 
當對性能和簡潔性有極高要求的場景,ProtobufThriftAvro 之間具有一定的競爭關係。
 
對於 T 級別的數據的持久化應用場景,Protobuf Avro 是首要選擇。如果持久化後的數據 存儲在 hadoop 子項目裏,Avro 會是更好的選擇。
對於持久層非 Hadoop 項目,以靜態類型語言爲主的應用場景,Protobuf 會更符合靜態類型語言工程師的開發習慣。由於 Avro 的設計理念偏向於動態類型語言,對於動態語言爲主 的應用場景,Avro 是更好的選擇。
 
如果需要提供一個完整的 RPC 解決方案,Thrift 是一個好的選擇。
如果序列化之後需要支持不同的傳輸層協議,或者需要跨防火牆訪問的高性能場景, Protobuf 可以優先考慮。
 
protobuf 的數據類型有多種:booldoublefloatint32int64stringbytesenum、 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()方法
 

8.Netty 的零拷貝實現?

 
Netty 的接收和發送 ByteBuffer 採用 DIRECT BUFFERS,使用堆外直接內存進行 Socket 讀 寫,不需要進行字節緩衝區的二次拷貝。堆內存多了一次內存拷貝,JVM 會將堆內存 Buffer 拷貝一份到直接內存中,然後才寫入 Socket 中。ByteBuffer ChannelConfig 分配, 而 ChannelConfig 創建 ByteBufAllocator 默認使用 Direct Buffer
 
CompositeByteBuf 類可以將多個 ByteBuf 合併爲一個邏輯上的 ByteBuf, 避免了傳統通過 內存拷貝的方式將幾個小 Buffer 合併成一個大的 Buffer。addComponents 方法將 header與 body 合併爲一個邏輯上的 ByteBuf, 這兩個 ByteBuf CompositeByteBuf 內部都是單 獨存在的, CompositeByteBuf 只是邏輯上是一個整體
 
通過 FileRegion 包裝的 FileChannel.tranferTo 方法 實現文件傳輸, 可以直接將文件緩衝區 的數據發送到目標 Channel,避免了傳統通過循環 write 方式導致的內存拷貝問題。
 
通過 wrap 方法, 我們可以將 byte[] 數組、ByteBufByteBuffer 等包裝成一個 Netty ByteBuf 對象, 進而避免了拷貝操作。
 
Selector       BUG:若 Selector 的輪詢結果爲空,也沒有 wakeup 或新消息處理,則發生空輪 詢,CPU 使用率 100%
 
Netty 的解決辦法:對 Selector select 操作週期進行統計,每完成一次空的 select 操作進行一次計數,若在某個週期內連續發生 N 次空輪詢,則觸發了 epoll 死循環 bug。重建 Selector,判斷是否是其他線程發起的重建請求,若不是則將SocketChannel 從舊的 Selector 上去除註冊,重新註冊到新的 Selector 上,並將原來的 Selector 關閉。
 

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

 
心跳,對服務端:會定時清除閒置會話 inactive(netty5),對客戶端:用來檢測會話是否斷開,是否重來,檢測網絡延遲,其中 idleStateHandler 類 用來檢測會話狀態
 
串行無鎖化設計,即消息的處理儘可能在同一個線程內完成,期間不進行線程切換,這樣 就避免了多線程競爭和同步鎖。表面上看,串行化設計似乎 CPU 利用率不高,併發程度不夠。但是,通過調整 NIO 線程池的線程參數,可以同時啓動多個串行化的線程並行運行, 這種局部無鎖化的串行線程設計相比一個隊列-多個工作線程模型性能更優。
 
可靠性,鏈路有效性檢測:鏈路空閒檢測機制,讀/寫空閒超時機制;內存保護機制:通過 內存池重用 ByteBuf;ByteBuf 的解碼保護;優雅停機:不再接收新消息、退出前的預處理操 作、資源的釋放操作。
 
Netty 安全性:支持的安全協議:SSL V2 V3TLSSSL 單向認證、雙向認證和第三方 CA 認證。
 
高效併發編程的體現:volatile 的大量、正確使用;CAS 和原子類的廣泛使用;線程安全容 器的使用;通過讀寫鎖提升併發性能。IO 通信性能三原則:傳輸(AIO)、協議(Http)、線 程(主從多線程) 流量整型的作用(變壓器):防止由於上下游網元性能不均衡導致下游網元被壓垮,業務流中斷;防止由於通信模塊接受消息過快,後端業務線程處理不及時導致撐死問題。
 
TCP 參數配置:SO_RCVBUF SO_SNDBUF:通常建議值爲 128K 或者 256K
 
SO_TCPNODELAYNAGLE 算法通過將緩衝區內的小封包自動相連,組成較大的封包,阻止 大量小封包的發送阻塞網絡,從而提高網絡應用效率。但是對於時延敏感的應用場景需要關閉該優化算法;
 

10.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 中的 bugselectCnt 用來記錄 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_READOP_WRITEOP_CONNECT 事件。
 
最後調用 runAllTasks 方法(非 IO 任務),該方法首先會調用 fetchFromScheduledTaskQueue 方法,把 scheduledTaskQueue 中已經超過延遲執行時間的任務移到 taskQueue 中等待被執行,然後依次從 taskQueue 中取任務執行,每執行 64 個任務,進行耗時檢查,如果已執行時間超過預先設定的執行時間,則停止執行非 IO 任務,避免非 IO 任務太多,影響 IO 任務 的執行。
 
每個 NioEventLoop 對應一個線程和一個 SelectorNioServerSocketChannel 會主動註冊到某
一個 NioEventLoop Selector 上,NioEventLoop 負責事件輪詢。
 
Outbound 事件都是請求事件, 發起者是 Channel,處理者是 unsafe,通過 Outbound 事件進行通知,傳播方向是 tail headInbound 事件發起者是 unsafe,事件的處理者是Channel, 是通知事件,傳播方向是從頭到尾。
 
內存管理機制,首先會預申請一大塊內存 ArenaArena 由許多 Chunk 組成,而每個 Chunk 默認由 2048 page 組成。Chunk 通過 AVL 樹的形式組織 Page,每個葉子節點表示一個 Page,而中間節點表示內存區域,節點自己記錄它在整個 Arena 中的偏移地址。當區域被 分配出去後,中間節點上的標記位會被標記,這樣就表示這個中間節點以下的所有節點都 已被分配了。大於 8k 的內存分配在 poolChunkList 中,而 PoolSubpage 用於分配小於 8k 的 內存,它會把一個 page 分割成多段,進行內存分配。
 
ByteBuf 的特點:支持自動擴容(4M),保證 put 方法不會拋出異常、通過內置的複合緩衝類型,實現零拷貝(zero-copy);不需要調用 flip()來切換讀/寫模式,讀取和寫入索引分開;方法鏈;引用計數基於 AtomicIntegerFieldUpdater 用於內存回收;PooledByteBuf 採用 二叉樹來實現一個內存池,集中管理內存的分配和釋放,不用每次使用都新建一個緩衝區
對象。UnpooledHeapByteBuf 每次都會新建一個緩衝區對象。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章