在幾個月前改造dubbo時,netty4已經穩定很久了,一時手癢,按照netty3-rpc的源碼克隆了一套netty4,在修正了大量的包、類型不同之后,基本保持了netty3的風格,并發量小或者數據包很小時,一切都很ok, 在進行大并發測試時,結果和netty3完全不同,基本用慘不忍睹來形容。由于當時急于開發php客戶端,就把netty4-rpc當做一個失敗的組件存檔起來, 前幾天php-dubbo開發基本完成之后,返回過來思考netty4-rpc的問題,經過仔細分析數據包的解析過程,單步跟蹤源碼
NettyCodecAdapter, TelnetCodec, ExchangeCoedec,發現ByteBuf的緩沖區為1024,當數據超過1024時,會調用多次Decoder.messageReceived函數,第一次分析dubbo的協議頭時,是正確的,第二次之后數據就錯誤了,然后dubbo內部緩沖區的數據越來越長,但是仍然分析不到一個完整的dubbo數據包
因此去看netty4的源碼,發現AbstractNioByteChannel中有網絡數據接收的代碼時這么處理ByteBuf的
ByteBuf byteBuf = null;
int messages = 0;
boolean close = false;
try {
int totalReadAmount = 0;
boolean readPendingReset = false;
do {
byteBuf = allocHandle.allocate(allocator);
int writable = byteBuf.writableBytes();
int localReadAmount = doReadBytes(byteBuf);
if (localReadAmount <= 0) {
// not was read release the buffer
byteBuf.release();
close = localReadAmount < 0;
break;
}
if (!readPendingReset) {
readPendingReset = true;
setReadPending(false);
}
pipeline.fireChannelRead(byteBuf);
byteBuf = null;
看見沒,內核是需要ByteBuf.release的,繼續通過byteBuf的一個實現PooledByteBuf分析源碼,原來是實現了一個基于簡單計數應用計數的循環使用的緩沖區,一旦計數變為1,該緩沖區被歸還到netty4內核,被后面的數據讀取線程重新使用
而我們InternalDecoder的代碼為
message = com.alibaba.dubbo.remoting.buffer.ChannelBuffers.wrappedBuffer(
input.toByteBuffer());
直接引用了ByteBuf.toByteBuffer,繼續查看源碼UnpooledHeapByteBuf, 其toByteBuffer實際是對內部數據的
一個nio封裝而已,因此,使用上述函數時,導致dubbo的decode保存了一個某一個ByteBuffer的內部數據,但是雖有該
buffer被歸還到netty4緩沖區中被循環引用,下一次可能被其他讀寫線程重新改寫數據,因此,高并發下當緩沖區被重復使用時,bytebuf將由于計數問題不斷被使用,而解碼器中缺傻傻等待。
解決方案
1.通過byteBuf的retain和release函數保證計數的有效性,通過程序例外或者緩沖區被使用完成時候歸還ByteBuf到netty4內核
2.拷貝數據到dubbo的緩沖區中
思考:
netty3 是否也有該問題呢???
只有nioBuffer方法,而該方法是將array數組轉換為ByteBuffer對象。
照理說應該沒有問題的,因為array數組在使用中并不可變的。
如果netty在使用是PooledUnsafeDirectByteBuf,那么就是有問題的,使用nioBuffer的時候,實際上還持有對原有ByteBuffer的引用。