和女朋友聊天:什么是Dubbo服務(wù)調(diào)用
本文轉(zhuǎn)載自微信公眾號(hào)「大魚(yú)仙人」,作者大魚(yú)仙人。轉(zhuǎn)載本文請(qǐng)聯(lián)系大魚(yú)仙人公眾號(hào)。
前言
之前有了服務(wù)的暴露和服務(wù)的引用了,服務(wù)提供者暴露出來(lái)服務(wù)了,服務(wù)消費(fèi)者將其引用進(jìn)來(lái)了,就差最后一步了,消費(fèi)者和提供者之間的調(diào)用了,調(diào)用也就是真正的通信RPC過(guò)程,既然涉及到通信,就涉及到相應(yīng)的客戶(hù)端和服務(wù)端之間的交互協(xié)議,約定,以及序列化和反序列化機(jī)制
先說(shuō)兩邊的約定其實(shí)就是客戶(hù)端這邊需要帶著參數(shù)、參數(shù)類(lèi)型,以及告訴服務(wù)端要調(diào)用的是哪個(gè)接口,這樣服務(wù)端就知道要調(diào)用的接口了,服務(wù)端就可以執(zhí)行了
關(guān)于應(yīng)用層的協(xié)議的交互一般有三種形式,分別是:固定長(zhǎng)度形式、特殊字符間隔形式和header+body形式,Dubbo支持dubbo、rmi、hessian、http、webservice、thrift、redis等多種協(xié)議,但是Dubbo官網(wǎng)是推薦我們使用Dubbo協(xié)議的
Dubbo協(xié)議
這種看看就行了,咱們主要了解下相應(yīng)的特點(diǎn),Dubbo協(xié)議采用單一長(zhǎng)連接和NIO異步通訊,適合于小數(shù)據(jù)量大并發(fā)的服務(wù)調(diào)用,以及服務(wù)消費(fèi)者機(jī)器數(shù)遠(yuǎn)大于服務(wù)提供者機(jī)器數(shù)的情況;不適合傳送大數(shù)據(jù)量的服務(wù),比如傳文件,傳視頻等,除非請(qǐng)求量很低。
適用范圍:傳入傳出參數(shù)數(shù)據(jù)包較小(建議小于100K),消費(fèi)者比提供者個(gè)數(shù)多,單一消費(fèi)者無(wú)法壓滿(mǎn)提供者,盡量不要用dubbo協(xié)議傳輸大文件或超大字符串。
為什么要消費(fèi)者比提供者個(gè)數(shù)多?
因dubbo協(xié)議采用單一長(zhǎng)連接,假設(shè)網(wǎng)絡(luò)為千兆網(wǎng)卡(1024Mbit=128MByte),根據(jù)測(cè)試經(jīng)驗(yàn)數(shù)據(jù)每條連接最多只能壓滿(mǎn)7MByte(不同的環(huán)境可能不一樣,供參考),理論上1個(gè)服務(wù)提供者需要20個(gè)服務(wù)消費(fèi)者才能壓滿(mǎn)網(wǎng)卡
為什么采用異步單一長(zhǎng)連接?
因?yàn)榉?wù)的現(xiàn)狀大都是服務(wù)提供者少,通常只有幾臺(tái)機(jī)器,而服務(wù)的消費(fèi)者多,可能整個(gè)網(wǎng)站都在訪問(wèn)該服務(wù),如果采用常規(guī)的服務(wù),服務(wù)提供者很容易就被壓跨,通過(guò)單一連接,保證單一消費(fèi)者不會(huì)壓死提供者,長(zhǎng)連接,減少連接握手驗(yàn)證等,并使用異步IO,復(fù)用線(xiàn)程池,防止出現(xiàn)網(wǎng)絡(luò)崩潰。
接口增加方法,對(duì)客戶(hù)端無(wú)影響,如果該方法不是客戶(hù)端需要的,客戶(hù)端不需要重新部署;輸入?yún)?shù)和結(jié)果集中增加屬性,對(duì)客戶(hù)端無(wú)影響,如果客戶(hù)端并不需要新屬性,不用重新部署
另外一個(gè)需要特別關(guān)注的點(diǎn)就是序列化,涉及到交互就會(huì)涉及到對(duì)象的傳遞,就會(huì)涉及到序列化,序列化就是內(nèi)存中的數(shù)據(jù)對(duì)象轉(zhuǎn)換成二進(jìn)制流才可以進(jìn)行數(shù)據(jù)持久化和網(wǎng)絡(luò)傳輸,將數(shù)據(jù)對(duì)象轉(zhuǎn)換為二進(jìn)制流的過(guò)程稱(chēng)為對(duì)象的序列化(Serialization)。
反之,將二進(jìn)制流恢復(fù)為數(shù)據(jù)對(duì)象的過(guò)程稱(chēng)為反序列化(Deserialization)。序列化需要保留充分的信息以恢復(fù)數(shù)據(jù)對(duì)象,但是為了節(jié)約存儲(chǔ)空間和網(wǎng)絡(luò)帶寬,序列化后的二進(jìn)制流又要盡可能小
常見(jiàn)的序列化方式有三種:Java原生序列化、Hessian序列化、Json序列化
Java原生序列化:Java類(lèi)通過(guò)實(shí)現(xiàn)Serializable接口來(lái)實(shí)現(xiàn)該類(lèi)對(duì)象的序列化,實(shí)現(xiàn)Serializable接口的類(lèi)建議設(shè)置serialVersionUID字段值,如果不設(shè)置,那么每次運(yùn)行時(shí),編譯器會(huì)根據(jù)類(lèi)的內(nèi)部實(shí)現(xiàn),包括類(lèi)名、接口名、方法和屬性等來(lái)自動(dòng)生成serialVersionUID。如果類(lèi)的源代碼有修改,那么重新編譯后serial VersionUID的取值可能會(huì)發(fā)生變化。
因此實(shí)現(xiàn)Serializable接口的類(lèi)一定要顯式地定義serialVersionUID屬性值。修改類(lèi)時(shí)需要根據(jù)兼容性決定是否修改serialVersionUID值:
如果是兼容升級(jí),請(qǐng)不要修改serialVersionUID字段,避免反序列化失敗。
如果是不兼容升級(jí),需要修改serialVersionUID值,避免反序列化混亂。
Hessian序列化:它的實(shí)現(xiàn)機(jī)制是著重于數(shù)據(jù),附帶簡(jiǎn)單的類(lèi)型信息的方法。就像Integer a = 1,hessian會(huì)序列化成I 1這樣的流,I表示int or Integer,1就是數(shù)據(jù)內(nèi)容。而對(duì)于復(fù)雜對(duì)象,通過(guò)Java的反射機(jī)制,hessian把對(duì)象所有的屬性當(dāng)成一個(gè)Map來(lái)序列化,而在序列化過(guò)程中,如果一個(gè)對(duì)象之前出現(xiàn)過(guò),hessian會(huì)直接插入一個(gè)R index這樣的塊來(lái)表示一個(gè)引用位置,從而省去再次序列化和反序列化的時(shí)間
在父類(lèi)、子類(lèi)存在同名成員變量的情況下, Hessian 序列化時(shí),先序列化子類(lèi) ,然后序列化父類(lèi),因此反序列化結(jié)果會(huì)導(dǎo)致子類(lèi)同名成員變量被父類(lèi)的值覆蓋。
Json序列化:是一種輕量級(jí)的數(shù)據(jù)交換格式。JSON 序列化就是將數(shù)據(jù)對(duì)象轉(zhuǎn)換為 JSON 字符串。在序列化過(guò)程中拋棄了類(lèi)型信息,所以反序列化時(shí)只有提供類(lèi)型信息才能準(zhǔn)確地反序列化。相比前兩種方式,JSON 可讀性比較好,方便調(diào)試
序列化通常會(huì)通過(guò)網(wǎng)絡(luò)傳輸對(duì)象,而對(duì)象中往往有敏感數(shù)據(jù),所以攻擊者可以巧妙地利用反序列化過(guò)程構(gòu)造惡意代碼,使得程序在反序列化的過(guò)程中執(zhí)行任意代碼。
Java 工程中廣泛使用的 Apache Commons Collections 、Jackson 、 fastjson 等都出現(xiàn)過(guò)反序列化漏洞。如何防范這種黑客攻擊呢?
有些對(duì)象的敏感屬性不需要進(jìn)行序列化傳輸,可以加 transient 關(guān)鍵字,避免把此屬性信息轉(zhuǎn)化為序列化的二進(jìn)制流。如果一定要傳遞對(duì)象的敏感屬性,可以使用對(duì)稱(chēng)與非對(duì)稱(chēng)加密方式獨(dú)立傳輸,再使用某個(gè)方法把屬性還原到對(duì)象中。總之吧,就是有一定的防范意識(shí)
我們接下來(lái)要分析的就是調(diào)用過(guò)程了,先看一下官網(wǎng)的流程圖
首先服務(wù)消費(fèi)者通過(guò)代理對(duì)象 Proxy 發(fā)起遠(yuǎn)程調(diào)用,接著通過(guò)網(wǎng)絡(luò)客戶(hù)端 Client 將編碼后的請(qǐng)求發(fā)送給服務(wù)提供方的網(wǎng)絡(luò)層上,也就是 Server。Server 在收到請(qǐng)求后,首先要做的事情是對(duì)數(shù)據(jù)包進(jìn)行解碼。然后將解碼后的請(qǐng)求發(fā)送至分發(fā)器 Dispatcher,再由分發(fā)器將請(qǐng)求派發(fā)到指定的線(xiàn)程池上,最后由線(xiàn)程池調(diào)用具體的服務(wù)。這就是一個(gè)遠(yuǎn)程調(diào)用請(qǐng)求的發(fā)送與接收過(guò)程。
整個(gè)調(diào)用鏈路大概就是分為三個(gè)步驟,我們按照這幾個(gè)步驟來(lái)分析下:
1、消費(fèi)者發(fā)起調(diào)用請(qǐng)求
2、提供者接收處理請(qǐng)求
3、消費(fèi)者處理響應(yīng)
消費(fèi)者發(fā)起調(diào)用請(qǐng)求
消費(fèi)者調(diào)用 Invoker 時(shí),實(shí)際上調(diào)用的是一個(gè)由 Java 動(dòng)態(tài)代理生成的代理對(duì)象。該代理對(duì)象經(jīng)過(guò) Cluster 層的路由與負(fù)載均衡,找到一個(gè)服務(wù)節(jié)點(diǎn),將調(diào)用參數(shù)封裝成 Request 形式,通過(guò) Netty Client 將數(shù)據(jù)進(jìn)行序列化,通過(guò) Netty 發(fā)送給對(duì)應(yīng)的服務(wù)提供者。
調(diào)用具體的接口會(huì)調(diào)用生成的代理類(lèi),而代理類(lèi)會(huì)生成一個(gè)RpcInvocation對(duì)象調(diào)用MockClusterInvoke#invoke 方法,包括方法名、參數(shù)類(lèi)和參數(shù)值,其實(shí)實(shí)際上最終調(diào)用的是AbstractClusterInvoker#invoker方法
入口在InvocationHandler這個(gè)家伙,這個(gè)類(lèi)其中的invoke會(huì)得到調(diào)用結(jié)果,并把結(jié)果返回給調(diào)用方
InvokerInvocationHandler 中的 invoker 成員變量類(lèi)型為 MockClusterInvoker,MockClusterInvoker 內(nèi)部封裝了服務(wù)降級(jí)邏輯。下面簡(jiǎn)單看一下:
服務(wù)降級(jí)就簡(jiǎn)單看看就行了先,當(dāng)然這也不是服務(wù)調(diào)用的重點(diǎn)
上面這塊代碼是AbstractInvoker類(lèi)中的invoke方法,看注釋分別是準(zhǔn)備RPC的調(diào)用列表,然后是真正的調(diào)用并且返回結(jié)果,如果是異步的,則等待結(jié)果返回;
重點(diǎn)在第二步的doInvokeAndReturn中,點(diǎn)進(jìn)去看到實(shí)際執(zhí)行的是doInvoke方法,而這個(gè)方法在這個(gè)類(lèi)中又是個(gè)抽象方法,需要由子類(lèi)實(shí)現(xiàn),下面到 DubboInvoker 中看一下。
上面的代碼包含了 Dubbo 對(duì)同步和異步調(diào)用的處理邏輯,搞懂了上面的代碼,會(huì)對(duì) Dubbo 的同步和異步調(diào)用方式有更深入的了解。Dubbo 實(shí)現(xiàn)同步和異步調(diào)用比較關(guān)鍵的一點(diǎn)就在于由誰(shuí)調(diào)用 CompletableFuture 的 get 方法。同步調(diào)用模式下,由框架自身調(diào)用 CompletableFuture 的 get 方法。異步調(diào)用模式下,則由用戶(hù)調(diào)用該方法。
當(dāng)服務(wù)消費(fèi)者還未接收到調(diào)用結(jié)果時(shí),用戶(hù)線(xiàn)程調(diào)用 get 方法會(huì)被阻塞住。同步調(diào)用模式下,框架獲得 DefaultFuture 對(duì)象后,會(huì)立即調(diào)用 get 方法進(jìn)行等待。而異步模式下則是將該對(duì)象封裝到 FutureAdapter 實(shí)例中,并將 FutureAdapter 實(shí)例設(shè)置到 RpcContext 中,供用戶(hù)使用
接下來(lái)我們看一個(gè)客戶(hù)端HeaderExchangeClient,HeaderExchangeClient 中很多方法只有一行代碼,即調(diào)用 HeaderExchangeChannel 對(duì)象的同簽名方法。那 HeaderExchangeClient 有什么用處呢?答案是封裝了一些關(guān)于心跳檢測(cè)的邏輯
來(lái)到了其內(nèi)部的屬性HeaderExchangeChannel這個(gè)類(lèi)之后,大家終于看到了 Request 語(yǔ)義了,上面的方法首先定義了一個(gè) Request 對(duì)象,然后再將該對(duì)象傳給 NettyClient 的 send 方法,進(jìn)行后續(xù)的調(diào)用
需要說(shuō)明的是,NettyClient 中并未實(shí)現(xiàn) send 方法,該方法繼承自父類(lèi) AbstractPeer,看其子類(lèi)AbstractClient類(lèi)中的send實(shí)現(xiàn)
然后就是NettyChannel的send發(fā)送了
提供者接收處理請(qǐng)求
默認(rèn)情況下 Dubbo 使用 Netty 作為底層的通信框架。Netty 檢測(cè)到有數(shù)據(jù)入站后,首先會(huì)通過(guò)解碼器對(duì)數(shù)據(jù)進(jìn)行解碼,并將解碼后的數(shù)據(jù)包裝成一個(gè)request對(duì)象傳遞給下一個(gè)入站處理器的指定方法。
解碼器將數(shù)據(jù)包解析成 Request 對(duì)象后,NettyHandler 的 messageReceived 方法緊接著會(huì)收到這個(gè)對(duì)象,并將這個(gè)對(duì)象繼續(xù)向下傳遞。
這期間該對(duì)象會(huì)被依次傳遞給 NettyServer、MultiMessageHandler、HeartbeatHandler 以及 AllChannelHandler。最后由 AllChannelHandler 將該對(duì)象封裝到 Runnable 實(shí)現(xiàn)類(lèi)對(duì)象中,并將 Runnable 放入線(xiàn)程池中執(zhí)行后續(xù)的調(diào)用邏輯
我們了解一下Dubbo的線(xiàn)程派發(fā)模型:
背景呢就是如果一個(gè)處理事件很快執(zhí)行完,此時(shí)可以直接在IO線(xiàn)程上執(zhí)行完就行了,但是如果處理比較耗時(shí)呢,比如邏輯可能會(huì)發(fā)起DB查詢(xún)或者HTTP請(qǐng)求,此時(shí)這種就不應(yīng)該讓事件處理邏輯在IO線(xiàn)程上執(zhí)行,而是直接派發(fā)到線(xiàn)程池中去執(zhí)行
原因很簡(jiǎn)單,IO線(xiàn)程主要是用來(lái)接收請(qǐng)求,如果IO被占滿(mǎn)被阻塞,就不能接收新的請(qǐng)求了
舉個(gè)例子,大公司業(yè)務(wù)數(shù)量大,核心部門(mén)A主要是負(fù)責(zé)分發(fā)業(yè)務(wù)的處理,會(huì)有其余部門(mén)分別處理,一些很簡(jiǎn)答的業(yè)務(wù)處理甚至還沒(méi)有分發(fā)任務(wù)的耗時(shí)時(shí)間長(zhǎng),所以核心部門(mén)就直接處理了,你想啊,發(fā)一個(gè)任務(wù)一秒,如果處理這個(gè)業(yè)務(wù)只需要0.5秒,那就沒(méi)必要去發(fā)這個(gè)業(yè)務(wù)了,直接自己處理了就可以了
于是就有了這個(gè)線(xiàn)程派發(fā)模型
Dispatcher就是線(xiàn)程派發(fā)器,但是它本身不具有線(xiàn)程派發(fā)能力,它的職責(zé)是創(chuàng)建具有線(xiàn)程派發(fā)能力的ChannelHandler,比如 AllChannelHandler、MessageOnlyChannelHandler 和 ExecutionChannelHandler 等,Dubbo 支持 5 種不同的線(xiàn)程派發(fā)策略
默認(rèn)配置下,Dubbo 使用 all 派發(fā)策略,即將所有的消息都派發(fā)到線(xiàn)程池中
處理的邏輯我覺(jué)得沒(méi)什么必要細(xì)細(xì)分析了,無(wú)非就是封裝成Runnable交給handler分發(fā)的線(xiàn)程來(lái)處理,然后把結(jié)果封裝成response,返回該對(duì)象
消費(fèi)者處理響應(yīng)
響應(yīng)數(shù)據(jù)解碼完成后,Dubbo 會(huì)將響應(yīng)對(duì)象派發(fā)到線(xiàn)程池上。要注意的是,線(xiàn)程池中的線(xiàn)程并非用戶(hù)的調(diào)用線(xiàn)程,所以要想辦法將響應(yīng)對(duì)象從線(xiàn)程池線(xiàn)程傳遞到用戶(hù)線(xiàn)程上。
一般情況下,服務(wù)消費(fèi)方會(huì)并發(fā)調(diào)用多個(gè)服務(wù),每個(gè)用戶(hù)線(xiàn)程發(fā)送請(qǐng)求后,會(huì)調(diào)用不同 DefaultFuture 對(duì)象的 get 方法進(jìn)行等待。
一段時(shí)間后,服務(wù)消費(fèi)方的線(xiàn)程池會(huì)收到多個(gè)響應(yīng)對(duì)象。這個(gè)時(shí)候要考慮一個(gè)問(wèn)題,如何將每個(gè)響應(yīng)對(duì)象傳遞給相應(yīng)的 DefaultFuture 對(duì)象,且不出錯(cuò)
消費(fèi)者接收到提供者發(fā)來(lái)的響應(yīng),解碼后投入到線(xiàn)程分發(fā)器中,置入線(xiàn)程池。
放到線(xiàn)程池的是一個(gè) DefaultFuture 對(duì)象,其中包含了響應(yīng)結(jié)果。在前面第一步發(fā)起調(diào)用請(qǐng)求的過(guò)程中,負(fù)載均衡之后的調(diào)用就是通過(guò) RpcInvocation 代理對(duì)象使用 DefaultFuture.get() 方法異步獲取響應(yīng)內(nèi)容,這也是 RPC 遠(yuǎn)程調(diào)用從同步轉(zhuǎn)為異步的方式。
答案是通過(guò)調(diào)用編號(hào)。DefaultFuture 被創(chuàng)建時(shí),會(huì)要求傳入一個(gè) Request 對(duì)象。此時(shí) DefaultFuture 可從 Request 對(duì)象中獲取調(diào)用編號(hào),并將 <調(diào)用編號(hào), DefaultFuture 對(duì)象> 映射關(guān)系存入到靜態(tài) Map 中,即 FUTURES
線(xiàn)程池中的線(xiàn)程在收到 Response 對(duì)象后,會(huì)根據(jù) Response 對(duì)象中的調(diào)用編號(hào)到 FUTURES 集合中取出相應(yīng)的 DefaultFuture 對(duì)象,然后再將 Response 對(duì)象設(shè)置到 DefaultFuture 對(duì)象中。
最后再喚醒用戶(hù)線(xiàn)程,這樣用戶(hù)線(xiàn)程即可從 DefaultFuture 對(duì)象中獲取調(diào)用結(jié)果了