1. Netty理論四:TCP vs UDP
從數據結構上可以看出來,TCP比UDP要復雜的多。
我們上面說tcp是面向連接的,這是啥意思呢,簡單的說,tcp要發送數據,首先得先建立連接,而udp不需要,直接發送數據就行了。
TCP是全雙工的,即客戶端在給伺服器端發送信息的同時,伺服器端也可以給客戶端發送信息。
而半雙工的意思是A可以給B發,B也可以給A發,但是A在給B發的時候,B不能給A發,即不同時,為半雙工。
為什麼採用三次握手?而不是二次?
是為了防止已失效的連接請求報文段突然又傳送到了服務端,因而產生錯誤;比如有以下場景,當客戶端發出第一個
連接請求1並沒有丟失,而是在某些網路節點長時間滯留了,這時客戶端會認為超時,會再次發送連接請求2,服務端在接收到連接請求2以後建立了正常的連接,這時候失效請求1又到達了服務端,服務端會誤以為是客戶端又發出的一個連接請求,於是會再次建立連接,假定不採用三次握手,那服務端發出確認,新的連接就建立了。但是由於客戶端並沒有發出新的建立連接的請求,因此客戶端不會再新的連接上發送數據,而服務端卻以為新的連接已經建立了,在一直等待客戶端的數據,由此會導致服務端許多資源的浪費。採用了三次握手後,可以防止這個現象發生,在客戶端收到服務端對於自己的無效連接的應答後,並不會向服務端發出確認,服務端由於收不到確認,就可以認為此次連接是無效的。
第二次襲搭揮手完成後,客戶端到服務端的連接已經釋放,B不會再接收數據,A也不會再發送數據。這個時候只是客戶端不再發送數據,但是 B 可能還有未發送完的數據,所以需要等待服務端也主動關閉。
為什麼是四次?
關閉連接時,當Server收到FIN報文時,很可能並不會立即關閉socket,因為Server端可能還有消息未發出,所有其只能先回復一個ACK報文,告訴Client端,你的關閉請求我收到了;當Server把鋒喚所有的報文都發送完以後,Server才能給Client端發送FIN報文關閉連接,Client收到後應答ASK。所以需要四次握手
在四次握手後,Server端先進入TIME_WAIT狀態,然後過2MS(最大報文生存時間)才能進入CLOSE狀態。為啥?
因為網路是不可靠的,客戶端在第四次握手的ACK可能會丟失,所以TIME_WAIT狀態就是用來重發可能丟失的ACK報文
TCP是順序性,是通過協議中的序號來保證,每個包都有一個序號ID,
在建立連接的時候會商定起始 序號ID 是什麼,然後按照 序號ID 一個個發送。
tcp是通過應答/確認/ACK 以及 重傳機制來保證消息可靠傳輸的。
即在消息包發送後,要進行確認,當然,這個確認不是一個一個來的,而是會確認某個之前的 ID,表示都收到了,這種模式成為累計應答或累計確認。
確認是通過報文頭裡面的確認序號來保證的。
為了記錄所有發送的包和接收的包,TCP 需要在發送端和接收端分別來緩存這些記錄,發送端的緩存里是按照包的 ID 一個個排列,根據處理的情況分成四個部分
流量控制指的是發送端不能無限的往接收端發送數據(UDP就可以),為啥呢?
因為在 TCP 里,接收端在發送 ACK 的時候會帶上接收端緩沖區的窗口大小,叫 Advertised window,超過這個窗口,銀禪凱接收端就接收不過來了,發送端就不能發送數據了。這個窗口大概等於上面的第二部分加上第三部分,即 發送未確認 + 未發送可發送。
流量控制是點對點通信量的控制,是一個端到端的問題,主要就是抑制發送端發送數據的速率,以便接收端來得及接收。
tcp接收端緩沖區的大小是可以調試的,見 Netty高級功能(五):IoT百萬長連接性能調優
TCP通過一個定時器(timer)采樣了RTT並計算RTO,但是,**如果網路上的延時突然增加,那麼,TCP對這個事做出的應對只有重傳數據,然而重傳會導致網路的負擔更重,於是會導致更大的延遲以及更多的丟包,這就導致了惡性循環,最終形成「網路風暴」 —— TCP的擁塞控制機制就是用於應對這種情況。 **
擁塞控制的問題,也是通過窗口的大小來控制的,即為了在發送端調節所要發送的數據量,定義了一個「擁塞窗口」(Congestion Window),在發送數據時,將擁塞窗口的大小與接收端ack的窗口大小做比較,取較小者作為發送數據量的上限。
所以擁塞控制是防止過多的數據注入到網路中,可以使網路中的路由器或鏈路不致過載,是一個全局性的過程。
TCP 擁塞控制主要來避免兩種現象,包丟失和超時重傳,一旦出現了這些現象說明發送的太快了,要慢一點。
具體的方法就是發送端慢啟動,比如倒水,剛開始倒的很慢,漸漸變快。然後設置一個閾值,當超過這個值的時候就要慢下來
通過 TCP 連接傳輸的數據無差錯,不丟失,不重復,且按順序到達。
第一層:物理層
第二層:數據鏈路層 802.2、802.3ATM、HDLC、FRAME RELAY
第三層:網路層 IP 、IPX、ARP、APPLETALK、ICMP
第四層:傳輸層 TCP、UDP 、SPX
第五層:會話層 RPC、SQL 、NFS 、X WINDOWS、ASP
第六層:表示層 ASCLL、PICT、TIFF、JPEG、 MIDI、MPEG
第七層:應用層 HTTP,FTP ,SNMP等
2. netty內存池
1:PooledByteBufAllocator 內存池入口,應用通過該類從內存池中申請內存
PoolThreadCache:線程緩存池
Recycler:上文的Recycler是個對象池,儲存的是相應類型的堆中對象的集合。
2:PooledByteBufAllocator獲取bytebuf步驟:
1)如果是pool類型,先在線程對象池中獲取一個相應類型的poolBuffer對象,這個對象是在堆中的,也是返回的對象,然後會使用PoolThreadCache這個線程內存池,獲取一塊合適大小的內存。接下來根據這塊內存的信息對這個poolBuffer對象進行init,比如設置內存的address,length等,用戶就可以通過這個buffer對象對相應的內存進行操作。
2)注意區別線程對象池和線程內存池。線程對象池指同一類型的對象集合,用戶也只能通過對象來進行操作,比如讀寫數據。而線程內存池是許多內存塊的集合,用戶通過對象讀寫數據還是要定位到實際的內存地址(虛擬內存地址)。內存池就通過把對象和一塊內存綁定,對象的讀寫操作都會反應到這塊內存上。
3)內存是有限的,為了最大化內存的利用率以及提高內存分配回收的效率,netty實現了類似jemalloc內存分配的方式給對象分配內存。
4)獲取一個buffer對象-->給buffer對象屬性賦值(賦的值就是內存的地址、大小)
5) 比如現在需要分配一個UnpooledHeapByteBuf類型的ByteBuf對象,其初始大小為20,最大容量為100,因為是unpooled,意味著這各類型的ByteBuf是沒有對象池的,需要的時候直接new一個即可
(1)new UnpooledHeapByteBuf(this,initialCapacity,maxCapacity);
單純的一個沒有經過初始化(成員變數沒有賦值)的ByteBuf是不能進行讀寫的,因為其讀寫方法都需要確切的讀寫內存地址的。對於UnpooledHeapByteBuf類型的ByteBuf,其是一個HeapByteBuf,Heap意味著這個ByteBuf的讀寫操作是在JVM堆上進行的,其讀寫內存地址需要在jvm對上進行分配,所以在初始化時要根據需要的大小創建一個Byte類型的數組Byte[]對UnpooledDirectByteBuf進行初始化,接下來對這個Buf的讀寫都會反應在這個位元組數組中。
6)對於UnpooledDirectByteBuf類型的ByteBuf,Direct意味著這個ByteBuf的讀寫實現方法是以直接內存為基礎進行實現的,其讀寫區域是在直接內存上,在初始化時構造一個DirectByteBuffer對象(nio中的ByteBuffer)賦值給UnpooledDirectByteBuf的buf屬性,接下來對這個UnpooledDirectByteBuf類型對象的讀寫都會反應到這個ByteBuffer上。
7)對於pool類型的,
3. Netty源碼_UnpooledDirectByteBuf詳解
本篇文章我們講解緩存區 ByteBuf 八大主要類型中兩種,未池化直接緩沖區 UnpooledDirectByteBuf 和 未池化不安全直接緩沖區 UnpooledUnsafeDirectByteBuf 。
UnpooledDirectByteBuf 一個基於 NIO ByteBuffer 的緩沖區。
建議使用 UnpooledByteBufAllocator.directBuffer(int, int) , Unpooled.directBuffer(int) 和 Unpooled.wrappedBuffer(ByteBuffer) ;而不是虛嫌顯式調用構造函數。
有四個成員屬性:
通過 allocateDirect(initialCapacity) 方法創建一個新的 NIO 緩存區實例來初始化此緩存區對象。
利用現有的差亮手 NIO 緩存區創建此緩存區。
通過 NIO 緩存區 buffer 對應方法獲取基本數據類型數據。
根據目標緩存區 dst 類型不同,處理的方式也不同。
你會發現這些方法都是獲取此緩存區對應 NIO 緩存區 ByteBuffer 對象,調用 ByteBuffer 對象的方法,與 IO 流的交互,進行數據傳輸
和 get 系列方法一樣, set 系列的實現也是靠 NIO 緩存區 ByteBuffer 對應方法。
剩餘方法也幾乎都是和 NIO 緩存區 ByteBuffer 有關,而且也不難,就不做過多介紹了。
UnpooledDirectByteBuf 主要是通過 NIO 緩存區 buffer 來存儲數據。而它獲取和設置數據,也都是通過 NIO 緩存區對應方法實現的。
光看介紹,和鍵戚 UnpooledDirectByteBuf 沒有任何區別。它也是 UnpooledDirectByteBuf 的子類。
那麼 UnpooledUnsafeDirectByteBuf 和 UnpooledDirectByteBuf 不同處在那裡呢?
通過復習 setByteBuffer 方法,獲取 NIO 緩存區 buffer 對應的直接內存地址。
通過 UnsafeByteBufUtil 對應方法,直接從內存地址獲取對應基本類型數據。
通過 UnsafeByteBufUtil 對應方法,直接向內存地址設置對應基本類型數據。
只有這個類型 hasMemoryAddress() 方法才會返回 true 。
UnpooledUnsafeDirectByteBuf 就是通過直接從內存地址中獲取和設置數據的方式,提高性能。
4. netty DirectByteBuffer能不能預留大小,內存泄露問題
計是網路轉發不過做悔來 ,緩沖區數據沒有發送出去 ,業務層的數據又不停純盯正的wirte,所以就不斷的擴容,然後 就OOM。你可以這樣處理:
在業務層上做一個隊列則殲緩存發送的數據,當channel.isWirteAble()時候,你在channe.writeAndFlush();默認的 應該就可以吧DirectByteBuffe
5. Netty怎樣增大緩沖區
有現成的方法,你說的那種限制還根本沒涉及到粘包解碼的問題,而是netty底層限制了接收位元組緩存區大小為1024,下面這樣就行了
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(64, 1024, 65536))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new IdleStateHandler(10, 10, 0));
ch.pipeline().addLast(new HeartBeat());
ch.pipeline().addLast(new ByteStreamDecoder());
ch.pipeline().addLast(new ByteStreamEncoder());
ch.pipeline().addLast(new ServerStreamHandler());
}
});
6. Netty內存模型-PoolChunk
1概述
從netty 4開始,netty加入了內存池管理,採用內存池管理比普通的new ByteBuf性能提高了數十倍。首先介紹PoolChunk
2原理
PoolChunk主要負責內存塊的分配與回收,首先來看看兩個重要的術語。
上圖中是一個默認大小的chunk, 由2048個page組成了一個chunk,一個page的大小為8192, chunk之上有11層節點,最後一層節點數與page數量相等。每次內存分配需要保證內存的連續性,這樣才能簡單的操作分配到的內存,因此這里構造了一顆完整的平衡二叉樹,所有子節點的管理的內存也屬於其父節伍敏森點。如果我們想獲取一個8K的內存,則只需在第11層找一個可用節點即可,而如果我們需要16K的數據,則需要在第10層找一個可用節點。如果一個節點存在一個已經被分配的子節點,則該節點不能被分配,例如我們需要16K內存,這個時候id為2048的節點已經被分配,id為2049的節點未分配,就不能直接分配1024這個節點,因為這個節點下的內存只有8K了。
通過上面這個樹結構,我們可以看到每次內存分配都是8K*(2^n), 比如需要24K內存時,實際上會申請到一塊32K的內存。為了分配一個大小為chunkSize/(2^k)的內存段,需要在深度為k的層從左開始查找可用節點。如想分配16K的內存,chunkSize = 16M, 則k=10, 需要從第10層找一個空閑的節點分配內存。那如何快速分配到指定內存呢,netty使用memoryMap記錄分配情況。
初始化中memoryMap中key是上圖中節點值,value是該節點所在層數,對於節點512,其層數是9,則:
下面看看如何向PoolChunk申請一段內存:
當需要分配的內存大於pageSize時,使用allocateRun實現內存分配。否則使用allocateSubpage分配內存,主要是將一個page分割成多個element進行分配。其中針對請求的大小進行標准化處理(normCapacity是處理後的值),在分配內存是根據使用者請求的內存大小進行計算,匹配最接近的內存單元腔畝。在計算時分如下幾拿汪種情況:
接下來看看allocateRun是如何實現的:
在allocateNode中遍歷匹配:
如節點2048被分配出去,更新如下:
References
1. https://www.jianshu.com/p/c4bd37a3555b
2. https://www.cnblogs.com/pugongying017/p/9616333.html
3. https://blog.csdn.net/youaremoon
7. netty 性能 怎麼 測試 工具
測試方法
採用 mina 和 netty 各實現一個 基於 nio 的EchoServer,測試在不同大小網路報文下的性能宴搭渣表現
測試環境
客戶端-服務端:
model name: Intel(R) Core(TM) i5-2320 CPU @ 3.00GHz
cache size: 6144 KB
cpu cores: 4
jdk: 1.6.0_30-b12
network: 1000Mb
memory: -Xms256m -Xmx256m
Linux: centos 5.7, kernel 2.6.18-274.el5
測試工具:
jmeter v2.4
版本:
mina 2.0.7
netty 3.6.2.Final
配置:
mina
io-processor cpu 核數
executor cpu 核數
buffer 初始 buffer 大小,設置為 2048(2k)
netty
boss netty 默認配置 1
worker cpu 核數
executor cpu 核數
其實,從理論上來說, echo 型的應用不配置 executor 業務執行線程池會獲得更好的性能和更低的消耗,但考慮在真實業務應用中,真實的業務場景處理通晌悄常枝嫌涉及各種復雜邏輯計算,緩存、資料庫、外部介面訪問,為避免業務執行延時阻塞 io 線程執行導致吞吐降低,通常都會分離 io 處理線程 和 業務處理線程,因此我們的測試案例中也配置了業務執行線程池考查它們線程池的調度效能。
8. Netty writeAndFlush解析
Netty事件分為入站事件與出站事件,可以通過ChannelPipline或者ChannelHandlerContext進行事件傳播。通過ChannelPipline傳播入站事件,它將被從ChannelPipeline的頭部開始一直被傳播到ChannelPipeline的尾端,出站事件則從尾端開始傳遞到頭部。通過ChannelHandlerContext傳播入站事件,它將被從下一個ChannelHandler開始直至傳遞到尾端,出站事件則從下一個ChannelHandler直至傳遞到頭部。
Netty為了提高傳輸數據的效率,在寫出數據時,會先將數據(ByteBuf)緩存到ChannelOutboundBuffer中,等到調用flush方法時才會將ChannelOutboundBuffer中的數據寫入到socket緩沖區。
ChannelOutboundBuffer中有三個重要屬性:
從其屬性可以看出,ChannelOutboundBuffer內部是一個鏈表結構,裡面有三個指針:
ChannelOutboundBuffer中有兩個比較重要的方法,addMessage:將數據以鏈表形式緩存下來,addFlush:移動鏈表指針,將緩存的數據標記為已刷新,注意此時並沒有將數據寫入到socket緩沖區。接下來我們看下兩個方法的實現:
我們進入其addMessage方法分析下它是怎麼緩存數據的:
當第一次添加數據數,會將數據封裝為Entry,此時tailEntry、unflushedEntry指針指向這個Entry,flushedEntry指針此時為null。每次添加數據都會生成新的Entry,並將tailEntry指針指向該Entry,而unflushedEntry指針則一直指向最初添加的Entry,我們通過畫圖展示下:
第一次添加:
第N次添加:
為了防止緩存數據過大,Netty對緩存數據的大小做了限制:
addMessage方法最後會調用incrementPendingOutboundBytes方法記錄已緩存的數據大小(totalPendingSize),如果該大小超過了寫緩沖區高水位閾值(默認64K),則更新不可寫標志(unwritable),並傳播Channel可寫狀態發生變化事件:fireChannelWritabilityChanged:
移動鏈表指針,將緩存的數據標記為已刷新,並設置每個數據節點狀態為不可取消:
執行完addFlush方法後,鏈表圖示如下:
通過ChannelHandlerContext#writeAndFlush方法來分析下Netty是如何將數據通過網路進行傳輸的:
ChannelHandlerContext#writeAndFlush方法最終會調用其子類AbstractChannelHandlerContext的writeAndFlush方法:
主要邏輯在write方法中:
write方法主要做了兩件事,一是:找到下一個ChannelHandlerContext。二是:調用下一個ChannelHandlerContext的w riteAndFlush方法傳播事件。writeAndFlush方法是一個出站事件,前面我們也講過對於出站事件,通過ChannelHandlerContext進行事件傳播,事件是從pipline鏈中找到當前ChannelHandlerContext的下一個ChannelHandlerContext進行傳播直至頭部(HeadContext),在這期間我們需要自定義編碼器對傳輸的Java對象進行編碼,轉換為ByteBuf對象,最終事件會傳遞到HeadContext進行處理。
invokeWriteAndFlush方法主要做了兩件事,一是:調用invokeWrite0方法將數據放入Netty緩沖區中(ChannelOutboundBuffer),二是:調用invokeFlush0方法將緩沖區數據通過NioSocketChannel寫入到socket緩沖區。
invokeWrite0方法內部會調用ChannelOutboundHandler#write方法:
前面說過,出站事件最終會傳播到HeadContext,在傳播到HeadContext之前我們需要自定義編碼器對Java對象進行編碼,將Java對象編碼為ByteBuf,關於編碼器本章節暫不進行解析。我們進入HeadContext的write方法:
HeadContext#write方法中會調用AbstractChannelUnsafe#write方法:
該方法主要做了三件事情,一:對數據進行過濾轉換,二:估測數據大小,三:緩存數據。我們先看下filterOutboundMessage方法:
一、對數據進行過濾轉換:
filterOutboundMessage方法首先會對數據進行過濾,如果數據不是ByteBuf或者FileRegion類型,則直接拋出異常。如果數據是ByteBuf類型,則判斷數據是否為直接直接內存,如果不是則轉換為直接內存以提升性能。
二、估測數據大小:
三、緩存數據:
最後會調用ChannelOutboundBuffer#addMessage方法將數據緩存到鏈表中,關於addMessage方法可以回顧下文章中的Netty緩沖區部分。
回到AbstractChannelHandlerContext#invokeWriteAndFlush方法,方法內部在調用完invokeWrite0方法將數據放入到緩存後,會調用invokeFlush0方法,將緩存中的數據寫入到socket緩沖區。invokeFlush0方法內部會調用ChannelOutboundHandler#flush方法:
flush方法最終會將事件傳播到HeadContext的flush方法:
HeadContext#flush方法中會調用AbstractChannelUnsafe#flush方法:
方法主要做了兩件事,一:調用ChannelOutboundBuffer#addFlush方法,移動鏈表指針,將緩存的數據標記為已刷新。二:調用flush0方法將緩存中數據寫入到socket緩沖區。關於addFlush方法可以看文中Netty緩沖區部分,我們直接進入flush0方法:
flush0方法主要做了兩件事,一:判斷是否有掛起的刷新。二:調用父類flush0方法。
一、判斷是否有掛起的刷新
文中提到寫入數據時,當socket緩沖區沒有可用空間時會設置不可寫狀態,並注冊OP_WRITE事件,等待socket緩沖區有空閑空間時會觸發forceFlush,我們進入到isFlushPending方法看下方法是如何判斷的:
二、調用父類flush0方法
寫入socket緩沖區的具體邏輯在AbstractNioChannel#AbstractNioUnsafe父類AbstractChannel#AbstractUnsafe中:
核心邏輯在doWrite方法中,我們進入到AbstractChannel子類NioSocketChannel的doWrite方法看下具體實現:
NioSocketChannel#doWrite方法根據nioBufferCnt大小執行不同的寫邏輯,如果為0則調用AbstractNioByteChannel#doWrite方法。如果nioBufferCnt為1或者大於1,則調用NioSocketChannel不同的重載方法進行處理。注意,寫數據時自旋次數默認為16,也就是說如果執行16次write後仍有數據未寫完,則調用incompleteWrite方法將flush操作封裝為一個任務,放入到隊列中,目的是不阻塞其他任務。另外,如果調用NioSocketChannel#write方法後,返回的localWrittenBytes為0,則表示socket緩沖區空間不足,則注冊OP_WRITE事件,等待有可用空間時會觸發該事件,然後調用forceFlush方法繼續寫入數據。
9. Integer 的常量緩存池
Integer中有個靜態內租帆部類IntegerCache,裡面有個弊畢雹cache[],也就是Integer常量池,常量池的大小為一個位元組(-128~127)
Byte,Short,Long 的緩存數攔池范圍默認都是: -128 到 127。可以看出,Byte的所有值都在緩存區中,用它生成的相同值對象都是相等的。
所有整型(Byte,Short,Long)的比較規律與Integer是一樣的。
10. Netty源碼分析(七) PoolChunk
在分析源碼之前,我們先來了解讓圓一下Netty的內存管理機制。我們知道,jvm是自動管理內存的,這帶來了一些好處,在分配內存的時候可以方便管理,也帶來了一些問題。jvm每次分配內存的時候,都是先要去堆上申請內存空間進行分配,這就帶來了很大的性能上的開銷。當然,也可以使用堆外內存,Netty就用了堆外內存,但是內存的申請和釋放,依然需要性能的開銷。所以Netty實現了內存池來管理內存的申請和使用,提高了內存使用的效率。
PoolChunk就是Netty的內存管理的一種實現。羨虛Netty一次向系統申請16M的連續內存空間,這塊內存通過PoolChunk對象包裝,為了更細粒度的管理它,進一步的把這16M內存分成了2048個頁(pageSize=8k)。頁作為Netty內存管理的最基本的單位 ,所有的內存分配首先必須申請一塊空閑頁。Ps: 這里可能有一個疑問,如果申請1Byte的空間就分配一個頁是不是太浪費空間,在Netty中Page還會被細化用於專門處理小於4096Byte的空間申請 那麼這些Page需要通過某種數據結構跟演算法管理起來。
先來看看PoolChunk有哪些屬性
Netty採用完全二叉樹進行管理,樹中每個葉子節點表示一個Page,即樹高為12,中間節點表示頁節點的持有者。坦派塌有了上面的數據結構,那麼頁的申請跟釋放就非常簡單了,只需要從根節點一路遍歷找到可用的節點即可。主要來看看PoolChunk是怎麼分配內存的。
Netty的內存按大小分為tiny,small,normal,而類型上可以分為PoolChunk,PoolSubpage,小於4096大小的內存就被分成PoolSubpage。Netty就是這樣實現了對內存的管理。
PoolChunk就分析到這里了。