我是一個(gè)Dubbo數(shù)據(jù)包,我的旅行開始了!
hello,大家好呀,我是小樓!
今天給大家?guī)?lái)一篇關(guān)于Dubbo IO交互的文章,本文是一位同事所寫,用有趣的文字把枯燥的知識(shí)點(diǎn)寫出來(lái),通俗易懂,非常有意思,所以迫不及待找作者授權(quán)然后分享給大家:
一些有趣的問題
Dubbo是一個(gè)優(yōu)秀的RPC框架,其中有錯(cuò)綜復(fù)雜的線程模型,本篇文章筆者從自己淺薄的認(rèn)知中,來(lái)剖析Dubbo的整個(gè)IO過(guò)程。在開始之前,我們先來(lái)看如下幾個(gè)問題:
- 業(yè)務(wù)方法執(zhí)行之后,數(shù)據(jù)包就發(fā)出去了嗎?
- netty3和netty4在線程模型上有什么區(qū)別?
- 數(shù)據(jù)包到了操作系統(tǒng)socket buffer,經(jīng)歷了什么?
- Provider打出的log耗時(shí)很小,而Consumer端卻超時(shí)了,怎么可以排查到問題?
- 數(shù)據(jù)包在物理層是一根管道就直接發(fā)過(guò)去嗎?
- Consumer 業(yè)務(wù)線程await在Condition上,在哪個(gè)時(shí)機(jī)被喚醒?
- ……
接下來(lái)筆者將用Dubbo2.5.3 作為Consumer,2.7.3作為Provider來(lái)講述整個(gè)交互過(guò)程,筆者站在數(shù)據(jù)包視角,用第一人稱來(lái)講述,系好安全帶,我們出發(fā)咯。
有意思的旅行
1.Dubbo2.5.3 Consumer端發(fā)起請(qǐng)求
我是一個(gè)數(shù)據(jù)包,出生在一個(gè)叫Dubbo2.5.3 Consumer的小鎮(zhèn),我的使命是是傳遞信息,同時(shí)也喜歡出門旅行。
某一天,我即將被發(fā)送出去,據(jù)說(shuō)是要去一個(gè)叫Dubbo 2.7.3 Provider的地方。
這一天,業(yè)務(wù)線程發(fā)起發(fā)起方法調(diào)用,在FailoverClusterInvoker#doInvoke我選擇了一個(gè)Provider,然后經(jīng)過(guò)各種Consumer Filter,再經(jīng)過(guò)Netty3的pipeline,最后通過(guò)NioWorker#scheduleWriteIfNecessary方法,我來(lái)到了NioWorker的writeTaskQueue隊(duì)列中。
當(dāng)我回頭看主線程時(shí),發(fā)現(xiàn)他在DefaultFuture中的Condition等待,我不知道他在等什么,也不知道他要等多久。
我在writeTaskQueue隊(duì)列排了一會(huì)隊(duì),看到netty3 IO worker線程在永不停歇的執(zhí)行run方法,大家都稱這個(gè)為死循環(huán)。
最后,我很幸運(yùn),NioWorker#processWriteTaskQueue選擇了我,我被寫到操作系統(tǒng)的Socket緩沖區(qū),我在緩沖區(qū)等待,反正時(shí)間充足,我回味一下今天的旅行,期間我輾轉(zhuǎn)了兩個(gè)旅行團(tuán),分別叫主線程和netty3 IO worker線程,嗯,兩個(gè)旅行團(tuán)服務(wù)都不錯(cuò),效率很高。
索性我把今天的見聞?dòng)涗浵聛?lái),繪制成一張圖,當(dāng)然不重要的地方我就忽略了。
2.操作系統(tǒng)發(fā)送數(shù)據(jù)包
我在操作系統(tǒng)socket緩沖區(qū),經(jīng)過(guò)了很多神奇的事情。
- 在一個(gè)叫傳輸層的地方給我追加上了目標(biāo)端口號(hào)、源端口號(hào)
- 在一個(gè)叫網(wǎng)絡(luò)層的地方給我追加上了目標(biāo)IP、源IP,同時(shí)通過(guò)目標(biāo)IP與掩碼做與運(yùn)算,找到“下一跳”的IP
- 在一個(gè)叫數(shù)據(jù)鏈路層的地方通過(guò)ARP協(xié)議給我追加上了“下一跳”的目標(biāo)MAC地址、源MAC地址
最有意思的是,我們坐的都是一段一段纜車,每換一個(gè)纜車,就要修改目標(biāo)MAC地址、源MAC地址,后來(lái)問了同行的數(shù)據(jù)包小伙伴,這個(gè)模式叫“下一跳”,一跳一跳的跳過(guò)去。這里有很多數(shù)據(jù)包,體型大的單獨(dú)一個(gè)纜車,體型小的幾個(gè)擠一個(gè)纜車,還有一個(gè)可怕的事情,體型再大一點(diǎn),要分拆做多個(gè)纜車(雖然這對(duì)我們數(shù)據(jù)包沒啥問題),這個(gè)叫拆包和粘包。期間我們經(jīng)過(guò)交換機(jī)、路由器,這些地方玩起來(lái)很Happy。
當(dāng)然也有不愉快的事情,就是擁堵,目的地纜車滿了,來(lái)不及被拉走,只能等待咯。
3.在Provider端的經(jīng)歷
好不容易,我來(lái)到了目的地,我坐上了一個(gè)叫“零拷貝”號(hào)的快艇,迅速到了netty4,netty4果然富麗堂皇,經(jīng)過(guò)NioEventLoop#processSelectedKeys,再經(jīng)過(guò)pipeline中的各種入站handler,我來(lái)到了AllChannelHandler的線程池,當(dāng)然我有很多選擇,但是我隨便選了一個(gè)目的地,這里會(huì)經(jīng)歷解碼、一系列的Filter,才會(huì)來(lái)的目的地“業(yè)務(wù)方法”,NettyCodecAdapter#InternalDecoder解碼器很厲害,他可以處理拆包和粘包。
在AllChannelHandler的線程池中我會(huì)停留一會(huì),于是我也畫了一張圖,記錄旅程。
自此,我的旅行結(jié)束,新的故事將由新的數(shù)據(jù)包續(xù)寫。
4.Provider端產(chǎn)生了新的數(shù)據(jù)包
我是一個(gè)數(shù)據(jù)包,出生在一個(gè)叫Dubbo2.7.3 Provider的小鎮(zhèn),我的使命是去喚醒命中注定的線程,接下來(lái)我會(huì)開始一段旅行,去一個(gè)叫Dubbo2.5.3 Consumer的地方。
在Provider業(yè)務(wù)方法執(zhí)行之后
- 由業(yè)務(wù)線程經(jīng)過(guò)io.netty.channel.AbstractChannelHandlerContext#writeAndFlush
- 再經(jīng)過(guò)io.netty.util.concurrent.SingleThreadEventExecutor#execute 執(zhí)行addTask
- 將任務(wù)放入隊(duì)列io.netty.util.concurrent.SingleThreadEventExecutor#taskQueue
- 我便跟隨著io.netty.channel.AbstractChannelHandlerContext$WriteTask等待NioEventLoop發(fā)車,等待的過(guò)程中,我記錄了走過(guò)的腳步。
在這里,我看到NioEventLoop是一個(gè)死循環(huán),不停地從任務(wù)隊(duì)列取任務(wù),執(zhí)行任務(wù)AbstractChannelHandlerContext.WriteAndFlushTask,然后指引我們到socket緩沖區(qū)等候,永不知疲倦,我似乎領(lǐng)略到他身上有一種倔強(qiáng)的、追求極致的匠人精神。
經(jīng)過(guò)io.netty.channel.AbstractChannel.AbstractUnsafe#write,我到達(dá)了操作系統(tǒng)socket緩沖區(qū)。在操作系統(tǒng)層面和大多數(shù)數(shù)據(jù)包一樣,也是做纜車達(dá)到目的地。
5.到達(dá)dubbo 2.5.3 Consumer端
到達(dá)dubbo 2.5.3 Consumer端,我在操作系統(tǒng)socket緩沖區(qū)等了一會(huì),同樣是坐了“零拷貝”號(hào)快艇,到達(dá)了真正的目的地dubbo 2.5.3 Consumer,在這里我發(fā)現(xiàn),NioWorker#run是一個(gè)死循環(huán),然后執(zhí)行NioWorker#processSelectedKeys,通過(guò)NioWorker#read方式讀出來(lái),我就到達(dá)了AllChannelHandler的線程池,這是一個(gè)業(yè)務(wù)線程池。
我在這里等待一會(huì),等任務(wù)被調(diào)度,我看見com.alibaba.dubbo.remoting.exchange.support.DefaultFuture#doReceived被執(zhí)行了,同時(shí)Condition的signal被執(zhí)行了。我在遠(yuǎn)處看到了一個(gè)被阻塞線程被喚醒,我似乎明白,因?yàn)槲业牡絹?lái),喚醒了一個(gè)沉睡的線程,我想這應(yīng)該是我生命的意義。
至此,我的使命也完成了,本次旅程結(jié)束。
總結(jié)netty3和netty4的線程模型
我們根據(jù)兩個(gè)數(shù)據(jù)包的自述,來(lái)總結(jié)一下netty3和netty4的線程模型。
1.netty3寫過(guò)程
2.Netty4的讀寫過(guò)程
說(shuō)明:這里沒有netty3的讀過(guò)程,netty3讀過(guò)程和netty4相同,pipeline是由IO線程執(zhí)行。
總結(jié):netty3與netty4線程模型的區(qū)別在于寫過(guò)程,netty3中pipeline由業(yè)務(wù)線程執(zhí)行,而netty4無(wú)論讀寫,pipeline統(tǒng)一由IO線程執(zhí)行。
netty4中ChannelPipeline中的Handler鏈統(tǒng)一由I/O線程串行調(diào)度,無(wú)論是讀還是寫操作,netty3中的write操作時(shí)由業(yè)務(wù)線程處理Handler鏈。netty4中可以降低線程之間的上下文切換帶來(lái)的時(shí)間消耗,但是netty3中業(yè)務(wù)線程可以并發(fā)執(zhí)行Handler鏈。如果有一些耗時(shí)的Handler操作會(huì)導(dǎo)致netty4的效率低下,但是可以考慮將這些耗時(shí)操作放在業(yè)務(wù)線程最先執(zhí)行,不放在Handler里處理。由于業(yè)務(wù)線程可以并發(fā)執(zhí)行,同樣也可以提高效率。
一些疑難問題排查
有遇到一些比較典型的疑難問題,例如當(dāng)Provider答應(yīng)的didi.log耗時(shí)正常,而Consumer端超時(shí)了,此時(shí)有如下排查方向,didi.log的Filter其實(shí)處于非常里層,往往不能反映真實(shí)的業(yè)務(wù)方法執(zhí)行情況。
- Provider除了業(yè)務(wù)方向執(zhí)行外,序列化也有可能是耗時(shí)的,所以可以用arthas監(jiān)控最外側(cè)方法org.apache.dubbo.remoting.transport.DecodeHandler#received,排除業(yè)務(wù)方法耗時(shí)高的問題
- Provider中數(shù)據(jù)包寫入是否耗時(shí),監(jiān)控io.netty.channel.AbstractChannelHandlerContext#invokeWrite方法
- 通過(guò)netstat 也能查看當(dāng)前tcp socket的一些信息,比如Recv-Q, Send-Q,Recv-Q是已經(jīng)到了接受緩沖區(qū),但是還沒被應(yīng)用代碼讀走的數(shù)據(jù)。Send-Q是已經(jīng)到了發(fā)送緩沖區(qū),但是對(duì)方還沒有回復(fù)Ack的數(shù)據(jù)。這兩種數(shù)據(jù)正常一般不會(huì)堆積,如果堆積了,可能就有問題了。
- 看Consumer NioWorker#processSelectedKeys (dubbo2.5.3)方法是否耗時(shí)高。
- 直到最終整個(gè)鏈路的所有細(xì)節(jié)……問題肯定是可以解決的。
尾聲
在整個(gè)交互過(guò)程中,筆者省略線程棧調(diào)用的一些細(xì)節(jié)和源代碼的細(xì)節(jié),例如序列化與反序列化,dubbo怎么讀出完整的數(shù)據(jù)包的,業(yè)務(wù)方法執(zhí)行前那些Filter是怎么排序和分布的,netty的Reactor模式是如何實(shí)現(xiàn)的。這些都是非常有趣的問題……