前沿拓展:
MSXML 4.0 Servi
MSXML全名是:「或更前版本。安裝Nero 8時要安裝最新版本MSXML 4.0,按正常的安裝就可以了對你的電腦沒有多大的影響
1.BIO、NIO和AIO的區(qū)別?BIO:一個連接一個線程,客戶端有連接請求時服務器端就需要啟動一個線程進行處理。線程開銷大。偽異步IO:將請求連接放入線程池,一對多,但線程還是很寶貴的資源。NIO:一個請求一個線程,但客戶端發(fā)送的連接請求都會注冊到多路復用器上,多路復用器輪詢到連接有I/O請求時才啟動一個線程進行處理。AIO:一個有效請求一個線程,客戶端的I/O請求都是由OS先完成了再通知服務器應用去啟動線程進行處理,BIO是面向流的,NIO是面向緩沖區(qū)的;BIO的各種流是阻塞的。而NIO是非阻塞的;BIO的Stream是單向的,而NIO的channel是雙向的。NIO的特點:**驅動模型、單線程處理多任務、非阻塞I/O,I/O讀寫不再阻塞,而是返回0、基于block的傳輸比基于流的傳輸更高效、更高級的IO函數zero-copy、IO多路復用大大提高了Java網絡應用的可伸縮性和實用性?;赗eactor線程模型。在Reactor模式中,**分發(fā)器等待某個**或者可應用或個**作的狀態(tài)發(fā)生,**分發(fā)器就把這個**傳給事先注冊的**處理函數或者回調函數,由后者來做實際的讀寫**作。如在Reactor中實現讀:注冊讀就緒**和相應的**處理器、**分發(fā)器等待**、**到來,激活分發(fā)器,分發(fā)器調用**對應的處理器、**處理器完成實際的讀**作,處理讀到的數據,注冊新的**,第二返還控制權。2.NIO的組成?
Buffer:與Channel進行交互,數據是從Channel讀入緩沖區(qū),從緩沖區(qū)寫入Channel中的
flip方法 : 反轉此緩沖區(qū),將position給limit,第二將position置為0,其實就是切換讀寫模式
clear方法 :清除此緩沖區(qū),將position置為0,把capacity的值給limit。
rewind方法 : 重繞此緩沖區(qū),將position置為0
DirectByteBuffer可減少一次系統空間到用戶空間的拷貝。但Buffer創(chuàng)建和銷毀的成本更高,不可控,通常會用內存池來提高性能。直接緩沖區(qū)主要分配給那些易受基礎系統的本機I/O **作影響的大型、持久的緩沖區(qū)。如果數據量比較小的中小應用情況下,可以考慮使用heapBuffer,由JVM進行管理。
Channel:表示 IO 源與目標打開的連接,是雙向的,但不能直接訪問數據,只能與Buffer 進行交互。通過源碼可知,FileChannel的read方法和write方法都導致數據**了兩次!
Selector可使一個單獨的線程管理多個Channel,open方法可創(chuàng)建Selector,register方法向多路復用器器注冊通道,可以**的**類型:讀、寫、連接、accept。注冊**后會產生一個SelectionKey:它表示SelectableChannel 和Selector 之間的注冊關系,wakeup方法:使尚未返回的第一個選擇**作立即返回,喚醒的原因是:注冊了新的channel或者**;channel關閉,取消注冊;優(yōu)先級更高的**觸發(fā)(如定時器**),希望及時處理。
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():創(chuàng)建服務端的Channel;bind():綁定到某個端口上。并配置非阻塞模式;register():注冊Channel和關注的**到Selector上;select()輪詢拿到已經就緒的**
3.Netty的特
一個高性能、異步**驅動的NIO框架,它提供了對TCP、UDP和文件傳輸的支持
使用更高效的socket底層,對epoll空輪詢引起的cpu占用飆升在內部進行了處理,避免了直接使用NIO的陷阱,簡化了NIO的處理方式。
采用多種decoder/encoder 支持,對TCP粘包/分包進行自動化處理
可使用接受/處理線程池,提高連接效率,對重連、心跳檢測的簡單支持
可配置IO線程數、TCP參數, TCP接收和發(fā)送緩沖區(qū)使用直接內存代替堆內存,通過內存池的方式循環(huán)利用ByteBuf
通過引用計數器及時申請釋放不再引用的對象,降低了GC頻率
使用單線程串行化的方式,高效的Reactor線程模型
大量使用了volitale、使用了CAS和原子類、線程安全類的使用、讀寫鎖的使用
使用
4.Netty的線程模型?
Netty通過Reactor模型基于多路復用器接收并處理用戶請求,內部實現了兩個線程池,boss線程池和work線程池,其中boss線程池的線程負責處理請求的accept**,當接收到accept**的請求時,把對應的socket封裝到一個NioSocketChannel中,并交給work線程池,其中work線程池負責請求的read和write**,由對應的Handler處理。
單線程模型:所有I/O**作都由一個線程完成,即多路復用、**分發(fā)和處理都是在一個Reactor線程上完成的。既要接收客戶端的連接請求,向服務端發(fā)起連接,又要發(fā)送/讀取請求或應答/響應消息。一個NIO 線程同時處理成百上千的鏈路,性能上無法支撐,速度慢,若線程進入**循環(huán),整個程序不可用,對于高負載、大并發(fā)的應用場景不合適。
多線程模型:有一個NIO 線程(Acceptor) 只負責**服務端,接收客戶端的TCP 連接請求;NIO 線程池負責網絡IO 的**作,即消息的讀取、解碼、編碼和發(fā)送;1 個NIO 線程可以同時處理N 條鏈路,但是1 個鏈路只對應1 個NIO 線程,這是為了防止發(fā)生并發(fā)**作問題。但在并發(fā)百萬客戶端連接或需要安全認證時,一個Acceptor 線程可能會存在性能不足問題。
主從多線程模型:Acceptor 線程用于綁定**端口,接收客戶端連接,將SocketChannel 從主線程池的Reactor 線程的多路復用器上移除,重新注冊到Sub 線程池的線程上,用于處理I/O 的讀寫等**作,從而保證mainReactor只負責接入認證、握手等**作;
5.TCP 粘包/拆包的原因及解決方法?
TCP是以流的方式來處理數據,一個完整的包可能會被TCP拆分成多個包進行發(fā)送,也可能把小的封裝成一個大的數據包發(fā)送。
TCP粘包/分包的原因:
應用程序寫入的字節(jié)大小大于套接字發(fā)送緩沖區(qū)的大小,會發(fā)生拆包現象,而應用程序寫入數據小于套接字緩沖區(qū)大小,網卡將應用多次寫入的數據發(fā)送到網絡上,這將會發(fā)生粘包現象;進行MSS大小的TCP分段,當TCP報文長度-TCP頭部長度>MSS的時候將發(fā)生拆包以太網幀的payload(凈荷)大于MTU(1500字節(jié))進行ip分片。
解決方法
消息定長:FixedLengthFrameDecoder類包尾增加特殊字符分割:行分隔符類:LineBasedFrameDecoder或自定義分隔符類 :DelimiterBasedFrameDecoder將消息分為消息頭和消息體:LengthFieldBasedFrameDecoder類。分為有頭部的拆包與粘包、長度字段在前且有頭部的拆包與粘包、多擴展頭部的拆包與粘包。6.了解哪幾種序列化協議?序列化(編碼)是將對象序列化為二進制形式(字節(jié)數組),主要用于網絡傳輸、數據持久化等;而反序列化(解碼)則是將從網絡、磁盤等讀取的字節(jié)數組還原成原始對象,主要用于網絡傳輸對象的解碼,以便完成遠程調用。影響序列化性能的關鍵因素:序列化后的碼流大小(網絡帶寬的占用)、序列化的性能(CPU資源占用);是否支持跨語言(異構系統的對接和開發(fā)語言切換)。Java默認提供的序列化:無法跨語言、序列化后的碼流太大、序列化的性能差XML,優(yōu)點:人機可讀性好,可指定元素或特性的名稱。缺點:序列化數據只包含數據本身以及類的結構,不包括類型標識和程序集信息;只能序列化公共屬性和字段;不能序列化方法;文件龐大,文件格式復雜,傳輸占帶寬。適用場景:當做配置文件存儲數據,實時數據轉換。JSON,是一種輕量級的數據交換格式,優(yōu)點:兼容性高、數據格式比較簡單,易于讀寫、序列化后數據較小,可擴展性好,兼容性好、與XML相比,其協議比較簡單,解析速度比較快。缺點:數據的描述性比XML差、不適合性能要求為ms級別的情況、額外空間開銷比較大。適用場景(可替代XML):跨防火墻訪問、可調式性要求高、基于Web browser的Ajax請求、傳輸數據量相對小,實時性要求相對低(例如秒級別)的服務。Fastjson,采用一種“假定有序快速匹配”的算法。優(yōu)點:接口簡單易用、目前java語言中最快的json庫。缺點:過于注重快,而偏離了“標準”及功能性、代碼質量不高,文檔不全。適用場景:協議交互、Web輸出、Android客戶端Thrift,不僅是序列化協議,還是一個RPC框架。優(yōu)點:序列化后的體積小, 速度快、支持多種語言和豐富的數據類型、對于數據字段的增刪具有較強的兼容性、支持二進制壓縮編碼。缺點:使用者較少、跨防火墻訪問時,不安全、不具有可讀性,調試代碼時相對困難、不能與其他傳輸層協議共同使用(例如HTTP)、無法支持向持久層直接讀寫數據,即不適合做數據持久化序列化協議。適用場景:分布式系統的RPC解決方案Avro,Hadoop的一個子項目,解決了JSON的冗長和沒有IDL的問題。優(yōu)點:支持豐富的數據類型、簡單的動態(tài)語言結合功能、具有自我描述屬性、提高了數據解析速度、快速可壓縮的二進制數據形式、可以實現遠程過程調用RPC、支持跨編程語言實現。缺點:對于習慣于靜態(tài)類型語言的用戶不直觀。適用場景:在Hadoop中做Hive、Pig和MapReduce的持久化數據格式。Protobuf,將數據結構以.proto文件進行描述,通過代碼生成工具可以生成對應數據結構的POJO對象和Protobuf相關的方法和屬性。優(yōu)點:序列化后碼流小,性能高、結構化數據存儲格式(XML JSON等)、通過標識字段的順序,可以實現協議的前向兼容、結構化的文檔更容易管理和維護。缺點:需要依賴于工具生成代碼、支持的語言相對較少,官方只支持Java 、C++ 、python。適用場景:對性能要求高的RPC調用、具有良好的跨防火墻的訪問屬性、適合應用層對象的持久化其它protostuff 基于protobuf協議,但不需要配置proto文件,直接導包即可**oss marshaling 可以直接序列化java類, 無須實java.io.Serializable接口Message pack 一個高效的二進制序列化格式Hessian 采用二進制協議的輕量級remoting onhttp工具kryo 基于protobuf協議,只支持java語言,需要注冊(Registration),第二序列化(Output),反序列化(Input)7.如何選擇序列化協議?具體場景對于公司間的系統調用,如果性能要求在100ms以上的服務,基于XML的SOAP協議是一個值得考慮的方案?;赪eb browser的Ajax,以及Mobile app與服務端之間的通訊,JSON協議是首選。對于性能要求不太高,或者以動態(tài)類型語言為主,或者傳輸數據載荷很小的的運用場景,JSON也是非常不錯的選擇。對于調試環(huán)境比較惡劣的場景,采用JSON或XML能夠極大的提高調試效率,降低系統開發(fā)成本。當對性能和簡潔性有極高要求的場景,Protobuf,Thrift,Avro之間具有一定的競爭關系。對于T級別的數據的持久化應用場景,Protobuf和Avro是首要選擇。如果持久化后的數據存儲在hadoop子項目里,Avro會是更好的選擇。對于持久層非Hadoop項目,以靜態(tài)類型語言為主的應用場景,Protobuf會更符合靜態(tài)類型語言工程師的開發(fā)習慣。由于Avro的設計理念偏向于動態(tài)類型語言,對于動態(tài)語言為主的應用場景,Avro是更好的選擇。如果需要提供一個完整的RPC解決方案,Thrift是一個好的選擇。如果序列化之后需要支持不同的傳輸層協議,或者需要跨防火墻訪問的高性能場景,Protobuf可以優(yōu)先考慮。protobuf的數據類型有多種:bool、double、float、int32、int64、string、bytes、enum、message。protobuf的限定符:required: 必須賦值,不能為空、optional:字段可以賦值,也可以不賦值、repeated: 該字段可以重復任意次數(包括0次)、枚舉;只能用指定的常量集中的一個值作為其值;protobuf的基本規(guī)則:每個消息中必須至少留有一個required類型的字段、包含0個或多個optional類型的字段;repeated表示的字段可以包含0個或多個數據;[1,15]之內的標識號在編碼的時候會占用一個字節(jié)(常用),[16,2047]之內的標識號則占用2個字節(jié),標識號一定不能重復、使用消息類型,也可以將消息嵌套任意多層,可用嵌套消息類型來代替組。protobuf的消息升級原則:不要更改任何已有的字段的數值標識;不能移除已經存在的required字段,optional和repeated類型的字段可以被移除,但要保留標號不能被重用。新添加的字段必須是optional或repeated。因為舊版本程序無法讀取或寫入新增的required限定符的字段。編譯器為每一個消息類型生成了一個.java文件,以及一個特殊的Builder類(該類是用來創(chuàng)建消息類接口的)。如:UserProto.User.Builder builder = UserProto.User.newBuilder();builder.build();Netty中的使用:ProtobufVarint32FrameDecoder 是用于處理半包消息的解碼類;ProtobufDecoder(UserProto.User.getDefaultInstance())這是創(chuàng)建的UserProto.java文件中的解碼類;ProtobufVarint32LengthFieldPrepender 對protobuf協議的消息頭上加上一個長度為32的×××字段,用于標志這個消息的長度的類;ProtobufEncoder 是編碼類將StringBuilder轉換為ByteBuf類型:copiedBuffer()方法8.Netty的零拷貝實現Netty的接收和發(fā)送ByteBuffer采用DIRECT BUFFERS,使用堆外直接內存進行Socket讀寫,不需要進行字節(jié)緩沖區(qū)的二次拷貝。堆內存多了一次內存拷貝,JVM會將堆內存Buffer拷貝一份到直接內存中,第二才寫入Socket中。ByteBuffer由ChannelConfig分配,而ChannelConfig創(chuàng)建ByteBufAllocator默認使用Direct BufferCompositeByteBuf 類可以將多個 ByteBuf 合并為一個邏輯上的 ByteBuf, 避免了傳統通過內存拷貝的方式將幾個**uffer合并成一個大的Buffer。addComponents方法將 header 與 body 合并為一個邏輯上的 ByteBuf, 這兩個 ByteBuf 在CompositeByteBuf 內部都是單獨存在的, CompositeByteBuf 只是邏輯上是一個整體通過 FileRegion 包裝的FileChannel.tranferTo方法 實現文件傳輸, 可以直接將文件緩沖區(qū)的數據發(fā)送到目標 Channel,避免了傳統通過循環(huán)write方式導致的內存拷貝問題。通過 wrap方法, 我們可以將 byte[] 數組、ByteBuf、ByteBuffer等包裝成一個 Netty ByteBuf 對象, 進而避免了拷貝**作。Selector BUG:若Selector的輪詢結果為空,也沒有wakeup或新消息處理,則發(fā)生空輪詢,CPU使用率100%,Netty的解決辦法:對Selector的select**作周期進行統計,每完成一次空的select**作進行一次計數,若在某個周期內連續(xù)發(fā)生N次空輪詢,則觸發(fā)了epoll**循環(huán)bug。重建Selector,判斷是否是其他線程發(fā)起的重建請求,若不是則將原SocketChannel從舊的Selector上去除注冊,重新注冊到新的Selector上,并將原來的Selector關閉。9.Netty的高性能表現在哪些方面?心跳,對服務端:會定時清除閑置會話inactive(netty5),對客戶端:用來檢測會話是否斷開,是否重來,檢測網絡延遲,其中idleStateHandler類 用來檢測會話狀態(tài)串行無鎖化設計,即消息的處理盡可能在同一個線程內完成,期間不進行線程切換,這樣就避免了多線程競爭和同步鎖。表面上看,串行化設計似乎CPU利用率不高,并發(fā)程度不夠。但是,通過調整NIO線程池的線程參數,可以同時啟動多個串行化的線程并行運行,這種局部無鎖化的串行線程設計相比一個隊列-多個工作線程模型性能更優(yōu)??煽啃?,鏈路有效性檢測:鏈路空閑檢測機制,讀/寫空閑超時機制;內存保護機制:通過內存池重用ByteBuf;ByteBuf的解碼保護;優(yōu)雅停機:不再接收新消息、退出前的預處理**作、資源的釋放**作。Netty安全性:支持的安全協議:SSL V2和V3,TLS,SSL單向認證、雙向認證和第三方CA認證。高效并發(fā)編程的體現:volatile的大量、正確使用;CAS和原子類的廣泛使用;線程安全容器的使用;通過讀寫鎖提升并發(fā)性能。IO通信性能三原則:傳輸(AIO)、協議(Http)、線程(主從多線程)流量整型的作用(變壓器):防止由于上下游網元性能不均衡導致下游網元被壓垮,業(yè)務流中斷;防止由于通信模塊接受消息過快,后端業(yè)務線程處理不及時導致撐**問題。TCP參數配置:SO_RCVBUF和SO_SNDBUF:通常建議值為128K或者256K;SO_TCPNODELAY:NAGLE算法通過將緩沖區(qū)內的小封包自動相連,組成較大的封包,阻止大量小封包的發(fā)送阻塞網絡,從而提高網絡應用效率。但是對于時延敏感的應用場景需要關閉該優(yōu)化算法;10.NIOEventLoopGroup源碼
NioEventLoopGroup(其實是MultithreadEventExecutorGroup) 內部維護一個類型為 EventExecutor children [], 默認大小是處理器核數 * 2, 這樣就構成了一個線程池,初始化EventExecutor時NioEventLoopGroup重載newChild方法,所以children元素的實際類型為NioEventLoop。線程啟動時調用SingleThreadEventExecutor的構造方法,執(zhí)行NioEventLoop類的run方法,第一會調用hasTasks()方法判斷當前taskQueue是否有元素。如果taskQueue中有元素,執(zhí)行 selectNow() 方法,最終執(zhí)行selector.selectNow(),該方**立即返回。如果taskQueue沒有元素,執(zhí)行 select(oldWakenUp) 方法select ( oldWakenUp) 方法解決了 Nio 中的 bug,selectCnt 用來記錄selector.select方法的執(zhí)行次數和標識是否執(zhí)行過selector.selectNow(),若觸發(fā)了epoll的空輪詢bug,則會反復執(zhí)行selector.select(timeoutMillis),變量selectCnt 會逐漸變大,當selectCnt 達到閾值(默認512),則執(zhí)行rebuildSelector方法,進行selector重建,解決cpu占用100%的bug。rebuildSelector方法先通過openSelector方法創(chuàng)建一個新的selector。第二將old selector的selectionKey執(zhí)行cancel。最后將old selector的channel重新注冊到新的selector中。rebuild后,需要重新執(zhí)行方法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中已經超過延遲執(zhí)行時間的任務移到taskQueue中等待被執(zhí)行,第二依次從taskQueue中取任務執(zhí)行,每執(zhí)行64個任務,進行耗時檢查,如果已執(zhí)行時間超過預先設定的執(zhí)行時間,則停止執(zhí)行非IO任務,避免非IO任務太多,影響IO任務的執(zhí)行。每個NioEventLoop對應一個線程和一個Selector,NioServerSocketChannel會主動注冊到某一個NioEventLoop的Selector上,NioEventLoop負責**輪詢。Outbound **都是請求**, 發(fā)起者是 Channel,處理者是 unsafe,通過 Outbound **進行通知,傳播方向是 tail到head。Inbound **發(fā)起者是 unsafe,**的處理者是 Channel, 是通知**,傳播方向是從頭到尾。內存管理機制,第一會預申請一大塊內存Arena,Arena由許多Chunk組成,而每個Chunk默認由2048個page組成。Chunk通過AVL樹的形式組織Page,每個葉子節(jié)點表示一個Page,而中間節(jié)點表示內存區(qū)域,節(jié)點自己記錄它在整個Arena中的偏移地址。當區(qū)域被分配出去后,中間節(jié)點上的標記位會被標記,這樣就表示這個中間節(jié)點以下的所有節(jié)點都已被分配了。大于8k的內存分配在poolChunkList中,而PoolSubpage用于分配小于8k的內存,它會把一個page分割成多段,進行內存分配。
ByteBuf的特點:支持自動擴容(4M),保證put方法不會拋出異常、通過內置的復合緩沖類型,實現零拷貝(zero-copy);不需要調用flip()來切換讀/寫模式,讀取和寫入索引分開;方法鏈;引用計數基于AtomicIntegerFieldUpdater用于內存回收;PooledByteBuf采用二叉樹來實現一個內存池,集中管理內存的分配和釋放,不用每次使用都新建一個緩沖區(qū)對象。UnpooledHeapByteBuf每次都會新建一個緩沖區(qū)對象。
本文首次分布在https://blog.51cto.com/9928699/2354140
拓展知識:
前沿拓展:
MSXML 4.0 Servi
MSXML全名是:「或更前版本。安裝Nero 8時要安裝最新版本MSXML 4.0,按正常的安裝就可以了對你的電腦沒有多大的影響
1.BIO、NIO和AIO的區(qū)別?BIO:一個連接一個線程,客戶端有連接請求時服務器端就需要啟動一個線程進行處理。線程開銷大。偽異步IO:將請求連接放入線程池,一對多,但線程還是很寶貴的資源。NIO:一個請求一個線程,但客戶端發(fā)送的連接請求都會注冊到多路復用器上,多路復用器輪詢到連接有I/O請求時才啟動一個線程進行處理。AIO:一個有效請求一個線程,客戶端的I/O請求都是由OS先完成了再通知服務器應用去啟動線程進行處理,BIO是面向流的,NIO是面向緩沖區(qū)的;BIO的各種流是阻塞的。而NIO是非阻塞的;BIO的Stream是單向的,而NIO的channel是雙向的。NIO的特點:**驅動模型、單線程處理多任務、非阻塞I/O,I/O讀寫不再阻塞,而是返回0、基于block的傳輸比基于流的傳輸更高效、更高級的IO函數zero-copy、IO多路復用大大提高了Java網絡應用的可伸縮性和實用性?;赗eactor線程模型。在Reactor模式中,**分發(fā)器等待某個**或者可應用或個**作的狀態(tài)發(fā)生,**分發(fā)器就把這個**傳給事先注冊的**處理函數或者回調函數,由后者來做實際的讀寫**作。如在Reactor中實現讀:注冊讀就緒**和相應的**處理器、**分發(fā)器等待**、**到來,激活分發(fā)器,分發(fā)器調用**對應的處理器、**處理器完成實際的讀**作,處理讀到的數據,注冊新的**,第二返還控制權。2.NIO的組成?
Buffer:與Channel進行交互,數據是從Channel讀入緩沖區(qū),從緩沖區(qū)寫入Channel中的
flip方法 : 反轉此緩沖區(qū),將position給limit,第二將position置為0,其實就是切換讀寫模式
clear方法 :清除此緩沖區(qū),將position置為0,把capacity的值給limit。
rewind方法 : 重繞此緩沖區(qū),將position置為0
DirectByteBuffer可減少一次系統空間到用戶空間的拷貝。但Buffer創(chuàng)建和銷毀的成本更高,不可控,通常會用內存池來提高性能。直接緩沖區(qū)主要分配給那些易受基礎系統的本機I/O **作影響的大型、持久的緩沖區(qū)。如果數據量比較小的中小應用情況下,可以考慮使用heapBuffer,由JVM進行管理。
Channel:表示 IO 源與目標打開的連接,是雙向的,但不能直接訪問數據,只能與Buffer 進行交互。通過源碼可知,FileChannel的read方法和write方法都導致數據**了兩次!
Selector可使一個單獨的線程管理多個Channel,open方法可創(chuàng)建Selector,register方法向多路復用器器注冊通道,可以**的**類型:讀、寫、連接、accept。注冊**后會產生一個SelectionKey:它表示SelectableChannel 和Selector 之間的注冊關系,wakeup方法:使尚未返回的第一個選擇**作立即返回,喚醒的原因是:注冊了新的channel或者**;channel關閉,取消注冊;優(yōu)先級更高的**觸發(fā)(如定時器**),希望及時處理。
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():創(chuàng)建服務端的Channel;bind():綁定到某個端口上。并配置非阻塞模式;register():注冊Channel和關注的**到Selector上;select()輪詢拿到已經就緒的**
3.Netty的特
一個高性能、異步**驅動的NIO框架,它提供了對TCP、UDP和文件傳輸的支持
使用更高效的socket底層,對epoll空輪詢引起的cpu占用飆升在內部進行了處理,避免了直接使用NIO的陷阱,簡化了NIO的處理方式。
采用多種decoder/encoder 支持,對TCP粘包/分包進行自動化處理
可使用接受/處理線程池,提高連接效率,對重連、心跳檢測的簡單支持
可配置IO線程數、TCP參數, TCP接收和發(fā)送緩沖區(qū)使用直接內存代替堆內存,通過內存池的方式循環(huán)利用ByteBuf
通過引用計數器及時申請釋放不再引用的對象,降低了GC頻率
使用單線程串行化的方式,高效的Reactor線程模型
大量使用了volitale、使用了CAS和原子類、線程安全類的使用、讀寫鎖的使用
使用
4.Netty的線程模型?
Netty通過Reactor模型基于多路復用器接收并處理用戶請求,內部實現了兩個線程池,boss線程池和work線程池,其中boss線程池的線程負責處理請求的accept**,當接收到accept**的請求時,把對應的socket封裝到一個NioSocketChannel中,并交給work線程池,其中work線程池負責請求的read和write**,由對應的Handler處理。
單線程模型:所有I/O**作都由一個線程完成,即多路復用、**分發(fā)和處理都是在一個Reactor線程上完成的。既要接收客戶端的連接請求,向服務端發(fā)起連接,又要發(fā)送/讀取請求或應答/響應消息。一個NIO 線程同時處理成百上千的鏈路,性能上無法支撐,速度慢,若線程進入**循環(huán),整個程序不可用,對于高負載、大并發(fā)的應用場景不合適。
多線程模型:有一個NIO 線程(Acceptor) 只負責**服務端,接收客戶端的TCP 連接請求;NIO 線程池負責網絡IO 的**作,即消息的讀取、解碼、編碼和發(fā)送;1 個NIO 線程可以同時處理N 條鏈路,但是1 個鏈路只對應1 個NIO 線程,這是為了防止發(fā)生并發(fā)**作問題。但在并發(fā)百萬客戶端連接或需要安全認證時,一個Acceptor 線程可能會存在性能不足問題。
主從多線程模型:Acceptor 線程用于綁定**端口,接收客戶端連接,將SocketChannel 從主線程池的Reactor 線程的多路復用器上移除,重新注冊到Sub 線程池的線程上,用于處理I/O 的讀寫等**作,從而保證mainReactor只負責接入認證、握手等**作;
5.TCP 粘包/拆包的原因及解決方法?
TCP是以流的方式來處理數據,一個完整的包可能會被TCP拆分成多個包進行發(fā)送,也可能把小的封裝成一個大的數據包發(fā)送。
TCP粘包/分包的原因:
應用程序寫入的字節(jié)大小大于套接字發(fā)送緩沖區(qū)的大小,會發(fā)生拆包現象,而應用程序寫入數據小于套接字緩沖區(qū)大小,網卡將應用多次寫入的數據發(fā)送到網絡上,這將會發(fā)生粘包現象;進行MSS大小的TCP分段,當TCP報文長度-TCP頭部長度>MSS的時候將發(fā)生拆包以太網幀的payload(凈荷)大于MTU(1500字節(jié))進行ip分片。
解決方法
消息定長:FixedLengthFrameDecoder類包尾增加特殊字符分割:行分隔符類:LineBasedFrameDecoder或自定義分隔符類 :DelimiterBasedFrameDecoder將消息分為消息頭和消息體:LengthFieldBasedFrameDecoder類。分為有頭部的拆包與粘包、長度字段在前且有頭部的拆包與粘包、多擴展頭部的拆包與粘包。6.了解哪幾種序列化協議?序列化(編碼)是將對象序列化為二進制形式(字節(jié)數組),主要用于網絡傳輸、數據持久化等;而反序列化(解碼)則是將從網絡、磁盤等讀取的字節(jié)數組還原成原始對象,主要用于網絡傳輸對象的解碼,以便完成遠程調用。影響序列化性能的關鍵因素:序列化后的碼流大?。ňW絡帶寬的占用)、序列化的性能(CPU資源占用);是否支持跨語言(異構系統的對接和開發(fā)語言切換)。Java默認提供的序列化:無法跨語言、序列化后的碼流太大、序列化的性能差XML,優(yōu)點:人機可讀性好,可指定元素或特性的名稱。缺點:序列化數據只包含數據本身以及類的結構,不包括類型標識和程序集信息;只能序列化公共屬性和字段;不能序列化方法;文件龐大,文件格式復雜,傳輸占帶寬。適用場景:當做配置文件存儲數據,實時數據轉換。JSON,是一種輕量級的數據交換格式,優(yōu)點:兼容性高、數據格式比較簡單,易于讀寫、序列化后數據較小,可擴展性好,兼容性好、與XML相比,其協議比較簡單,解析速度比較快。缺點:數據的描述性比XML差、不適合性能要求為ms級別的情況、額外空間開銷比較大。適用場景(可替代XML):跨防火墻訪問、可調式性要求高、基于Web browser的Ajax請求、傳輸數據量相對小,實時性要求相對低(例如秒級別)的服務。Fastjson,采用一種“假定有序快速匹配”的算法。優(yōu)點:接口簡單易用、目前java語言中最快的json庫。缺點:過于注重快,而偏離了“標準”及功能性、代碼質量不高,文檔不全。適用場景:協議交互、Web輸出、Android客戶端Thrift,不僅是序列化協議,還是一個RPC框架。優(yōu)點:序列化后的體積小, 速度快、支持多種語言和豐富的數據類型、對于數據字段的增刪具有較強的兼容性、支持二進制壓縮編碼。缺點:使用者較少、跨防火墻訪問時,不安全、不具有可讀性,調試代碼時相對困難、不能與其他傳輸層協議共同使用(例如HTTP)、無法支持向持久層直接讀寫數據,即不適合做數據持久化序列化協議。適用場景:分布式系統的RPC解決方案Avro,Hadoop的一個子項目,解決了JSON的冗長和沒有IDL的問題。優(yōu)點:支持豐富的數據類型、簡單的動態(tài)語言結合功能、具有自我描述屬性、提高了數據解析速度、快速可壓縮的二進制數據形式、可以實現遠程過程調用RPC、支持跨編程語言實現。缺點:對于習慣于靜態(tài)類型語言的用戶不直觀。適用場景:在Hadoop中做Hive、Pig和MapReduce的持久化數據格式。Protobuf,將數據結構以.proto文件進行描述,通過代碼生成工具可以生成對應數據結構的POJO對象和Protobuf相關的方法和屬性。優(yōu)點:序列化后碼流小,性能高、結構化數據存儲格式(XML JSON等)、通過標識字段的順序,可以實現協議的前向兼容、結構化的文檔更容易管理和維護。缺點:需要依賴于工具生成代碼、支持的語言相對較少,官方只支持Java 、C++ 、python。適用場景:對性能要求高的RPC調用、具有良好的跨防火墻的訪問屬性、適合應用層對象的持久化其它protostuff 基于protobuf協議,但不需要配置proto文件,直接導包即可**oss 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協議是首選。對于性能要求不太高,或者以動態(tài)類型語言為主,或者傳輸數據載荷很小的的運用場景,JSON也是非常不錯的選擇。對于調試環(huán)境比較惡劣的場景,采用JSON或XML能夠極大的提高調試效率,降低系統開發(fā)成本。當對性能和簡潔性有極高要求的場景,Protobuf,Thrift,Avro之間具有一定的競爭關系。對于T級別的數據的持久化應用場景,Protobuf和Avro是首要選擇。如果持久化后的數據存儲在hadoop子項目里,Avro會是更好的選擇。對于持久層非Hadoop項目,以靜態(tài)類型語言為主的應用場景,Protobuf會更符合靜態(tài)類型語言工程師的開發(fā)習慣。由于Avro的設計理念偏向于動態(tài)類型語言,對于動態(tài)語言為主的應用場景,Avro是更好的選擇。如果需要提供一個完整的RPC解決方案,Thrift是一個好的選擇。如果序列化之后需要支持不同的傳輸層協議,或者需要跨防火墻訪問的高性能場景,Protobuf可以優(yōu)先考慮。protobuf的數據類型有多種:bool、double、float、int32、int64、string、bytes、enum、message。protobuf的限定符:required: 必須賦值,不能為空、optional:字段可以賦值,也可以不賦值、repeated: 該字段可以重復任意次數(包括0次)、枚舉;只能用指定的常量集中的一個值作為其值;protobuf的基本規(guī)則:每個消息中必須至少留有一個required類型的字段、包含0個或多個optional類型的字段;repeated表示的字段可以包含0個或多個數據;[1,15]之內的標識號在編碼的時候會占用一個字節(jié)(常用),[16,2047]之內的標識號則占用2個字節(jié),標識號一定不能重復、使用消息類型,也可以將消息嵌套任意多層,可用嵌套消息類型來代替組。protobuf的消息升級原則:不要更改任何已有的字段的數值標識;不能移除已經存在的required字段,optional和repeated類型的字段可以被移除,但要保留標號不能被重用。新添加的字段必須是optional或repeated。因為舊版本程序無法讀取或寫入新增的required限定符的字段。編譯器為每一個消息類型生成了一個.java文件,以及一個特殊的Builder類(該類是用來創(chuàng)建消息類接口的)。如:UserProto.User.Builder builder = UserProto.User.newBuilder();builder.build();Netty中的使用:ProtobufVarint32FrameDecoder 是用于處理半包消息的解碼類;ProtobufDecoder(UserProto.User.getDefaultInstance())這是創(chuàng)建的UserProto.java文件中的解碼類;ProtobufVarint32LengthFieldPrepender 對protobuf協議的消息頭上加上一個長度為32的×××字段,用于標志這個消息的長度的類;ProtobufEncoder 是編碼類將StringBuilder轉換為ByteBuf類型:copiedBuffer()方法8.Netty的零拷貝實現Netty的接收和發(fā)送ByteBuffer采用DIRECT BUFFERS,使用堆外直接內存進行Socket讀寫,不需要進行字節(jié)緩沖區(qū)的二次拷貝。堆內存多了一次內存拷貝,JVM會將堆內存Buffer拷貝一份到直接內存中,第二才寫入Socket中。ByteBuffer由ChannelConfig分配,而ChannelConfig創(chuàng)建ByteBufAllocator默認使用Direct BufferCompositeByteBuf 類可以將多個 ByteBuf 合并為一個邏輯上的 ByteBuf, 避免了傳統通過內存拷貝的方式將幾個**uffer合并成一個大的Buffer。addComponents方法將 header 與 body 合并為一個邏輯上的 ByteBuf, 這兩個 ByteBuf 在CompositeByteBuf 內部都是單獨存在的, CompositeByteBuf 只是邏輯上是一個整體通過 FileRegion 包裝的FileChannel.tranferTo方法 實現文件傳輸, 可以直接將文件緩沖區(qū)的數據發(fā)送到目標 Channel,避免了傳統通過循環(huán)write方式導致的內存拷貝問題。通過 wrap方法, 我們可以將 byte[] 數組、ByteBuf、ByteBuffer等包裝成一個 Netty ByteBuf 對象, 進而避免了拷貝**作。Selector BUG:若Selector的輪詢結果為空,也沒有wakeup或新消息處理,則發(fā)生空輪詢,CPU使用率100%,Netty的解決辦法:對Selector的select**作周期進行統計,每完成一次空的select**作進行一次計數,若在某個周期內連續(xù)發(fā)生N次空輪詢,則觸發(fā)了epoll**循環(huán)bug。重建Selector,判斷是否是其他線程發(fā)起的重建請求,若不是則將原SocketChannel從舊的Selector上去除注冊,重新注冊到新的Selector上,并將原來的Selector關閉。9.Netty的高性能表現在哪些方面?心跳,對服務端:會定時清除閑置會話inactive(netty5),對客戶端:用來檢測會話是否斷開,是否重來,檢測網絡延遲,其中idleStateHandler類 用來檢測會話狀態(tài)串行無鎖化設計,即消息的處理盡可能在同一個線程內完成,期間不進行線程切換,這樣就避免了多線程競爭和同步鎖。表面上看,串行化設計似乎CPU利用率不高,并發(fā)程度不夠。但是,通過調整NIO線程池的線程參數,可以同時啟動多個串行化的線程并行運行,這種局部無鎖化的串行線程設計相比一個隊列-多個工作線程模型性能更優(yōu)。可靠性,鏈路有效性檢測:鏈路空閑檢測機制,讀/寫空閑超時機制;內存保護機制:通過內存池重用ByteBuf;ByteBuf的解碼保護;優(yōu)雅停機:不再接收新消息、退出前的預處理**作、資源的釋放**作。Netty安全性:支持的安全協議:SSL V2和V3,TLS,SSL單向認證、雙向認證和第三方CA認證。高效并發(fā)編程的體現:volatile的大量、正確使用;CAS和原子類的廣泛使用;線程安全容器的使用;通過讀寫鎖提升并發(fā)性能。IO通信性能三原則:傳輸(AIO)、協議(Http)、線程(主從多線程)流量整型的作用(變壓器):防止由于上下游網元性能不均衡導致下游網元被壓垮,業(yè)務流中斷;防止由于通信模塊接受消息過快,后端業(yè)務線程處理不及時導致撐**問題。TCP參數配置:SO_RCVBUF和SO_SNDBUF:通常建議值為128K或者256K;SO_TCPNODELAY:NAGLE算法通過將緩沖區(qū)內的小封包自動相連,組成較大的封包,阻止大量小封包的發(fā)送阻塞網絡,從而提高網絡應用效率。但是對于時延敏感的應用場景需要關閉該優(yōu)化算法;10.NIOEventLoopGroup源碼
NioEventLoopGroup(其實是MultithreadEventExecutorGroup) 內部維護一個類型為 EventExecutor children [], 默認大小是處理器核數 * 2, 這樣就構成了一個線程池,初始化EventExecutor時NioEventLoopGroup重載newChild方法,所以children元素的實際類型為NioEventLoop。線程啟動時調用SingleThreadEventExecutor的構造方法,執(zhí)行NioEventLoop類的run方法,第一會調用hasTasks()方法判斷當前taskQueue是否有元素。如果taskQueue中有元素,執(zhí)行 selectNow() 方法,最終執(zhí)行selector.selectNow(),該方**立即返回。如果taskQueue沒有元素,執(zhí)行 select(oldWakenUp) 方法select ( oldWakenUp) 方法解決了 Nio 中的 bug,selectCnt 用來記錄selector.select方法的執(zhí)行次數和標識是否執(zhí)行過selector.selectNow(),若觸發(fā)了epoll的空輪詢bug,則會反復執(zhí)行selector.select(timeoutMillis),變量selectCnt 會逐漸變大,當selectCnt 達到閾值(默認512),則執(zhí)行rebuildSelector方法,進行selector重建,解決cpu占用100%的bug。rebuildSelector方法先通過openSelector方法創(chuàng)建一個新的selector。第二將old selector的selectionKey執(zhí)行cancel。最后將old selector的channel重新注冊到新的selector中。rebuild后,需要重新執(zhí)行方法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中已經超過延遲執(zhí)行時間的任務移到taskQueue中等待被執(zhí)行,第二依次從taskQueue中取任務執(zhí)行,每執(zhí)行64個任務,進行耗時檢查,如果已執(zhí)行時間超過預先設定的執(zhí)行時間,則停止執(zhí)行非IO任務,避免非IO任務太多,影響IO任務的執(zhí)行。每個NioEventLoop對應一個線程和一個Selector,NioServerSocketChannel會主動注冊到某一個NioEventLoop的Selector上,NioEventLoop負責**輪詢。Outbound **都是請求**, 發(fā)起者是 Channel,處理者是 unsafe,通過 Outbound **進行通知,傳播方向是 tail到head。Inbound **發(fā)起者是 unsafe,**的處理者是 Channel, 是通知**,傳播方向是從頭到尾。內存管理機制,第一會預申請一大塊內存Arena,Arena由許多Chunk組成,而每個Chunk默認由2048個page組成。Chunk通過AVL樹的形式組織Page,每個葉子節(jié)點表示一個Page,而中間節(jié)點表示內存區(qū)域,節(jié)點自己記錄它在整個Arena中的偏移地址。當區(qū)域被分配出去后,中間節(jié)點上的標記位會被標記,這樣就表示這個中間節(jié)點以下的所有節(jié)點都已被分配了。大于8k的內存分配在poolChunkList中,而PoolSubpage用于分配小于8k的內存,它會把一個page分割成多段,進行內存分配。
ByteBuf的特點:支持自動擴容(4M),保證put方法不會拋出異常、通過內置的復合緩沖類型,實現零拷貝(zero-copy);不需要調用flip()來切換讀/寫模式,讀取和寫入索引分開;方法鏈;引用計數基于AtomicIntegerFieldUpdater用于內存回收;PooledByteBuf采用二叉樹來實現一個內存池,集中管理內存的分配和釋放,不用每次使用都新建一個緩沖區(qū)對象。UnpooledHeapByteBuf每次都會新建一個緩沖區(qū)對象。
本文首次分布在https://blog.51cto.com/9928699/2354140
拓展知識:
原創(chuàng)文章,作者:九賢生活小編,如若轉載,請注明出處:http:///32397.html