10個面試必問的Netty框架知識!
1.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中實現讀:註冊讀就緒事件和相應的事件處理器、事件分發器等待事件、事件到來,激活分發器,分發器調用事件對應的處理器、事件處理器完成實際的讀操作,處理讀到的數據,註冊新的事件,然後返還控制權。
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可使一個單獨的線程管理多個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()輪詢拿到已經就緒的事件
3.Netty的特點?
- 一個高性能、非同步事件驅動的NIO框架,它提供了對TCP、UDP和文件傳輸的支持
- 使用更高效的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(凈荷)大於MTU(1500位元組)進行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解決方案
- 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)
7.如何選擇序列化協議?
- 具體場景
- 對於公司間的系統調用,如果性能要求在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()方法
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[] 數組、ByteBuf、ByteBuffer等包裝成一個 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和V3,TLS,SSL單向認證、雙向認證和第三方CA認證。
- 高效並發編程的體現:volatile的大量、正確使用;CAS和原子類的廣泛使用;線程安全容器的使用;通過讀寫鎖提升並發性能。IO通信性能三原則:傳輸(AIO)、協議(Http)、線程(主從多線程)
- 流量整型的作用(變壓器):防止由於上下游網元性能不均衡導致下游網元被壓垮,業務流中斷;防止由於通信模塊接受消息過快,後端業務線程處理不及時導致撐死問題。
- TCP參數配置:SO_RCVBUF和SO_SNDBUF:通常建議值為128K或者256K;SO_TCPNODELAY:NAGLE演算法通過將緩衝區內的小封包自動相連,組成較大的封包,阻止大量小封包的發送阻塞網路,從而提高網路應用效率。但是對於時延敏感的應用場景需要關閉該優化演算法;