1. 程式人生 > >Netty中的坑(下篇)

Netty中的坑(下篇)

其實這篇應該叫Netty實踐,但是為了與前一篇名字保持一致,所以還是用一下坑這個名字吧。

Netty是高效能Java NIO網路框架,在很多開源系統裡都有她的身影,而在絕大多數網際網路公司所實施的服務化,以及最近流行的MicroService中,她都作為基礎中的基礎出現。

Netty的出現讓我們可以簡單容易地就可以使用NIO帶來的高效能網路程式設計的潛力。她用一種統一的流水線方式組織我們的業務程式碼,將底層網路繁雜的細節隱藏起來,讓我們只需要關注業務程式碼即可。並且用這種機制將不同的業務劃分到不同的handler裡,比如將編碼,連線管理,業務邏輯處理進行分開。Netty也力所能及的遮蔽了一些NIO bug,比如著名的epoll cpu 100% bug。而且,還提供了很多優化支援,比如使用buffer來提高網路吞吐量。

但是,和所有的框架一樣,框架為我們遮蔽了底層細節,讓我們可以很快上手。但是,並不表示我們不需要對框架所遮蔽的那一層進行了解。本文所涉及的幾個地方就是Netty與底層網路結合的幾個地方,看看我們使用的時候應該怎麼處理,以及為什麼要這麼處理。

autoread

在Netty 4裡我覺得一個很有用的功能是autoread。autoread是一個開關,如果開啟的時候Netty就會幫我們註冊讀事件(這個需要對NIO有些基本的瞭解)。當註冊了讀事件後,如果網路可讀,則Netty就會從channel讀取資料,然後我們的pipeline就會開始流動起來。那如果autoread關掉後,則Netty會不註冊讀事件,這樣即使是對端傳送資料過來了也不會觸發讀時間,從而也不會從channel讀取到資料。那麼這樣一個功能到底有什麼作用呢?

它的作用就是更精確的速率控制。那麼這句話是什麼意思呢?比如我們現在在使用Netty開發一個應用,這個應用從網路上傳送過來的資料量非常大,大到有時我們都有點處理不過來了。而我們使用Netty開發應用往往是這樣的安排方式:Netty的Worker執行緒處理網路事件,比如讀取和寫入,然後將讀取後的資料交給pipeline處理,比如經過反序列化等最後到業務層。到業務層的時候如果業務層有阻塞操作,比如資料庫IO等,可能還要將收到的資料交給另外一個執行緒池處理。因為我們絕對不能阻塞Worker執行緒,一旦阻塞就會影響網路處理效率,因為這些Worker是所有網路處理共享的,如果這裡阻塞了,可能影響很多channel的網路處理。

但是,如果把接到的資料交給另外一個執行緒池處理就又涉及另外一個問題:速率匹配。

比如現在網路實在太忙了,接收到很多資料交給執行緒池。然後就出現兩種情況:

1. 由於開發的時候沒有考慮到,這個執行緒池使用了某些無界資源。比如很多人對ThreadPoolExecutor的幾個引數不是特別熟悉,就有可能用錯,最後導致資源無節制使用,整個系統crash掉。

//比如開始的時候沒有考慮到會有這麼大量
//這種方式執行緒數是無界的,那麼有可能建立大量的執行緒對系統穩定性造成影響
Executor executor = Executors.newCachedTheadPool();
executor.execute(requestWorker);

//或者使用這個
//這種queue是無界的,有可能會消耗太多記憶體,對系統穩定性造成影響
Executor executor = Executors.newFixedThreadPool(8);
executor.execute(requestWorker);

2. 第二種情況就是限制了資源使用,所以只好把最老的或最新的資料丟棄。

//執行緒池滿後,將最老的資料丟棄
Executor executor = new ThreadPoolExecutor(8, 8, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(1000), namedFactory, new ThreadPoolExecutor.DiscardOldestPolicy());

其實上面兩種情況,不管哪一種都不是太合理。不過在Netty 4裡我們就有了更好的解決辦法了。如果我們的執行緒池暫時處理不過來,那麼我們可以將autoread關閉,這樣Netty就不再從channel上讀取資料了。那麼這樣造成的影響是什麼呢?這樣socket在核心那一層的read buffer就會滿了。因為TCP預設就是帶flow control的,read buffer變小之後,向對端傳送ACK的時候,就會降低視窗大小,直至變成0,這樣對端就會自動的降低傳送資料的速率了。等到我們又可以處理資料了,我們就可以將autoread又開啟這樣資料又源源不斷的到來了。

這樣整個系統就通過TCP的這個負反饋機制,和諧的執行著。那麼autoread涉及的網路知識就是,傳送端會根據對端ACK時候所攜帶的advertises window來調整自己傳送的資料量。而ACK裡的這個window的大小又跟接收端的read buffer有關係。而不註冊讀事件後,read buffer裡的資料沒有被消費掉,就會達到控制傳送端速度的目的。

不過設計關閉和開啟autoread的策略也要注意,不要設計成我們不能處理任何資料了就立即關閉autoread,而我們開始能處理了就立即開啟autoread。這個地方應該留一個緩衝地帶。也就是如果現在排隊的資料達到我們預設定的一個高水位線的時候我們關閉autoread,而低於一個低水位線的時候才打開autoread。不這麼弄的話,有可能就會導致我們的autoread頻繁開啟和關閉。autoread的每次調整都會涉及系統呼叫,對效能是有影響的。類似下面這樣一個程式碼,在將任務提交到執行緒池之前,判斷一下現在的排隊量(注:本文的所有數字純為演示作用,所有執行緒池,佇列等大小資料要根據實際業務場景仔細設計和考量)。

int highReadWaterMarker = 900;
int lowReadWaterMarker = 600;

ThreadPoolExecutor executor = new ThreadPoolExecutor(8, 8, 1, TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(1000), namedFactory, new ThreadPoolExecutor.DiscardOldestPolicy());

int queued = executor.getQueue().size();
if(queued > highReadWaterMarker){
    channel.config().setAutoRead(false);
}
if(queued < lowReadWaterMarker){
    channel.config().setAutoRead(true);
}

但是使用autoread也要注意一件事情。autoread如果關閉後,對端傳送FIN的時候,接收端應用層也是感知不到的。這樣帶來一個後果就是對端傳送了FIN,然後核心將這個socket的狀態變成CLOSE_WAIT。但是因為應用層感知不到,所以應用層一直沒有呼叫close。這樣的socket就會長期處於CLOSE_WAIT狀態。特別是一些使用連線池的應用,如果將連線歸還給連線池後,一定要記著autoread一定是開啟的。不然就會有大量的連線處於CLOSE_WAIT狀態。

其實所有非同步的場合都存在速率匹配的問題,而同步往往不存在這樣的問題,因為同步本身就是帶負反饋的。

isWritable

isWritable其實在上一篇文章已經介紹了一點,不過這裡我想結合網路層再囉嗦一下。上面我們講的autoread一般是接收端的事情,而傳送端也有速率控制的問題。Netty為了提高網路的吞吐量,在業務層與socket之間又增加了一個ChannelOutboundBuffer。在我們呼叫channel.write的時候,所有寫出的資料其實並沒有寫到socket,而是先寫到ChannelOutboundBuffer。當呼叫channel.flush的時候才真正的向socket寫出。因為這中間有一個buffer,就存在速率匹配了,而且這個buffer還是無界的。也就是你如果沒有控制channel.write的速度,會有大量的資料在這個buffer裡堆積,而且如果碰到socket又『寫不出』資料的時候,很有可能的結果就是資源耗盡。而且這裡讓這個事情更嚴重的是ChannelOutboundBuffer很多時候我們放到裡面的是DirectByteBuffer,什麼意思呢,意思是這些記憶體是放在GC Heap之外。如果我們僅僅是監控GC的話還監控不出來這個隱患。

那麼說到這裡,socket什麼時候會寫不出資料呢?在上一節我們瞭解到接收端有一個read buffer,其實發送端也有一個send buffer。我們呼叫socket的write的時候其實是向這個send buffer寫資料,如果寫進去了就表示成功了(所以這裡千萬不能將socket.write呼叫成功理解成資料已經到達接收端了),如果send buffer滿了,對於同步socket來講,write就會阻塞直到超時或者send buffer又有空間(這麼一看,其實我們可以將同步的socket.write理解為半同步嘛)。對於非同步來講這裡是立即返回的。 

那麼進入send buffer的資料什麼時候會減少呢?是傳送到網路的資料就會從send buffer裡去掉麼?也不是這個樣子的。還記得TCP有重傳機制麼,如果傳送到網路的資料都從send buffer刪除了,那麼這個資料沒有得到確認TCP怎麼重傳呢?所以send buffer的資料是等到接收端回覆ACK確認後才刪除。那麼,如果接收端非常慢,比如CPU佔用已經到100%了,而load也非常高的時候,很有可能來不及處理網路事件,這個時候send buffer就有可能會堆滿。這就導致socket寫不出資料了。而傳送端的應用層在傳送資料的時候往往判斷socket是不是有效的(是否已經斷開),而忽略了是否可寫,這個時候有可能就還一個勁的寫資料,最後導致ChannelOutboundBuffer膨脹,造成系統不穩定。

所以,Netty已經為我們考慮了這點。channel有一個isWritable屬性,可以來控制ChannelOutboundBuffer,不讓其無限制膨脹。至於isWritable的實現機制可以參考前一篇。

序列化

所有講TCP的書都會有這麼一個介紹:TCP provides a connection-oriented, reliable, byte stream service。前面兩個這裡就不關心了,那麼這個byte stream到底是什麼意思呢?我們在傳送端傳送資料的時候,對於應用層來說我們傳送的是一個個物件,然後序列化成一個個位元組陣列,但無論怎樣,我們傳送的是一個個『包』。每個都是獨立的。那麼接收端是不是也像傳送端一樣,接收到一個個獨立的『包』呢?很遺憾,不是的。這就是byte stream的意思。接收端沒有『包』的概念了。

這對於應用層編碼的人員來說可能有點困惑。比如我使用Netty開發,我的handler的channelRead這次明明傳遞給我的是一個ByteBuf啊,是一個『獨立』的包啊,如果是byte stream的話難道不應該傳遞我一個Stream麼。但是這個ByteBuf和傳送端的ByteBuf一點關係都沒有。比如:

public class Decorder extends ChannelInboundHandlerAdapter{
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //這裡的msg和傳送端channel.write(msg)時候的msg沒有任何關係
    } 
}    

這個ByteBuf可能包含傳送端多個ByteBuf,也可能只包含傳送端半個ByteBuf。但是別擔心,TCP的可靠性會確保接收端的順序和傳送端的順序是一致的。這樣的byte stream協議對我們的反序列化工作就帶來了一些挑戰。在反序列化的時候我們要時刻記著這一點。對於半個ByteBuf我們按照設計的協議如果解不出一個完整物件,我們要留著,和下次收到的ByteBuf拼湊在一起再次解析,而收到的多個ByteBuf我們要根據協議解析出多個完整物件,而很有可能最後一個也是不完整的。不過幸運的是,我們有了Netty。Netty為我們已經提供了很多種協議解析的方式,並且對於這種半包粘包也已經有考慮,我們可以參考ByteToMessageDecoder以及它的一連串子類來實現自己的反序列化機制。而在反序列化的時候我們可能經常要取ByteBuf中的一個片段,這個時候建議使用ByteBuf的readSlice方法而不是使用copy。

另外,Netty還提供了兩個ByteBuf的流封裝:ByteBufInputStream, ByteBufOutputStream。比如我們在使用一些序列化工具,比如Hessian之類的時候,我們往往需要傳遞一個InputStream(反序列化),OutputStream(序列化)到這些工具。而很多協議的實現都涉及大量的記憶體copy。比如對於反序列化,先將ByteBuf裡的資料讀取到byte[],然後包裝成ByteArrayInputStream,而序列化的時候是先將物件序列化成ByteArrayOutputStream再copy到ByteBuf。而使用ByteBufInputStream和ByteBufOutputStream就不再有這樣的記憶體拷貝了,大大節約了記憶體開銷。

另外,因為socket.write和socket.read都需要一個direct byte buffer(即使你傳入的是一個heap byte buffer,socket內部也會將內容copy到direct byte buffer)。如果我們直接使用ByteBufInputStream和ByteBufOutputStream封裝的direct byte buffer再加上Netty 4的記憶體池,那麼記憶體將更有效的使用。這裡提一個問題:為什麼socket.read和socket.write都需要direct byte buffer呢?heap byte buffer不行麼?

總結起來,對於序列化和反序列化來講就是兩條:1 減少記憶體拷貝 2 處理好TCP的粘包和半包問題

後記

作為一個應用層程式設計師,往往是幸福的。因為我們有豐富的框架和工具為我們遮蔽下層的細節,這樣我們可以更容易的解決很多業務問題。但是目前程式設計並沒有發展到不需要了解所有下層的知識就可以寫出更有效率的程式,所以我們在使用一個框架的時候最好要對它所遮蔽和所依賴的知識進行一些瞭解,這樣在碰到一些問題的時候我們可以根據這些理論知識去分析原因。這就是理論和實踐的相結合。

相關推薦

Netty(下篇)

其實這篇應該叫Netty實踐,但是為了與前一篇名字保持一致,所以還是用一下坑這個名字吧。 Netty是高效能Java NIO網路框架,在很多開源系統裡都有她的身影,而在絕大多數網際網路公司所實施的服務化,以及最近流行的MicroService中,她都作為基礎中的基礎出現。 Netty的出現讓我們可以簡單容

netty學習之Reactor線程模型以及在netty的應用

rec 直接 滿足 red 轉載 chan tail io處理 理論 轉載:http://blog.csdn.net/u010853261/article/details/55805216 說道netty的線程模型,我們第一反應就是經典的Reactor線程模型,下面我們就

論python3下“多態”與“繼承”

ict for all order section 有意思 back ani eve 1、背景: 近日切換到python3後,發現python3在多態處理上,有一些比較有意思的情況,特別記載,供大家參考。。。 以廖老師的python3教程中的animal 和dog的繼承

NettyLineBasedFrameDecoder解碼器使用與分析:解決TCP粘包問題

ring public xpl cep ctx new 綁定端口 註意 相關 [toc] Netty中LineBasedFrameDecoder解碼器使用與分析:解決TCP粘包問題 上一篇文章《Netty中TCP粘包問題代碼示例與分析》演示了使用了時間服務器的例子演示了T

NettyTCP粘包問題代碼示例與分析

sep 會有 value 指南 esp syn logger soc pipe [toc] Netty中TCP粘包問題代碼示例 Netty中會發生TCP粘包和拆包的問題,當然,其實對於曾經的網絡工程師來說,第一次看到這名詞可能會有點不適應,因為在那會我們是說TCP的累計發

Netty分隔符解碼器代碼示例與分析

rac ride 通用 否則 eventloop connect href throw java [toc] Netty中分隔符解碼器代碼示例與分析 通過特別解碼器的使用,可以解決Netty中TCP的粘包問題,上一篇《Netty中LineBasedFrameDecoder

Netty定長解碼器的使用

ble 解碼器 erb cau option bootstra cef 成功 ios [toc] Netty中定長解碼器的使用 有了前面的基礎,定長解碼器的使用相對就比較簡單了,所以這裏只使用服務端的代碼,測試時,用telnet作為客戶客戶端,數據只作單向的發送,即從客戶

MessagePack在Netty的應用

泛型類 time 緩沖 cli 程序 cep his t對象 學習 [toc] MessagePack在Netty中的應用 前面使用Netty通信時,傳輸的都是字符串對象,因為在進行遠程過程調用時,更多的是傳輸pojo對象,這時就需要對pojo對象進行序列化與反序列化(編

Netty使用MessagePack時的TCP粘包問題與解決方案

private pri delay read complete bcd 資源 tina object [toc] Netty中使用MessagePack時的TCP粘包問題與解決方案 通過下面的實例代碼來演示在Netty中使用MessagPack時會出現的TCP粘包問題,為

Google Protobuf在Netty的使用

連接 delay exce gin bootstrap 語言 sync socket cau [toc] Google Protobuf在Netty中的使用 程序代碼來自於《Netty權威指南》第8章,已經加了註釋,不過需要註意的是,使用的proto源代碼是在Google

DLL線程爹的Synchronize?

工程文件 一個 creat init eat 因此 測試 錄音 捕捉 1, 緣起 某次開發語音對講windows程序,采用delphi語言,及delphix的TDXSound控件。 DXSound提供了TSoundCaptureStream類,可以實現指定頻率、位數、聲道的

7.Netty handler 的執行順序

什麽 pre art img 代碼 client bind throws cau 1.Netty中handler的執行順序    Handler在Netty中,無疑占據著非常重要的地位。Handler與Servlet中的filter很像,通過Handler可以完成通訊報文的

netty常用概念的理解

目錄   目錄 ChannelHandler ChannelHandler功能介紹 通過ChannelHandlerAdapter自定義攔截器 ChannelHandlerContext介面 ChannelPipel

Netty有哪些自帶的ChannelHandler?

https://blog.csdn.net/weixin_39687783/article/details/80792930 Netty中有哪些自帶的ChannelHandler? SslHandler:負責對請求進行加密和解密,是放在ChannelPipeline中的第一個Chann

理解Netty的零拷貝(Zero-Copy)機制【轉】

理解零拷貝 零拷貝是Netty的重要特性之一,而究竟什麼是零拷貝呢?  WIKI中對其有如下定義: “Zero-copy” describes computer operations in which the CPU does not perform the task of

Netty ChannelFuture 介面的作用

正如我們所知道的Netty中所有的I/O操作都是非同步的,由於一個操作的結果可能不會立即返回,所有我們需要一種可以在之後的某個時間點確定其結果的方法,為此,Netty提供了ChannelFuture介面,就如JavaJdk中的Future 介面可以返回執行緒的執行結果一樣,我

你真的瞭解Netty@Sharable? | 併發程式設計網

一、前言 Netty 是一個可以快速開發網路應用程式的基於事件驅動的非同步 網路通訊 框架,它大大簡化了 TCP 或者 UDP 伺服器的網路程式設計。Netty 的應用還是比較廣泛的,比如阿里巴巴開源的 Dubbo 和 Sofa-Bolt等 框架底層網路通訊都是基於 Netty 來實現的。Ne

Netty 的ChannelHandler介面的一些作用說明

作為開發人員的角度來看,ChannelHandler是Netty的主要元件,它充當了所有處理入站和出站資料的應用程式邏輯的容器。 ChannelHandler的方法是由網路事件(其中術語“事件”的 使用非常廣泛)觸發的。事實上,ChannelHandler可專門用於幾乎任何

NettyChannelPipeline和ChannelHandler的關係

ChannelPipeline為ChannelHandler鏈提供了容器,並定義了用於在該鏈上傳播入站和出站事件流的API。當Channel被建立時,它會被自動地分配到它專屬的ChannelPipeline。 ChannelHandler安裝到ChannelPipeline

netty第一步:了解netty和編寫簡單的Echo服務器和客戶端

vnr ech pac str 能夠 長連接 pos 通過 pre 早期java API通過原生socket產生所謂的"blocking",大致過程是這樣 這種的特點是每次只能處理一個請求,如果要實現多個請求並行,就還要分配一個新的線程來給每個客戶端的socket