https://blog.csdn.net/feiyingHiei/article/details/78735754?utm_source=blogxgwz9
有源碼分析
在啟動Netty bootstrap的時候可以設置ChannelOption選項,其中ChannelOption中有一項WRITE_BUFFER_HIGH_WATER_MARK選項和WRITE_BUFFER_LOW_WATER_MARK選項,,此配置寫緩沖區(OutbounduBuffer)相關,此配置可以幫助用戶監控當前寫緩沖區的水位狀況,ChannelOutboundBuffer本身是無界的,如果水位控制不當的話就會造成占用大量的內存,今天准備結合代碼來看看這個配置究竟是有什么作用。
ChannelConfig默認的水位配置為低水位32K,高水位64K,如果用戶沒有配置就會使用默認配置。
即使使用了默認配置,沒有控制,則仍然會導致ChannelOutboundBuffer趨於無窮,水位只是提醒你,並不會操作,你不控制還是會爆掉
高水位的時候就會可以通知到業務handler中的WritabilityChanged方法,並且修改buffer的狀態,channel調用isWriteable的時候就會返回false,當前channel處於不可寫狀態。
如果低於該水位就會設置當前的channel為可寫,然后觸發可讀事件。
水位配置可以幫助我們監控緩沖區的使用情況,在寫數據的時候需要判斷當前channel是否可以繼續向緩沖區寫數據(isWriteable)。在之前的工作中出現過沒有正確判斷,而使用的編碼器默認使用的又是堆外內存,導致在不斷寫入緩存的時候堆外內存超過jvm配置最大值。
https://blog.csdn.net/qq_34772568/article/details/106524734
netty中的高低水位機制會在發送緩沖區的數據超過高水位時觸發channelWritabilityChanged事件同時將channel的寫狀態設置為false,但是這個寫狀態只是程序層面的狀態,程序還是可以繼續寫入數據。這與我們在第一點判斷的一致
當我們在netty中使用write方法發送數據時,這個數據其實是寫到了一個緩沖區中,並未直接發送給接收方,netty使用ChannelOutboundBuffer封裝出站消息的發送,所有的消息都會存儲在一個鏈表中,直到緩沖區被flush方法刷新,netty才會將數據真正發送出去。
netty默認設置的高水位為64KB,低水位為32KB.
https://my.oschina.net/u/3959468/blog/3018592 yet
通過以上分析可以看出,在直播高峰期,服務端向上萬客戶端推送消息時,發生了發送隊列積壓,引起內存泄漏,最終導致服務端頻繁 GC,無法正常處理業務。
服務端在進行消息發送的時候做保護,具體策略如下:
-
根據可接入的最大用戶數做客戶端並發接入數流控,需要根據內存、CPU 處理能力,以及性能測試結果做綜合評估。
-
設置消息發送的高低水位,針對消息的平均大小、客戶端並發接入數、JVM 內存大小進行計算,得出一個合理的高水位取值。服務端在推送消息時,對 Channel 的狀態進行判斷,如果達到高水位之后,Channel 的狀態會被 Netty 置為不可寫,此時服務端不要繼續發送消息,防止發送隊列積壓。
服務端基於上述策略優化了代碼,內存泄漏問題得到解決。
當發送隊列待發送的字節數組達到高水位上限時,對應的 Channel 就變為不可寫狀態。由於高水位並不影響業務線程調用 write 方法並把消息加入到待發送隊列中,因此,必須要在消息發送時對 Channel 的狀態進行判斷:當到達高水位時,Channel 的狀態被設置為不可寫,通過對 Channel 的可寫狀態進行判斷來決定是否發送消息。
https://www.jianshu.com/p/6c4a7cbbe2b5
在有些場景下,由於各種原因,會導致客戶端消息發送積壓,進而導致OOM。
- 1、當netty服務端並發壓力過大,超過了服務端的處理能力時,channel中的消息服務端不能及時消費,這時channel堵塞,客戶端消息就會堆積在發送隊列中
- 2、網絡瓶頸,當客戶端發送速度超過網絡鏈路處理能力,會導致客戶端發送隊列積壓
- 3、當對端讀取速度小於己方發送速度,導致自身TCP發送緩沖區滿,頻繁發生write 0字節時,待發送消息會在netty發送隊列中排隊
這三種情況下,如果客戶端沒有流控保護,這時候就很容易發生內存泄露。
io.netty.channel.AbstractChannelHandlerContext#writeAndFlush (java.lang.Object, io.netty.channel.ChannelPromise),如果發送方為業務線程,則將發送操作封裝成WriteTask(繼承Runnable),放到Netty的NioEventLoop中執行,當NioEventLoop無法完成如此多的消息的發送的時候,發送任務隊列積壓,進而導致內存泄漏。

經過一些系統處理操作,最終會調用io.netty.channel.ChannelOutboundBuffer#addMessage方法,將發送消息加入發送隊列(鏈表)。
https://www.jianshu.com/p/890525ff73cb great done
在Netty3的時候,upstream是在IO線程里執行的,而downstream是在業務線程里執行的。比如netty從網絡讀取一個包傳遞給你的handler的時候,你的handler部分的代碼是執行在IO線程里,而你的業務線程調用write向網絡寫出一些東西的時候,你的handler是執行在業務線程里。而Netty 4修改了這一模型。在Netty4里inbound(upstream)和outbound(downstream)都是執行在EventLoop(IO線程)里。也就是你如果在業務線程里通過channel.write向網絡寫出一些東西的時候,在某一點,netty4會往這個channel的EventLoop里提交一個寫出的任務。那也就是業務線程和IO線程是異步執行的。
因為序列化和業務線程異步執行,那么在write執行后並不表示user對象已經序列化了,如果這個時候修改了user對象那么傳遞到peer的對象可能就不再是你期望的那個user了。
我們就決定不再在handler里做序列化了,而是直接在業務線程里做。但是為了減少內存的拷貝,我們就期望在序列化的時候直接將字節流序列化到DirectByteBuf里,這樣通過socket寫出的時候就不進行拷貝了
DirectByteBuf的分配成本比HeapByteBuf的成本要高,為此Netty4借鑒jemalloc的思路實現了一個PooledByteBufAllocator。顧名思義,就是將DirectByteBuf池化起來,回收的時候不真正回收,分配的時候從池里取一個空閑的。
PooledByteBufAllocator為了減少鎖競爭,池是通過threadlocal來實現的。也就是分配的時候會從本線程(這里就是業務線程)的threadlocal里取。而channel.writeAndFlush調用后,在將buffer寫到socket后,這個buffer將被回收到池里。回收的時候也是通過thread local找到對應的池,回收掉。這樣就有一個問題,分配的時候是在業務線程,也就是說從業務線程的threadlocal對應的池里分配的,而回收的時候是在IO線程。這兩個是不同的線程。池的作用完全喪失了,一個線程不斷地去分配,不斷地轉移到另外一個池。
buffer默認大小是256個字節,當你將對象往這個buffer里序列化的時候,如果超過了256個字節ByteBuf就會自動擴展,而對於PooledByteBuf來說,自動擴展是會去池里取一個,然后將舊的回收掉。而這一切都是在業務線程里進行的。意味着你使用專用的線程來做分配和回收功虧一簣。
是關於Netty里的ChannelOutboundBuffer這個東西的。這個buffer是用在netty向channelwrite數據的時候,有個buffer緩沖,這樣可以提高網絡的吞吐量(每個channel有一個這樣的buffer)。初始大小是32(32個元素,不是指字節),但是如果超過32就會翻倍,一直增長。大部分時候是沒有什么問題的,但是在碰到對端非常慢(對端慢指的是對端處理TCP包的速度變慢,比如對端負載特別高的時候就有可能是這個情況)的時候就有問題了,這個時候如果還是不斷地寫數據,這個buffer就會不斷地增長,最后就有可能出問題了(我們的情況是開始吃swap,最后進程被linux killer干掉了)。為什么說這個地方是坑呢,因為大部分時候我們往一個channel寫數據會判斷channel是否active,但是往往忽略了這種慢的情況。那這個問題怎么解決呢?其實ChannelOutboundBuffer雖然無界,但是可以給它配置一個高水位線和低水位線,當buffer的大小超過高水位線的時候對應channel的isWritable就會變成false,當buffer的大小低於低水位線的時候,isWritable就會變成true。所以應用應該判斷isWritable,如果是false就不要再寫數據了。高水位線和低水位線是字節數,默認高水位是64K,低水位是32K,我們可以根據我們的應用需要支持多少連接數和系統資源進行合理規划。
2.調用write方法並沒有將數據寫到Socket緩沖區中,而是寫到了一個單向鏈表的數據結構中,flush才是真正的寫出
3.writeAndFlush等價於先將數據寫到netty的緩沖區,再將netty緩沖區中的數據寫到Socket緩沖區中,寫的過程與並發編程類似,用自旋鎖保證寫成功
4.netty中的緩沖區中的ByteBuf為DirectByteBuf
https://www.cnblogs.com/stateis0/p/9062155.html
Netty 的 write 的操作不會立即寫入,而是存儲在了 ChannelOutboundBuffer 緩沖區里,這個緩沖區內部是 Entry 節點組成的鏈表結構,通過 addMessage 方法添加進鏈表,通過 addFlush 方法表示可以開始寫入了,最后通過 SocketChannel 的 flush0 方法真正的寫入到 JDK 的 Socket 中。同時需要注意如果 TCP 緩沖區到達一個水位線了,不能寫入 TCP 緩沖區了,就需要晚點寫入,這里的方法判斷是 isFlushPending()。
其中,有一個需要注意的點就是,如果對方接收數據較慢,可能導致緩沖區存在大量的數據無法釋放,導致OOM,Netty 通過一個 isWritable 開關嘗試解決此問題,但用戶需要重寫 ChannelWritabilityChanged 方法,因為一旦超過默認的高水位閾值,Netty 就會調用 ChannelWritabilityChanged 方法,執行完畢后,繼續進行 flush。用戶可以在該方法中嘗試慢一點的操作。等到緩沖區的數據小於低水位的值時,開關就關閉了,就不會調用 ChannelWritabilityChanged 方法。因此,合理設置這兩個數值也挺重要的。
https://www.zhihu.com/question/35487154
https://blog.csdn.net/u010739551/article/details/82887411 great done
有了線索就趕緊去查Netty源碼,發現的確像調用channel.write()操作不是在當前線程上執行。Netty內部統一使用executor.inEventLoop()判斷當前線程是否是EventLoopGroup的線程,否則會包裝好Task交給內部線程池執行
業務線程池原來是把雙刃劍。雖然將任務交給業務線程池異步執行降低了Netty的I/O線程的占用時間、減輕了壓力,但同時業務線程池增加了線程上下文切換的次數。通過上述這些優化手段,終於將壓測時的CS從每秒30w+降到了8w左右,效果還是挺明顯的!
系統調用一般會涉及到從User Space到Kernel Space的模態轉換(Mode Transition或Mode Switch)。這種轉換也是有一定開銷的。如:從實踐模擬角度再議bio nio【重點】 我們寫的Java程序其本質在輪詢每個Socket的時候也需要去調用系統函數,那么輪詢一次調用一次,會造成不必要的上下文切換開銷
Netty涉及的系統調用最多的就是網絡通信操作了,所以為了降低系統調用的頻度,最直接的方法就是緩沖輸出內容,達到一定的數據大小、寫入次數或時間間隔時才flush緩沖區。
對於緩沖區大小不足,寫入速度過快等問題,Netty提供了writeBufferLowWaterMark和writeBufferHighWaterMark選項,當緩沖區達到一定大小時則不能寫入,避免被撐爆。感覺跟Netty提供的Traffic Shaping流量整形功能有點像呢。具體還未深入研究,感興趣的同學可以自行學習一下。
因為網絡粘包拆包等因素,Decoder不可避免的要保存一些解析過程的中間狀態。netty(十九)ChannelInitializer 使用公共handler(@Shareable)實踐及邏輯解答【重點】
- 線程數控制:高並發下如果線程較多時,Context Switch會非常明顯,超過CPU核心數的線程不會帶來任何好處。不是特別耗時的操作的話,業務線程池也是有害無益的。Netty 5為我們提供了指定底層線程池的機會,這樣能更好的控制整個中間件的線程數和調度策略。
- 非阻塞I/O操作:要想線程少還多做事,避免阻塞是一定要做的。
- 減少系統調用:雖然Mode Switch比Context Switch的開銷要小得多,但我們還是要盡量減少頻繁的syscall。從實踐模擬角度再議bio nio【重點】 模擬的代碼在用戶態和內核態頻繁切換
- 數據零拷貝:從內核空間的Direct Buffer拷貝到用戶空間,每次透傳都拷貝的話累積起來是個不小的開銷。15階段結論整理
- 共享狀態保護:中間件內部的並發處理也是決定性能的關鍵。
不是,是netty自己搞了一段內存;這段東西的作用是降低用戶空間-內核的切換和拷貝頻率,socket.write就涉及到一次切換