Dubbo 核心組件
層次 | 作用 |
---|---|
Service | 該層與業(yè)務(wù)邏輯相關(guān),根據(jù) provider 和 consumer 的業(yè)務(wù)設(shè)計(jì)對(duì)應(yīng)的接口和實(shí)現(xiàn) |
Config | 對(duì)外配置接口,以 ServiceConfig 和 ReferenceConfig 為中心慨蛙》温疲可以理解為該層管理了整個(gè) Dubbo 配置 |
Proxy | 使用動(dòng)態(tài)代理的方式為接口創(chuàng)建代理類,Proxy 層最主要的接口就是 ProxyFactory扛禽。其默認(rèn)的擴(kuò)展點(diǎn)有:stub懂牧、jdk侈净、javassist。jdk 使用反射的方式創(chuàng)建代理類,javassist 通過(guò)拼接字符串然后編譯的方式創(chuàng)建代理類畜侦。對(duì)于服務(wù)提供者元扔,代理的對(duì)象是接口的真實(shí)實(shí)現(xiàn)。 對(duì)于服務(wù)消費(fèi)者旋膳,代理的對(duì)象是遠(yuǎn)程服務(wù)的 invoker 對(duì)象 |
Registry | 主要負(fù)責(zé)的就是服務(wù)的注冊(cè)與發(fā)現(xiàn)澎语。這層的主要接口就是 RegistryFactory,其接口方法有 @Adaptive 注解溺忧,會(huì)根據(jù)參數(shù) protocol 來(lái)選擇實(shí)現(xiàn)咏连,默認(rèn)的擴(kuò)展實(shí)現(xiàn)有:zookeeper、redis鲁森、multicast(廣播模式)、內(nèi)存 |
Cluster | 集群容錯(cuò)層振惰,主要負(fù)責(zé):遠(yuǎn)程調(diào)用時(shí)的容錯(cuò)策略(如快速失敗歌溉、失敗重試);選擇具體調(diào)用節(jié)點(diǎn)的負(fù)載均衡策略(如隨機(jī)骑晶、一致性 hash 等)痛垛;特殊調(diào)用路徑的路由策略(如消費(fèi)者只會(huì)調(diào)用某個(gè) IP 的生產(chǎn)者) |
Monitor | 負(fù)責(zé)監(jiān)控統(tǒng)計(jì)調(diào)用次數(shù)和調(diào)用時(shí)間 |
Protocal | 遠(yuǎn)程調(diào)用層,封裝 RPC 調(diào)用的具體過(guò)程桶蛔。Protocal 是 Invoker 暴露(發(fā)布一個(gè)任務(wù)讓消費(fèi)者調(diào)用)和引用(引用一個(gè)服務(wù)到本地)的主功能入口匙头,它負(fù)責(zé)管理 Invoker 的整個(gè)生命周期。Invoker 是 Dubbo 的核心模型仔雷,它代表一個(gè)可執(zhí)行體蹂析,允許向它發(fā)起 invoke 調(diào)用,它可能執(zhí)行一個(gè)本地接口實(shí)現(xiàn)碟婆,也可能執(zhí)行一個(gè)遠(yuǎn)程接口實(shí)現(xiàn) |
Exchange | 封裝請(qǐng)求響應(yīng)模式电抚,如如何將同步請(qǐng)求轉(zhuǎn)化為異步請(qǐng)求。以 Request 和 Response 為中心竖共,擴(kuò)展接口為 Exchanger蝙叛、ExchangeChannel、ExchangeClient 和 ExchangeServer |
Transport | 網(wǎng)絡(luò)傳輸層公给,抽象 Mina 和 Netty 為統(tǒng)一接口借帘。用戶也可以擴(kuò)展接口添加更多網(wǎng)絡(luò)傳輸方式 |
Serialize | 序列化的作用是把對(duì)象轉(zhuǎn)化為二進(jìn)制流,然后在網(wǎng)絡(luò)中傳輸淌铐。負(fù)責(zé)整個(gè)框架網(wǎng)絡(luò)傳輸時(shí)的序列化和反序列化工作 |
Dubbo 總體調(diào)用流程
服務(wù)暴露過(guò)程
服務(wù)端在框架啟動(dòng)時(shí)肺然,會(huì)初始化服務(wù)實(shí)例,通過(guò) Proxy 組件調(diào)用具體協(xié)議(Protocol)匣沼,把服務(wù)端要暴露的接口封裝成 Invoker(真實(shí)是類型是 AbstractInvoker)狰挡,然后轉(zhuǎn)換成 Exporter,這時(shí)框架會(huì)打開(kāi)服務(wù)端口并記錄服務(wù)實(shí)例到內(nèi)存中,最后通過(guò) Registry 把服務(wù)元數(shù)據(jù)注冊(cè)到注冊(cè)中心加叁。
- Proxy 組件:Dubbo 中只需要引用一個(gè)接口就可以調(diào)用遠(yuǎn)程服務(wù)倦沧。其實(shí)這是 Dubbo 為我們生成的代理類,調(diào)用的方法是 Proxy 組件生成的代理方法它匕,會(huì)自動(dòng)發(fā)起遠(yuǎn)程/本地調(diào)用展融,并返回結(jié)果。整個(gè)過(guò)程對(duì)用戶全透明
- Protocol:可以將對(duì)接口的配置分居不同的協(xié)議轉(zhuǎn)化成不同的 Invoker 對(duì)象豫柬。例如協(xié)議為 dubbo 時(shí)會(huì)將遠(yuǎn)程接口配置轉(zhuǎn)換成一個(gè) DubboInvoker
- Exporter:用于暴露到注冊(cè)中心的對(duì)象告希,持有一個(gè) Invoker 對(duì)象
- Registry:將 Exporter 注冊(cè)到注冊(cè)中心
以上就是服務(wù)暴露過(guò)程。消費(fèi)者在啟動(dòng)時(shí)會(huì)通過(guò) Registry 在注冊(cè)中心訂閱服務(wù)端的元數(shù)據(jù)(包括 IP 和 端口)烧给,并在第一個(gè)初始化時(shí)從注冊(cè)中心**拉取全量服務(wù)端信息****
消費(fèi)者調(diào)用流程
首先調(diào)用也是從一個(gè) Proxy 開(kāi)始燕偶,Proxy 持有一個(gè) Invoker 對(duì)象并觸發(fā) invoke 調(diào)用。在 invoke 調(diào)用過(guò)程中础嫡,使用 Cluster 進(jìn)行容錯(cuò)指么、路由以及負(fù)載均衡。Cluster 先通過(guò) Directory 獲取可用的遠(yuǎn)程服務(wù) Invoker 列表榴鼎,根據(jù)用戶配置的路由規(guī)則(例如指定某些方法智能調(diào)用某個(gè)節(jié)點(diǎn))將 Invoker 過(guò)濾一遍
然后存活下來(lái)的 Invoker 通過(guò) LoadBalance 方法做負(fù)載均衡伯诬,選出一個(gè)可以調(diào)用的 Invoker。選中的 Invoker 會(huì)在調(diào)用 invoke 之前經(jīng)過(guò)一個(gè)過(guò)濾器鏈(通常處理上下文巫财、限流盗似、計(jì)數(shù)等操作)
接著會(huì)使用 Network Client 做數(shù)據(jù)傳輸。傳輸之前通過(guò) Codec 做私有協(xié)議構(gòu)造平项,然后進(jìn)行序列化傳輸至服務(wù)端赫舒。服務(wù)端接收到數(shù)據(jù)包后也會(huì)使用 Codec 處理協(xié)議頭等,完成后對(duì)數(shù)據(jù)報(bào)文進(jìn)行反序列化處理
隨后遠(yuǎn)程調(diào)用請(qǐng)求(Request)被分配到 ThreadPool 中進(jìn)行處理葵礼。Server 會(huì)處理這些 Request号阿,根據(jù)請(qǐng)求查找對(duì)應(yīng)的 Exporter(內(nèi)部持有 Invoker),Invoker 用裝飾器模式套了很多 Filter鸳粉,因此在 Invoker 調(diào)用之前會(huì)經(jīng)過(guò)服務(wù)端的過(guò)濾鏈
最終得到了服務(wù)端具體的接口實(shí)現(xiàn)并調(diào)用扔涧,然后將結(jié)果原路返回
Dubbo 注冊(cè)中心
在 Dubbo 為服務(wù)體系中,注冊(cè)中心是核心組件之一届谈。Dubbo 通過(guò)注冊(cè)中心實(shí)現(xiàn)了分布式環(huán)境中服務(wù)的注冊(cè)和發(fā)現(xiàn)枯夜,是各個(gè)分布式節(jié)點(diǎn)之前的紐帶。其主要作用如下:
- 動(dòng)態(tài)加入:一個(gè)服務(wù)提供者通過(guò)注冊(cè)中心可以動(dòng)態(tài)地把自己暴露給其他消費(fèi)者艰山,無(wú)需消費(fèi)者逐個(gè)更新配置文件
- 動(dòng)態(tài)發(fā)現(xiàn):一個(gè)消費(fèi)者可以動(dòng)態(tài)感知新的配置湖雹、路由規(guī)劃和服務(wù)提供者,無(wú)需重啟服務(wù)使之生效
- 動(dòng)態(tài)調(diào)整:注冊(cè)中心支持參數(shù)動(dòng)態(tài)調(diào)整曙搬,新參數(shù)自動(dòng)更新到所有相關(guān)服務(wù)點(diǎn)
- 統(tǒng)一配置:避免本地配置導(dǎo)致每個(gè)服務(wù)的配置不一致問(wèn)題
1 工作流程
注冊(cè)中心整體流程如下圖所示:
- 服務(wù)提供者啟動(dòng)時(shí)摔吏,會(huì)向注冊(cè)中心寫入自己的元數(shù)據(jù)信息鸽嫂,同時(shí)會(huì)訂閱配置元數(shù)據(jù)信息
- 消費(fèi)者啟動(dòng)時(shí),會(huì)向注冊(cè)中心寫入自己的元數(shù)據(jù)信息征讲,同時(shí)訂閱服務(wù)提供者据某、配置蛛芥、路由元數(shù)據(jù)信息
- 服務(wù)治理中心(dubbo-admin)啟動(dòng)時(shí)蒋困,會(huì)同時(shí)訂閱 服務(wù)提供者、消費(fèi)者程拭、配置滤祖、路由元數(shù)據(jù)信息
- 當(dāng)有服務(wù)離開(kāi)或新的服務(wù)加入時(shí)筷狼,注冊(cè)中心服務(wù)提供者目錄會(huì)發(fā)生變化,變化信息將動(dòng)態(tài)通知給消費(fèi)者和服務(wù)治理中心
- 當(dāng)消費(fèi)者發(fā)起服務(wù)調(diào)用時(shí)匠童,會(huì)異步將調(diào)用埂材、統(tǒng)計(jì)信息上報(bào)至監(jiān)控中心(dubbo-monitor)
2 數(shù)據(jù)結(jié)構(gòu)
注冊(cè)中心的總體流程相同,但不同的注冊(cè)中心有不同的實(shí)現(xiàn)方式汤求,其數(shù)據(jù)結(jié)構(gòu)也不同楞遏。常見(jiàn)的注冊(cè)中心實(shí)現(xiàn)是 Zookeeper 和 Redis。下面重點(diǎn)介紹注冊(cè)中心的 Zookeeper 的實(shí)現(xiàn)方式
Zookeeper 是樹(shù)形結(jié)構(gòu)的注冊(cè)中心首昔,每個(gè)節(jié)點(diǎn)分為持久節(jié)點(diǎn)、持久順序節(jié)點(diǎn)糙俗、臨時(shí)節(jié)點(diǎn)勒奇、臨時(shí)順序節(jié)點(diǎn)
- 持久節(jié)點(diǎn):服務(wù)注冊(cè)后保證節(jié)點(diǎn)不會(huì)丟失,注冊(cè)中心重啟仍然節(jié)點(diǎn)存在
- 持久順序節(jié)點(diǎn):在持久節(jié)點(diǎn)的基礎(chǔ)上增加節(jié)點(diǎn)順序功能
- 臨時(shí)節(jié)點(diǎn):服務(wù)注冊(cè)后連接丟失或 session 超時(shí)巧骚,注冊(cè)節(jié)點(diǎn)會(huì)被自動(dòng)移除
- 臨時(shí)順序節(jié)點(diǎn):在臨時(shí)節(jié)點(diǎn)的基礎(chǔ)上增加節(jié)點(diǎn)順序功能
Dubbo 使用 Zookeeper 作為注冊(cè)中心時(shí)赊颠,只使用持久節(jié)點(diǎn)和臨時(shí)節(jié)點(diǎn)
假設(shè) /dubbo/com.test.TestService/providers
是服務(wù)提供者在 Zookeeper 上的注冊(cè)路徑,該結(jié)構(gòu)分為四層:root(根節(jié)點(diǎn)劈彪,默認(rèn)是 dubbo)竣蹦、service(接口名稱,對(duì)應(yīng)于 com.test.TestService)沧奴、四種服務(wù)目錄(示例中的 providers 以及 consumers痘括、routers、configurators)滔吠。在服務(wù)分類節(jié)點(diǎn)下是具體的 DUbbo 服務(wù) URL纲菌,樹(shù)形結(jié)構(gòu)實(shí)例如下:
+ /dubbo
+-- com.test.TestService
+-- providers
+-- consumers
+-- routers
+-- configurators
- 樹(shù)節(jié)點(diǎn)的根目錄是注冊(cè)中心分組,下面有多個(gè)服務(wù)接口疮绷,分組值來(lái)自用戶配置 < dubbo:registry > 中的 group 屬性
- 服務(wù)接口下包含 4 個(gè)子目錄(providers翰舌、consumers、routers 以及 configurators)冬骚,都是持久節(jié)點(diǎn)
- 服務(wù)提供者目錄(
/dubbo/com.test.TestService/providers
)下面包含多個(gè)服務(wù)提供者 URL 元數(shù)據(jù)信息 - 服務(wù)消費(fèi)者目錄(
/dubbo/com.test.TestService/consumers
)下面包含多個(gè)消費(fèi)者 URL 元數(shù)據(jù)信息 - 路由配置目錄(
/dubbo/com.test.TestService/routers
)下面包含多個(gè)用于消費(fèi)者路由策略 URL 元數(shù)據(jù)信息 - 動(dòng)態(tài)配置目錄(
/dubbo/com.test.TestService/configurators
)下面包含多個(gè)用于服務(wù)提供者動(dòng)態(tài)配置 URL 元數(shù)據(jù)信息
3 訂閱發(fā)布的實(shí)現(xiàn)
3.1 發(fā)布實(shí)現(xiàn)
服務(wù)提供者和服務(wù)消費(fèi)者都需要將自己的元數(shù)據(jù)信息注冊(cè)到注冊(cè)中心椅贱。服務(wù)提供者的注冊(cè)是為了讓服務(wù)消費(fèi)者感知服務(wù)的存在懂算,從而發(fā)起遠(yuǎn)程調(diào)用;也讓服務(wù)治理中心感知有新的服務(wù)提供者上線庇麦。消費(fèi)者的注冊(cè)是讓了讓服務(wù)治理中心可以發(fā)現(xiàn)自己
Zookeeper 實(shí)現(xiàn)發(fā)布的代碼很簡(jiǎn)單计技,只是調(diào)用了 Zookeeper 客戶端在注冊(cè)中心上創(chuàng)建一個(gè)目錄
zkClient.create(toUrlPath(url));
取消發(fā)布也很簡(jiǎn)單,只是把 ZooKeeper 注冊(cè)中心上對(duì)應(yīng)的路徑刪除即可
zkClient.delete(toUrlPath(url));
3.2 訂閱實(shí)現(xiàn)
訂閱通常有 pull 和 push 兩種方式女器,Dubbo 目前采用 pull(拉取方式)酸役,后續(xù)接收事件并重新拉取數(shù)據(jù)
在服務(wù)暴露時(shí),服務(wù)提供者會(huì)訂閱 configurators 用于監(jiān)聽(tīng)動(dòng)態(tài)配置驾胆。消費(fèi)者啟動(dòng)時(shí)會(huì)訂閱 providers涣澡、routers 以及 configurators 這三個(gè)目錄
Dubbo 中 Zookeeper 的客戶端實(shí)現(xiàn)有兩種:
- Apache Curator(默認(rèn))
- zKClient
用戶可以通過(guò) < dubbo:registry > 的 client 屬性設(shè)置 curator 或 zkclient 作為 Zookeeper 客戶端的實(shí)現(xiàn)方式
Zookeeper 注冊(cè)中心采用的是 事件通知 + 客戶端拉取 的方式,客戶端第一次連接上注冊(cè)中心時(shí)丧诺,會(huì)獲取對(duì)應(yīng)目錄下的全量數(shù)據(jù)入桂,并在訂閱的節(jié)點(diǎn)上注冊(cè)一個(gè) watcher,客戶端于注冊(cè)中心保持 TCP 長(zhǎng)連接驳阎,后續(xù)每個(gè)節(jié)點(diǎn)有數(shù)據(jù)變化的時(shí)候抗愁,注冊(cè)中心會(huì)根據(jù) watcher 的回調(diào)通知客戶端數(shù)據(jù)有更新,客戶端會(huì)將對(duì)應(yīng)節(jié)點(diǎn)下的數(shù)據(jù)重新從注冊(cè)中心拉取過(guò)來(lái)呵晚。
Dubbo 啟停原理解析
1 優(yōu)雅停機(jī)原理解析
優(yōu)雅停機(jī)特性是所有 RPC 框架中非常重要的特性之一蜘腌,因?yàn)楹诵臉I(yè)務(wù)在服務(wù)器正在執(zhí)行時(shí)突然中斷可能出現(xiàn)嚴(yán)重后果。Dubbo 的優(yōu)雅停機(jī)原理如下圖所示:
Dubbo 中實(shí)現(xiàn)優(yōu)雅停機(jī)有幾下幾步:
- 收到 kill -9 進(jìn)程退出信號(hào)饵隙,Spring 容器會(huì)觸發(fā)容器銷毀事件
- provider 端取消注冊(cè)服務(wù)元數(shù)據(jù)信息
- consumer 端會(huì)收到最新的服務(wù)調(diào)用列表(不包含準(zhǔn)備停機(jī)的地址)
- Dubbo 協(xié)議發(fā)送 readonly 事件報(bào)文通知 consumer 服務(wù)不可用
- 服務(wù)端等待已執(zhí)行的任務(wù)結(jié)束并拒絕新任務(wù)執(zhí)行
- provider 端斷開(kāi)與 consumer 端的 TCP 連接
既然第三步 consumer 端已經(jīng)被通知最新的服務(wù)調(diào)用地址撮珠,provider 端為什么還要發(fā)送 readonly 事件報(bào)文給 consumer 端呢?這里主要考慮到注冊(cè)中心推送服務(wù)有網(wǎng)絡(luò)延遲金矛,以及客戶端計(jì)算服務(wù)列表可能占用一些時(shí)間芯急。Dubbo 協(xié)議發(fā)送 readonly 事件報(bào)文時(shí),consumer 端會(huì)設(shè)置響應(yīng)的 provider 為不可用狀態(tài)驶俊,下次負(fù)載均衡將不再調(diào)用下線的機(jī)器
Dubbo 遠(yuǎn)程調(diào)用
1 Dubbo 通信協(xié)議
Dubbo 支持 9 中通信協(xié)議
2.1 dubbo://(推薦)
描述:Dubbo 默認(rèn)通信協(xié)議娶耍,單一長(zhǎng)連接,進(jìn)行的是 NIO 異步通信饼酿,基于 hessian 作為序列化協(xié)議
連接個(gè)數(shù):?jiǎn)芜B接
連接方式:長(zhǎng)鏈接
傳輸協(xié)議:TCP
傳輸方式:NIO 異步傳輸
序列化:Hessian 二進(jìn)制序列化
適用范圍:
- 傳入傳出參數(shù)數(shù)據(jù)包較虚啪啤(建議小于100K)
- 消費(fèi)者比提供者個(gè)數(shù)多,單一消費(fèi)者無(wú)法壓滿提供者
- 盡量不要用 dubbo 協(xié)議傳輸大文件或超大字符串
適用場(chǎng)景 :
- 高并發(fā)場(chǎng)景嗜湃,一般是服務(wù)提供者就幾臺(tái)機(jī)器奈应,但是服務(wù)消費(fèi)者有上百臺(tái),可能每天調(diào)用量達(dá)到上億次购披!此時(shí)用長(zhǎng)連接是最合適的杖挣,就是跟每個(gè)服務(wù)消費(fèi)者維持一個(gè)長(zhǎng)連接就可以,可能總共就100個(gè)連接刚陡。然后后面直接基于長(zhǎng)連接 NIO 異步通信惩妇,可以支撐高并發(fā)請(qǐng)求 株汉,如果上億次請(qǐng)求每次都是短連接的話,服務(wù)提供者會(huì)扛不住
- 因?yàn)樽叩氖菃我婚L(zhǎng)連接歌殃,所以傳輸數(shù)據(jù)量太大的話乔妈,會(huì)導(dǎo)致并發(fā)能力降低。所以一般建議是傳輸數(shù)據(jù)量很小氓皱,支撐高并發(fā)訪問(wèn)
約束 :
- 參數(shù)及返回值需實(shí)現(xiàn) Serializable 接口
- 參數(shù)及返回值不能自定義實(shí)現(xiàn) List, Map, Number, Date, Calendar 等接口路召,只能用 JDK 自帶的實(shí)現(xiàn),因?yàn)?hessian 會(huì)做特殊處理波材,自定義實(shí)現(xiàn)類中的屬性值都會(huì)丟失
- Hessian 序列化股淡,只傳成員屬性值和值的類型,不傳方法或靜態(tài)變量
2.2 rmi://
描述:RMI 協(xié)議采用 JDK 標(biāo)準(zhǔn)的 java.rmi.* 實(shí)現(xiàn)廷区,采用阻塞式短連接和 JDK 標(biāo)準(zhǔn)序列化方式
連接個(gè)數(shù):多連接
連接方式:短鏈接
傳輸協(xié)議:TCP
傳輸方式:同步傳輸
序列化:Java 標(biāo)準(zhǔn)二進(jìn)制序列化
適用范圍:
- 傳入傳出參數(shù)數(shù)據(jù)包大小混合
- 消費(fèi)者與提供者個(gè)數(shù)差不多
- 可傳文件
適用場(chǎng)景 :
- 常規(guī)遠(yuǎn)程服務(wù)方法調(diào)用唯灵,與原生RMI服務(wù)互操作,一般較少用
約束 :
- 參數(shù)及返回值需實(shí)現(xiàn) Serializable 接口
- dubbo 配置中的超時(shí)時(shí)間對(duì) RMI 無(wú)效隙轻,需使用 java 啟動(dòng)參數(shù)設(shè)置
-Dsun.rmi.transport.tcp.responseTimeout=3000
2.3 hessian://
描述:Hessian 協(xié)議用于集成 Hessian 的服務(wù)埠帕,Hessian 底層采用 Http 通訊,采用 Servlet 暴露服務(wù)玖绿,Dubbo 缺省內(nèi)嵌 Jetty 作為服務(wù)器實(shí)現(xiàn)
連接個(gè)數(shù):多連接
連接方式:短鏈接
傳輸協(xié)議:HTTP
傳輸方式:同步傳輸
序列化:Hessian 二進(jìn)制序列化
適用范圍:
- 傳入傳出參數(shù)數(shù)據(jù)包較大
- 提供者比消費(fèi)者個(gè)數(shù)多敛瓷,提供者壓力較大
- 可傳文件
適用場(chǎng)景 :
- 頁(yè)面?zhèn)鬏?/li>
- 文件傳輸
- 原生 Hessian 服務(wù)互操作
約束 :
- 同 dubbo 協(xié)議
2.4 http://
描述:基于 HTTP 表單的遠(yuǎn)程調(diào)用協(xié)議,采用 Spring 的 HttpInvoker 實(shí)現(xiàn)
連接個(gè)數(shù):多連接
連接方式:短鏈接
傳輸協(xié)議:HTTP
傳輸方式:同步傳輸
序列化:表單序列化
適用范圍:
- 傳入傳出參數(shù)數(shù)據(jù)包大小混合斑匪,提供者比消費(fèi)者個(gè)數(shù)多琐驴,可用瀏覽器查看,可用表單或 URL 傳入?yún)?shù)秤标,暫不支持傳文件
適用場(chǎng)景 :
- 需同時(shí)給應(yīng)用程序和瀏覽器 JS 使用的服務(wù)
約束 :
- 參數(shù)及返回值需符合 Bean 規(guī)范
2.5 webservice://
描述:基于 WebService 的遠(yuǎn)程調(diào)用協(xié)議,基于 Apache CXF 的 frontend-simple 和 transports-http 實(shí)現(xiàn)
連接個(gè)數(shù):多連接
連接方式:短鏈接
傳輸協(xié)議:HTTP
傳輸方式:同步傳輸
序列化:同步傳輸 SOAP 文本序列化
適用場(chǎng)景 :
- 系統(tǒng)集成宙刘,跨語(yǔ)言調(diào)用
2.6 thrift://
描述:當(dāng)前 dubbo 支持 的 thrift 協(xié)議是對(duì) thrift 原生協(xié)議的擴(kuò)展苍姜,在原生協(xié)議的基礎(chǔ)上添加了一些額外的頭信息,比如 service name悬包,magic number 等
2.7 memcached://
描述:基于 memcached 實(shí)現(xiàn)的 RPC 協(xié)議
2.8 redis://
描述:基于 Redis 實(shí)現(xiàn)的 RPC 協(xié)議
2.9 rest://
描述:基于標(biāo)準(zhǔn)的 Java REST API——JAX-RS 2.0(Java API for RESTful Web Services的簡(jiǎn)寫)實(shí)現(xiàn)的REST調(diào)用支持
2 dubbo:// 協(xié)議詳解
Dubbo 數(shù)據(jù)包分為消息頭和消息體衙猪,消息頭用于存儲(chǔ)一些元信息,比如魔法數(shù)(Magic)布近,數(shù)據(jù)包類型(Request/Response)垫释,消息體長(zhǎng)度(Data Length)等。消息體中用于存儲(chǔ)具體的調(diào)用消息撑瞧,比如方法名稱棵譬,參數(shù)列表等。下面簡(jiǎn)單列舉一下消息頭的內(nèi)容
偏移量(Bit) | 字段 | 取值 |
---|---|---|
0 ~ 7 | 魔數(shù)高位 | 0xda00 |
8 ~ 15 | 魔數(shù)低位 | 0xbb |
16 | 數(shù)據(jù)包類型 | 0 - Response, 1 - Request |
17 | 調(diào)用方式 | 僅在第16位被設(shè)為1的情況下有效预伺,0 - 單向調(diào)用订咸,1 - 雙向調(diào)用 |
18 | 事件標(biāo)識(shí) | 0 - 當(dāng)前數(shù)據(jù)包是請(qǐng)求或響應(yīng)包曼尊,1 - 當(dāng)前數(shù)據(jù)包是心跳包 |
19 ~ 23 | 序列化器編號(hào) | 2 - Hessian2Serialization 3 - JavaSerialization 4 - CompactedJavaSerialization 6 - FastJsonSerialization 7 - NativeJavaSerialization 8 - KryoSerialization 9 - FstSerialization |
24 ~ 31 | 狀態(tài) | 20 - OK 30 - CLIENT_TIMEOUT 31 - SERVER_TIMEOUT 40 - BAD_REQUEST 50 - BAD_RESPONSE ...... |
32 ~ 95 | 請(qǐng)求編號(hào) | 共 8 字節(jié)存儲(chǔ) RPC 請(qǐng)求的唯一 id,運(yùn)行時(shí)生成脏嚷,用來(lái)將請(qǐng)求和響應(yīng)做關(guān)聯(lián) |
96 ~ 127 | 消息體長(zhǎng)度 | 運(yùn)行時(shí)計(jì)算 |
在網(wǎng)絡(luò)通信中(基于 TCP)需要解決網(wǎng)絡(luò)粘包/解包的問(wèn)題骆撇,一些常用的方法是用回車、換行父叙、固定長(zhǎng)度和特殊分隔符進(jìn)行處理神郊。Dubbo 就是用魔法數(shù)(0xdabb)來(lái)分割處理粘包問(wèn)題的
一般情況下,服務(wù)消費(fèi)方會(huì)并發(fā)調(diào)用多個(gè)服務(wù)趾唱,每個(gè)用戶線程發(fā)送請(qǐng)求后涌乳,會(huì)調(diào)用不同 DefaultFuture 對(duì)象的 get 方法進(jìn)行等待。 一段時(shí)間后鲸匿,服務(wù)消費(fèi)方的線程池會(huì)收到多個(gè)響應(yīng)對(duì)象爷怀。這個(gè)時(shí)候要考慮一個(gè)問(wèn)題,如何將每個(gè)響應(yīng)對(duì)象傳遞給相應(yīng)的 DefaultFuture 對(duì)象带欢,且不出錯(cuò)
答案是通過(guò) 請(qǐng)求編號(hào)运授。DefaultFuture 被創(chuàng)建時(shí),會(huì)要求傳入一個(gè) Request 對(duì)象乔煞。此時(shí) DefaultFuture 可從 Request 對(duì)象中獲取請(qǐng)求編號(hào)吁朦,并將 <請(qǐng)求編號(hào), DefaultFuture 對(duì)象> 映射關(guān)系存入到靜態(tài) HashMap 中,即 FUTURES渡贾。消費(fèi)者端線程池中的線程收到 Response 對(duì)象后逗宜,會(huì)根據(jù) Response 對(duì)象中的請(qǐng)求編號(hào)到 FUTURES 集合中取出相應(yīng)的 DefaultFuture 對(duì)象,然后再將 Response 對(duì)象設(shè)置到 DefaultFuture 對(duì)象中空骚。最后再喚醒用戶線程纺讲,這樣用戶線程即可從 DefaultFuture 對(duì)象中獲取調(diào)用結(jié)果了,整個(gè)過(guò)程大致如下圖:
3 Dubbo 序列化協(xié)議
Dubbo序列化支持 java囤屹、compactedjava熬甚、nativejava、fastjson肋坚、dubbo乡括、fst、hessian2智厌、kryo诲泌,其中 dubbo 協(xié)議缺省為 hessian2 序列化,rmi 協(xié)議缺省為 java 序列化铣鹏,http 協(xié)議缺省為 json 序列化
以下是 dubbo:2.7.7 擴(kuò)展的序列化方式敷扫,可在 <dubbo:protocol>
標(biāo)簽的 serialization
屬性進(jìn)行配置
fastjson=org.apache.dubbo.common.serialize.fastjson.FastJsonSerialization
fst=org.apache.dubbo.common.serialize.fst.FstSerialization
hessian2=org.apache.dubbo.common.serialize.hessian2.Hessian2Serialization
native-hessian=org.apache.dubbo.serialize.hessian.Hessian2Serialization
java=org.apache.dubbo.common.serialize.java.JavaSerialization
compactedjava=org.apache.dubbo.common.serialize.java.CompactedJavaSerialization
nativejava=org.apache.dubbo.common.serialize.nativejava.NativeJavaSerialization
kryo=org.apache.dubbo.common.serialize.kryo.KryoSerialization
kryo2=org.apache.dubbo.common.serialize.kryo.optimized.KryoSerialization2
avro=org.apache.dubbo.common.serialize.avro.AvroSerialization
protostuff=org.apache.dubbo.common.serialize.protostuff.ProtostuffSerialization
gson=org.apache.dubbo.common.serialize.gson.GsonSerialization
protobuf-json=org.apache.dubbo.common.serialize.protobuf.support.GenericProtobufJsonSerialization
protobuf=org.apache.dubbo.common.serialize.protobuf.support.GenericProtobufSerialization
Dubbo 序列化主要由 Serialization(序列化策略)、DataInput(反序列化诚卸,二進(jìn)制->對(duì)象)呻澜、DataOutput(序列化递礼,對(duì)象->二進(jìn)制流) 來(lái)進(jìn)行數(shù)據(jù)的序列化與反序列化
3.1 hessian 序列化
Hessian 數(shù)據(jù)結(jié)構(gòu)
8 種原始類型 | 1. raw binary data 2. boolean 3. 64-bit millisecond date 4. 64-bit double 5. 32-bit int 6. 64-bit long 7. null 8. UTF8-encoded string |
3 種遞歸類型 | 1. list for lists and arrays 2. map for maps and dictionaries 3. object for objects |
1 中特殊類型 | 1. ref for shared and circular object references. |
hessian 序列化 VS Java 原生序列化
Java 序列化會(huì)把要序列化的對(duì)象類的元數(shù)據(jù)和業(yè)務(wù)數(shù)據(jù)全部序列化為字節(jié)流,而且是把整個(gè)繼承關(guān)系上的東西全部序列化了羹幸。它序列化出來(lái)的字節(jié)流是一個(gè)對(duì)象從結(jié)構(gòu)到內(nèi)容的完整描述脊髓,包含所有的信息,因此效率較低而且字節(jié)流比較大栅受。但是由于序列化了所有內(nèi)容将硝,因此也更可用和可靠
hession 序列化實(shí)現(xiàn)機(jī)制是著重于數(shù)據(jù),附帶簡(jiǎn)單的類型信息的方法屏镊。就像 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)序列化误辑,產(chǎn)生類似 M className propertyName1 I 1 propertyName S stringValue
這樣的流,包含了基本的類型描述和數(shù)據(jù)內(nèi)容
hessian 的序列化在內(nèi)容的序列化上做了一些優(yōu)化歌逢,hessian 將需要序列化的多個(gè)相同的對(duì)象只會(huì)寫入一次巾钉,其他用到該對(duì)象的只使用對(duì)象的引用,而不重新寫入對(duì)象的描述信息和值信息
3.2 protobuf 序列化
Protocol Buffer 是 Google 出品的一種輕量并且高效的結(jié)構(gòu)化數(shù)據(jù)存儲(chǔ)格式秘案,性能比 JSON砰苍、XML 要高很多,原因如下:
- 它使用 proto 編譯器阱高,自動(dòng)進(jìn)行序列化和反序列化赚导,速度非常快赤惊,比 XML 和 JSON 快大概 20~100 倍
- 它的數(shù)據(jù)壓縮效果好辟癌,就是說(shuō)它序列化后的數(shù)據(jù)量體積小。因?yàn)轶w積小荐捻,傳輸起來(lái)帶寬和速度上會(huì)有優(yōu)化,詳見(jiàn)下一節(jié)
- 使用 Varint 數(shù)字表示方法寡夹,減少用來(lái)表示數(shù)字的字節(jié)數(shù)
- 一個(gè)負(fù)數(shù)一般會(huì)被表示為一個(gè)很大的整數(shù)处面,因?yàn)橛?jì)算機(jī)定義負(fù)數(shù)的符號(hào)位為數(shù)字的最高位。因此 Protocol Buffer 定義了 sint32 這種類型菩掏,采用 zigzag 編碼魂角,用無(wú)符號(hào)數(shù)來(lái)表示有符號(hào)數(shù)字,實(shí)現(xiàn)了用較少的 Bytes 來(lái)表示負(fù)數(shù)
- Protocol Buffer 的二進(jìn)制流采用 Key-Value 結(jié)構(gòu)存儲(chǔ)信息智绸,無(wú)需使用分隔符來(lái)分割不同的 Field野揪。對(duì)于可選的 Field访忿,如果消息中不存在該 field,那么在最終的 Message Buffer 中就沒(méi)有該 field斯稳,這個(gè)特性也有助于節(jié)約消息本身的大小
數(shù)據(jù)壓縮方法
Protobuf 序列化后所生成的二進(jìn)制消息非常緊湊海铆,這得益于 Protobuf 采用的非常巧妙的 Encoding 方法
考察消息結(jié)構(gòu)之前,讓我首先要介紹一個(gè)叫做 Varint 的術(shù)語(yǔ)
Varint 是一種緊湊的表示數(shù)字的方法挣惰。它用一個(gè)或多個(gè)字節(jié)來(lái)表示一個(gè)數(shù)字卧斟,值越小的數(shù)字使用越少的字節(jié)數(shù)。這能減少用來(lái)表示數(shù)字的字節(jié)數(shù)
比如對(duì)于 int32 類型的數(shù)字憎茂,一般需要 4 個(gè) Bytes 來(lái)表示珍语。但是采用 Varint,對(duì)于很小的 int32 類型的數(shù)字竖幔,則可以用 1 個(gè) Byte 來(lái)表示板乙。當(dāng)然凡事都有好的也有不好的一面,采用 Varint 表示法拳氢,大的數(shù)字則需要 5 個(gè) Byte 來(lái)表示募逞。從統(tǒng)計(jì)的角度來(lái)說(shuō),一般不會(huì)所有的消息中的數(shù)字都是大數(shù)饿幅,因此大多數(shù)情況下凡辱,采用 Varint 后,可以用更少的字節(jié)數(shù)來(lái)表示數(shù)字信息栗恩。下面就詳細(xì)介紹一下 Varint
Varint 中的每個(gè) Byte 的最高位 bit 有特殊的含義透乾,如果該位為 1,表示后續(xù)的 Byte 也是該數(shù)字的一部分磕秤,如果該位為 0乳乌,則結(jié)束。其他的 7 個(gè) bit 都用來(lái)表示數(shù)字市咆。因此小于 128 的數(shù)字都可以用一個(gè) Byte 表示汉操。大于 128 的數(shù)字,比如 300蒙兰,會(huì)用兩個(gè)字節(jié)來(lái)表示:1010 1100 0000 0010
下圖演示了 Google Protocol Buffer 如何解析兩個(gè) Bytes磷瘤。注意到最終計(jì)算前將兩個(gè) Bytes 的位置相互交換過(guò)一次,這是因?yàn)?Google Protocol Buffer 字節(jié)序采用 little-endian 的方式
消息經(jīng)過(guò)序列化后會(huì)成為一個(gè)二進(jìn)制數(shù)據(jù)流搜变,該流中的數(shù)據(jù)為一系列的 Key-Value 對(duì)采缚。如下圖所示:
采用這種 Key-Pair 結(jié)構(gòu)無(wú)需使用分隔符來(lái)分割不同的 Field。對(duì)于可選的 Field挠他,如果消息中不存在該 field扳抽,那么在最終的 Message Buffer 中就沒(méi)有該 field,這些特性都有助于節(jié)約消息本身的大小
以代碼清單 1 中的消息為例。假設(shè)我們生成如下的一個(gè)消息 Test1:
Test1.id = 10; Test1.str = "hello";
則最終的 Message Buffer 中有兩個(gè) Key-Value 對(duì)贸呢,一個(gè)對(duì)應(yīng)消息中的 id镰烧;另一個(gè)對(duì)應(yīng) str
Key 用來(lái)標(biāo)識(shí)具體的 field,在解包的時(shí)候楞陷,Protocol Buffer 根據(jù) Key 就可以知道相應(yīng)的 Value 應(yīng)該對(duì)應(yīng)于消息中的哪一個(gè) field
Key 的定義如下:
(field_number << 3) | wire_type
可以看到 Key 由兩部分組成怔鳖。第一部分是 field_number,比如消息 Test1 中 field id 的 field_number 為 1猜谚。第二部分為 wire_type败砂。表示 Value 的傳輸類型
Wire Type 可能的類型如下表所示:
Type | Meaning | Used For |
---|---|---|
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64-bit | fixed64, sfixed64, double |
2 | Length-delimi | string, bytes, embedded messages, packed repeated fields |
3 | Start group | Groups (deprecated) |
4 | End group | Groups (deprecated) |
5 | 32-bit | fixed32, sfixed32, float |
在我們的例子當(dāng)中,field id 所采用的數(shù)據(jù)類型為 int32魏铅,因此對(duì)應(yīng)的 wire type 為 0昌犹。細(xì)心的讀者或許會(huì)看到在 Type 0 所能表示的數(shù)據(jù)類型中有 int32 和 sint32 這兩個(gè)非常類似的數(shù)據(jù)類型。Google Protocol Buffer 區(qū)別它們的主要意圖也是為了減少 encoding 后的字節(jié)數(shù)
在計(jì)算機(jī)內(nèi)览芳,一個(gè)負(fù)數(shù)一般會(huì)被表示為一個(gè)很大的整數(shù)斜姥,因?yàn)橛?jì)算機(jī)定義負(fù)數(shù)的符號(hào)位為數(shù)字的最高位。如果采用 Varint 表示一個(gè)負(fù)數(shù)沧竟,那么一定需要 5 個(gè) Bytes(為何是 5 bytes 呢铸敏?)。為此 Google Protocol Buffer 定義了 sint32 這種類型悟泵,采用 zigzag 編碼
Zigzag 編碼用無(wú)符號(hào)數(shù)來(lái)表示有符號(hào)數(shù)字杈笔,正數(shù)和負(fù)數(shù)交錯(cuò),這就是 zigzag 這個(gè)詞的含義了
如圖所示:
使用 zigzag 編碼糕非,絕對(duì)值小的數(shù)字蒙具,無(wú)論正負(fù)都可以采用較少的 Bytes 來(lái)表示
其他的數(shù)據(jù)類型,比如字符串等則采用類似數(shù)據(jù)庫(kù)中的 varchar 的表示方法朽肥,即用一個(gè) varint 表示長(zhǎng)度禁筏,然后將其余部分緊跟在這個(gè)長(zhǎng)度部分之后即可
通過(guò)以上對(duì) protobuf Encoding 方法的介紹,想必您也已經(jīng)發(fā)現(xiàn) protobuf 消息的內(nèi)容小衡招,適于網(wǎng)絡(luò)傳輸篱昔。假如您對(duì)那些有關(guān)技術(shù)細(xì)節(jié)的描述缺乏耐心和興趣,那么下面這個(gè)簡(jiǎn)單而直觀的比較應(yīng)該能給您更加深刻的印象
假設(shè)有一條 helloworld 消息 id=101 str="hello"始腾,用 Protobuf 序列化后的字節(jié)序列為:
08 65 12 06 48 65 6C 6C 6F 77
而如果用 XML州刽,則類似這樣:
31 30 31 3C 2F 69 64 3E 3C 6E 61 6D 65 3E 68 65
6C 6C 6F 3C 2F 6E 61 6D 65 3E 3C 2F 68 65 6C 6C
6F 77 6F 72 6C 64 3E
一共 55 個(gè)字節(jié),這些奇怪的數(shù)字需要稍微解釋一下浪箭,其含義用 ASCII 表示如下:
<helloworld>
<id>101</id>
<name>hello</name>
</helloworld>
4 線程模型
如果事件處理的邏輯能迅速完成穗椅,并且不會(huì)發(fā)起新的 IO 請(qǐng)求,比如只是在內(nèi)存中記個(gè)標(biāo)識(shí)山林,則直接在 IO 線程上處理更快,因?yàn)闇p少了線程池調(diào)度
但如果事件處理邏輯較慢,或者需要發(fā)起新的 IO 請(qǐng)求驼抹,比如需要查詢數(shù)據(jù)庫(kù)桑孩,則必須派發(fā)到線程池,否則 IO 線程阻塞框冀,將導(dǎo)致不能接收其它請(qǐng)求
因此流椒,需要通過(guò)不同的派發(fā)策略和不同的線程池配置的組合來(lái)應(yīng)對(duì)不同的場(chǎng)景:
<dubbo:protocol name="dubbo" dispatcher="all" threadpool="fixed" threads="100" />
Dispatcher
-
all
所有消息都派發(fā)到線程池,包括請(qǐng)求明也,響應(yīng)宣虾,連接事件,斷開(kāi)事件温数,心跳等(默認(rèn)設(shè)置) -
direct
所有消息都不派發(fā)到線程池绣硝,全部在 IO 線程上直接執(zhí)行 -
message
只有請(qǐng)求響應(yīng)消息派發(fā)到線程池,其它連接斷開(kāi)事件撑刺,心跳等消息鹉胖,直接在 IO 線程上執(zhí)行 -
execution
只有請(qǐng)求消息派發(fā)到線程池,不含響應(yīng)够傍,響應(yīng)和其它連接斷開(kāi)事件甫菠,心跳等消息,直接在 IO 線程上執(zhí)行 -
connection
在 IO 線程上冕屯,將連接斷開(kāi)事件放入隊(duì)列寂诱,有序逐個(gè)執(zhí)行,其它消息派發(fā)到線程池
ThreadPool
-
fixed
固定大小線程池安聘,啟動(dòng)時(shí)建立線程痰洒,不關(guān)閉,一直持有(默認(rèn)設(shè)置) -
cached
緩存線程池搞挣,空閑一分鐘自動(dòng)刪除带迟,需要時(shí)重建 -
limited
可伸縮線程池,但池中的線程數(shù)只會(huì)增長(zhǎng)不會(huì)收縮囱桨。只增長(zhǎng)不收縮的目的是為了避免收縮時(shí)突然來(lái)了大流量引起的性能問(wèn)題 -
eager
優(yōu)先創(chuàng)建Worker
線程池仓犬。在任務(wù)數(shù)量大于corePoolSize
但是小于maximumPoolSize
時(shí),優(yōu)先創(chuàng)建Worker
來(lái)處理任務(wù)舍肠。當(dāng)任務(wù)數(shù)量大于maximumPoolSize
時(shí)搀继,將任務(wù)放入阻塞隊(duì)列中。阻塞隊(duì)列充滿時(shí)拋出RejectedExecutionException
翠语。(相比于cached
:cached
在任務(wù)數(shù)量超過(guò)maximumPoolSize
時(shí)直接拋出異常而不是將任務(wù)放入阻塞隊(duì)列)
Dubbo 集群容錯(cuò)
1 集群容錯(cuò)概述
為了避免單點(diǎn)故障叽躯,現(xiàn)在的應(yīng)用通常至少會(huì)部署在兩臺(tái)服務(wù)器上。對(duì)于一些負(fù)載比較高的服務(wù)肌括,會(huì)部署更多的服務(wù)器点骑。這樣,在同一環(huán)境下的服務(wù)提供者數(shù)量會(huì)大于 1。對(duì)于服務(wù)消費(fèi)者來(lái)說(shuō)黑滴,同一環(huán)境下出現(xiàn)了多個(gè)服務(wù)提供者憨募,這時(shí)會(huì)出現(xiàn)一個(gè)問(wèn)題,服務(wù)消費(fèi)者需要決定選擇哪個(gè)服務(wù)提供者進(jìn)行調(diào)用袁辈。另外服務(wù)調(diào)用失敗時(shí)的處理措施也是需要考慮的菜谣,是重試呢,還是拋出異常晚缩,亦或是只打印異常尾膊。為了處理這些問(wèn)題,Dubbo 定義了集群容錯(cuò)接口 Cluster
和 ClusterInvoker
集群 Cluster 用途是將多個(gè)服務(wù)提供者合并為一個(gè) Cluster Invoker荞彼,并將這個(gè) Invoker 暴露給服務(wù)消費(fèi)者冈敛。這樣一來(lái),服務(wù)消費(fèi)者只需通過(guò)這個(gè) Invoker 進(jìn)行遠(yuǎn)程調(diào)用即可卿泽,至于具體調(diào)用哪個(gè)服務(wù)提供者莺债,以及調(diào)用失敗后如何處理等問(wèn)題,現(xiàn)在都交給集群模塊去處理
集群模塊是服務(wù)提供者和服務(wù)消費(fèi)者的中間層签夭,為服務(wù)消費(fèi)者屏蔽了服務(wù)提供者的情況齐邦,這樣服務(wù)消費(fèi)者就可以專心處理遠(yuǎn)程調(diào)用相關(guān)事宜。比如發(fā)請(qǐng)求第租,接受服務(wù)提供者返回的數(shù)據(jù)等措拇。這就是集群的作用
總體來(lái)說(shuō),Cluster 作為一個(gè)集群容錯(cuò)層慎宾,其核心接口為 Cluster
和 ClusterInvoker
丐吓。每一個(gè)不同的容錯(cuò)機(jī)制都繼承 Cluster
接口進(jìn)行不同的實(shí)現(xiàn),每個(gè)實(shí)現(xiàn)類中都需要調(diào)用 join
方法創(chuàng)建一個(gè)對(duì)應(yīng)的 ClusterInvoker
實(shí)現(xiàn)類趟据。然后調(diào)用 ClusterInvoker
的 invoke
方法進(jìn)行具體的調(diào)用流程券犁。以 FailoverCluster
為例,其實(shí)現(xiàn)代碼如下:
public class FailoverCluster implements Cluster {
public static final String NAME = "failover";
public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
return new FailoverClusterInvoker(directory);
}
}
整體過(guò)程如下圖所示:
我們可以看到集群工作過(guò)程可分為兩個(gè)階段:
第一個(gè)階段是在服務(wù)消費(fèi)者初始化期間汹碱,集群 Cluster 實(shí)現(xiàn)類為服務(wù)消費(fèi)者創(chuàng)建 Cluster Invoker 實(shí)例粘衬,即上圖中的 merge 操作。
第二個(gè)階段是在服務(wù)消費(fèi)者進(jìn)行遠(yuǎn)程調(diào)用時(shí)咳促。以 FailoverClusterInvoker
為例:
-
FailoverClusterInvoker
首先會(huì)調(diào)用Directory#list
方法列舉Invoker
列表(可將 Invoker 簡(jiǎn)單理解為服務(wù)提供者)稚新。Directory 的用途是保存 Invoker,可簡(jiǎn)單類比為List<Invoker>
跪腹。其實(shí)現(xiàn)類RegistryDirectory
是一個(gè)動(dòng)態(tài)服務(wù)目錄褂删,可感知注冊(cè)中心配置的變化,它所持有的 Invoker 列表會(huì)隨著注冊(cè)中心內(nèi)容的變化而變化冲茸。每次變化后屯阀,RegistryDirectory 會(huì)動(dòng)態(tài)增刪 Invoker -
FailoverClusterInvoker
調(diào)用Router
的route
方法進(jìn)行路由诵盼,過(guò)濾掉不符合路由規(guī)則的Invoker
-
FailoverClusterInvoker
拿到Directory
返回的Invoker
列表后惜浅,它會(huì)通過(guò)LoadBalance
從Invoker
列表中選擇一個(gè)Invoker
-
FailoverClusterInvoker
將調(diào)用參數(shù)傳給LoadBalance
選擇出的Invoker
實(shí)例的invoke
方法俄讹,進(jìn)行真正的遠(yuǎn)程調(diào)用
2 容錯(cuò)機(jī)制實(shí)現(xiàn)
Dubbo 中的容錯(cuò)機(jī)制有以下幾種忍些,在 dubbo:service
堡僻、dubbo:reference
嫉晶、dubbo:provider
天吓、dubbo:consumer
的 cluster
屬性中配置
機(jī)制 | 機(jī)制簡(jiǎn)介 |
---|---|
Failover | 當(dāng)調(diào)用失敗時(shí)會(huì)重試其他服務(wù)器蘸际。用戶可以通過(guò) retries="2" 設(shè)置重試次數(shù)祭陷。這是 Dubbo 的默認(rèn)容錯(cuò)機(jī)制苍凛,會(huì)做負(fù)載均衡。通常使用在讀操作或冪等操作上兵志,但會(huì)導(dǎo)致接口延遲增大醇蝴,容易加重下游服務(wù)的負(fù)載 |
Failfast | 當(dāng)請(qǐng)求失敗后,快速返回異常結(jié)果想罕,不作任何重試悠栓。會(huì)對(duì)請(qǐng)求做負(fù)載均衡,通常使用在非冪等操作上 |
Failsafe | 請(qǐng)求出現(xiàn)異常時(shí)按价,直接忽略惭适。會(huì)對(duì)請(qǐng)求做負(fù)載均衡,不關(guān)心調(diào)用是否成功楼镐,不拋出異常影響外層調(diào)用癞志。適用于不重要的服務(wù)場(chǎng)景(如日志) |
Failback | 請(qǐng)求失敗后,會(huì)將失敗請(qǐng)求記錄在失敗隊(duì)列中框产,并有一個(gè)定時(shí)線程池定時(shí)重試凄杯,適用于異步或最終一致性的服務(wù)場(chǎng)景,會(huì)做負(fù)載均衡 |
Forking | 同時(shí)調(diào)用多個(gè)相同的服務(wù)秉宿,只要其中一個(gè)返回就立即返回結(jié)果戒突。用戶可以設(shè)置 fork 屬性確定最大并行調(diào)用數(shù)量。通常使用在對(duì)接口實(shí)時(shí)性要求極高的調(diào)用上描睦,但會(huì)浪費(fèi)更多資源 |
Broadcast | 廣播調(diào)用所有服務(wù)膊存,任意一個(gè)節(jié)點(diǎn)失敗則報(bào)錯(cuò),不用負(fù)載均衡 |
Available | 遍歷所有服務(wù)列表酌摇,找到第一個(gè)可用節(jié)點(diǎn)膝舅,直接請(qǐng)求并返回結(jié)果,不做負(fù)載均衡 |
Mergeable | 自動(dòng)將多個(gè)節(jié)點(diǎn)請(qǐng)求得到的結(jié)果進(jìn)行合并 |
Mock |
2.1 Failover 策略
該策略的邏輯如下:
- 校驗(yàn)從
AbstractClusterInvoker
傳入的Invoker
列表是否為空 - 獲取配置參數(shù)窑多,如
retries
- 初始化一些集合和對(duì)象仍稀,用于保存過(guò)程中出現(xiàn)的異常、記錄調(diào)用哪些節(jié)點(diǎn)
- 使用
for
循環(huán)實(shí)現(xiàn)充實(shí)埂息,循環(huán)次數(shù)就是重試次數(shù)技潘。成功則返回遥巴,否則繼續(xù)循環(huán)。如果循環(huán)結(jié)束仍沒(méi)有一個(gè)成功的返回享幽,則將(3)中的記錄拋出- 校驗(yàn)
Invoker
列表是否為空 - 負(fù)載均衡
- 遠(yuǎn)程調(diào)用
- 校驗(yàn)
2.2 Failfast 策略
該策略的邏輯如下:
- 校驗(yàn)從
AbstractClusterInvoker
傳入的Invoker
列表是否為空 - 負(fù)載均衡
- 遠(yuǎn)程調(diào)用铲掐,在
try
中調(diào)用Invoker#invoke
方法,若捕獲到異常則封裝成RpcException
拋出
public Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
this.checkInvokers(invokers, invocation);
Invoker invoker = this.select(loadbalance, invocation, invokers, (List)null);
try {
return invoker.invoke(invocation);
} catch (Throwable var6) {
if (var6 instanceof RpcException && ((RpcException)var6).isBiz()) {
throw (RpcException)var6;
} else {
throw new RpcException...;
}
}
}
2.3 Failsafe 策略
該策略的邏輯如下:
- 校驗(yàn)從
AbstractClusterInvoker
傳入的Invoker
列表是否為空 - 負(fù)載均衡
- 遠(yuǎn)程調(diào)用值桩,在
try
中調(diào)用Invoker#invoke
方法摆霉,若捕獲到異常則直接吞掉,返回一個(gè)結(jié)果集
public Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
try {
this.checkInvokers(invokers, invocation);
Invoker<T> invoker = this.select(loadbalance, invocation, invokers, (List)null);
return invoker.invoke(invocation);
} catch (Throwable var5) {
logger.error("Failsafe ignore exception: " + var5.getMessage(), var5);
return AsyncRpcResult.newDefaultAsyncResult((Object)null, (Throwable)null, invocation);
}
}
2.4 Failback 策略
若調(diào)用失敗奔坟,則定期重試携栋。FailbackClusterInvoker
中定義了一個(gè) ConcurrentHashMap
用來(lái)保存失敗調(diào)用請(qǐng)求。另外定義了一個(gè)定時(shí)線程池咳秉,默認(rèn)每 5s 把所有的失敗調(diào)用拿出來(lái)重試婉支。若調(diào)用成功則將請(qǐng)求從 ConcurrentHashMap
中移除
該策略的邏輯如下:
- 校驗(yàn)從
AbstractClusterInvoker
傳入的Invoker
列表是否為空 - 負(fù)載均衡
- 遠(yuǎn)程調(diào)用,在
try
中調(diào)用Invoker#invoke
方法澜建,若捕獲到異常則將調(diào)用請(qǐng)求保存到ConcurrentHashMap
向挖,并返回一個(gè)空結(jié)果集 - 定時(shí)線程池每 5s 將
ConcurrentHashMap
中的失敗請(qǐng)求進(jìn)行重新請(qǐng)求,請(qǐng)求成功則從ConcurrentHashMap
中移除炕舵。若還是請(qǐng)求失敗則異常也會(huì)被捕獲何之,防止中斷ConcurrentHashMap
后面的重試
2.5 Available 策略
該策略的邏輯如下:
- 遍歷
AbstractClusterInvoker
傳入的Invoker
列表,找到第一個(gè)可用的服務(wù)直接調(diào)用并返回 - 如果遍歷后沒(méi)有找到可用的
Invoker
咽筋,則拋出異常
2.6 Broadcast 策略
該策略的邏輯如下:
- 校驗(yàn)從
AbstractClusterInvoker
傳入的Invoker
列表是否為空 - 初始化一些集合和對(duì)象帝美,用于保存過(guò)程中出現(xiàn)的異常和結(jié)果信息
- 循環(huán)遍歷
Invoker
列表,直接 RPC 調(diào)用晤硕。任何一個(gè)節(jié)點(diǎn)出錯(cuò)不中整個(gè)廣播過(guò)程悼潭,會(huì)先記錄異常,在最后廣播完成后再拋出舞箍。最后一個(gè)節(jié)點(diǎn)的異常會(huì)覆蓋前面節(jié)點(diǎn)的異常信息
2.7 Forking 策略
該策略的邏輯如下:
校驗(yàn)從
AbstractClusterInvoker
傳入的Invoker
列表是否為空舰褪;初始化一個(gè)Invoker
集合,用于保存真正要調(diào)用的Invoker
列表疏橄;從 URL 中獲取最大并行數(shù)占拍、超時(shí)時(shí)間-
獲取最終要調(diào)用的
Invoker
列表。假設(shè)用戶設(shè)置的最大并行數(shù)是n
捎迫,實(shí)際可調(diào)用最大服務(wù)數(shù)為v
晃酒。若n < 0 or n > v
,說(shuō)明可用服務(wù)少于用戶設(shè)置窄绒,則最終調(diào)用的服務(wù)只能有v
個(gè)贝次;若n < v
,則循環(huán)調(diào)用n
次負(fù)載均衡方法彰导,獲取Invoker
加入Invoker
列表中注意:
Invoker
加入Invoker
列表中會(huì)進(jìn)行去重操作 初始化一個(gè)阻塞隊(duì)列和一個(gè)異常計(jì)數(shù)器
-
執(zhí)行調(diào)用蛔翅。循環(huán)使用線程池并行調(diào)用敲茄,調(diào)用成功則將成功結(jié)果放入阻塞隊(duì)列中;調(diào)用失敗山析,則異常計(jì)數(shù)器 + 1堰燎。若所有線程的調(diào)動(dòng)都失敗了,即異常計(jì)數(shù) ≥ 可調(diào)用
Invoker
數(shù)時(shí)笋轨,講異常信息放入阻塞隊(duì)列并行調(diào)用如何保證個(gè)別調(diào)用的失敗不返回異常信息秆剪,只有全部調(diào)用失敗財(cái)才返回異常信息呢?答案在于只有當(dāng)異常計(jì)數(shù) ≥ 可調(diào)用 Invoker 數(shù)時(shí)爵政,才會(huì)將異常信息放入阻塞隊(duì)列
-
主線程同步等待結(jié)果鸟款。主線程使用阻塞隊(duì)列的
poll(timeout)
方法,同步等待阻塞隊(duì)列中的一個(gè)結(jié)果茂卦,若是正常結(jié)果則返回,否則拋出異常從這點(diǎn)可知组哩,F(xiàn)orking 的超時(shí)是通過(guò)在阻塞隊(duì)列的
poll
方法中傳入超時(shí)時(shí)間實(shí)現(xiàn)的
3 Directory 實(shí)現(xiàn)
todo
4 路由機(jī)制實(shí)現(xiàn)
todo
5 負(fù)載均衡實(shí)現(xiàn)
Dubbo 提供了4種負(fù)載均衡實(shí)現(xiàn)等龙,分別是:
- 基于權(quán)重隨機(jī)算法的
RandomLoadBalance
- 基于最少活躍調(diào)用數(shù)算法的
LeastActiveLoadBalance
- 基于 hash 一致性的
ConsistentHashLoadBalance
- 基于加權(quán)輪詢算法的
RoundRobinLoadBalance
5.1 RandomLoadBalance
RandomLoadBalance
是加權(quán)隨機(jī)算法的具體實(shí)現(xiàn),它的算法思想很簡(jiǎn)單伶贰。假設(shè)我們有一組服務(wù)器 servers = [A, B, C]蛛砰,他們對(duì)應(yīng)的權(quán)重為 weights = [5, 3, 2],權(quán)重總和為10∈蜓茫現(xiàn)在把這些權(quán)重值平鋪在一維坐標(biāo)值上泥畅,[0, 5) 區(qū)間屬于服務(wù)器 A,[5, 8) 區(qū)間屬于服務(wù)器 B琅翻,[8, 10) 區(qū)間屬于服務(wù)器 C位仁。接下來(lái)通過(guò)隨機(jī)數(shù)生成器生成一個(gè)范圍在 [0, 10) 之間的隨機(jī)數(shù),然后計(jì)算這個(gè)隨機(jī)數(shù)會(huì)落到哪個(gè)區(qū)間上方椎。比如數(shù)字3會(huì)落到服務(wù)器 A 對(duì)應(yīng)的區(qū)間上聂抢,此時(shí)返回服務(wù)器 A 即可。權(quán)重越大的機(jī)器棠众,在坐標(biāo)軸上對(duì)應(yīng)的區(qū)間范圍就越大琳疏,因此隨機(jī)數(shù)生成器生成的數(shù)字就會(huì)有更大的概率落到此區(qū)間內(nèi)。只要隨機(jī)數(shù)生成器產(chǎn)生的隨機(jī)數(shù)分布性很好闸拿,在經(jīng)過(guò)多次選擇后空盼,每個(gè)服務(wù)器被選中的次數(shù)比例接近其權(quán)重比例。
生成隨機(jī)數(shù)使用了
ThreadLocalRandom
新荤,它的性能比Random
好一點(diǎn)揽趾。因?yàn)?Random
生成隨機(jī)數(shù)時(shí),為了保證線程安全性苛骨,使用了CAS
操作保證每次只有一個(gè)線程可以獲取并更新 seed但骨。獲取失敗的線程將自旋重試励七,因此多線程下會(huì)因競(jìng)爭(zhēng)同一個(gè) seed 導(dǎo)致性能下降
ThreadLocalRandom
則為每個(gè)線程維護(hù)了一個(gè) seed,這樣就可以有效避免 seed 競(jìng)爭(zhēng)
5.2 LeastActiveLoadBalance
LeastActiveLoadBalance
翻譯過(guò)來(lái)是<font color='red'>最小活躍數(shù)負(fù)載均衡 </font>奔缠÷犹В活躍調(diào)用數(shù)越小,表明該服務(wù)提供者效率越高校哎,單位時(shí)間內(nèi)可處理更多的請(qǐng)求两波。此時(shí)應(yīng)優(yōu)先將請(qǐng)求分配給該服務(wù)提供者。在具體實(shí)現(xiàn)中闷哆,每個(gè)服務(wù)提供者對(duì)應(yīng)一個(gè)活躍數(shù) active腰奋。初始情況下,所有服務(wù)提供者活躍數(shù)均為0抱怔。每收到一個(gè)請(qǐng)求劣坊,活躍數(shù)加1,完成請(qǐng)求后則將活躍數(shù)減1屈留。在服務(wù)運(yùn)行一段時(shí)間后局冰,性能好的服務(wù)提供者處理請(qǐng)求的速度更快,因此活躍數(shù)下降的也越快灌危,此時(shí)這樣的服務(wù)提供者能夠優(yōu)先獲取到新的服務(wù)請(qǐng)求康二、這就是最小活躍數(shù)負(fù)載均衡算法的基本思想
5.3 ConsistentHashLoadBalance
一致性 hash 算法由麻省理工學(xué)院的 Karger 及其合作者于1997年提出的,算法提出之初是用于大規(guī)模緩存系統(tǒng)的負(fù)載均衡勇蝙。它的工作過(guò)程是這樣的沫勿,首先根據(jù) ip 或者其他的信息為緩存節(jié)點(diǎn)生成一個(gè) hash,并將這個(gè) hash 投射到 [0, 232 - 1] 的圓環(huán)上味混。當(dāng)有查詢或?qū)懭胝?qǐng)求時(shí)产雹,則為緩存項(xiàng)的 key 生成一個(gè) hash 值。然后查找第一個(gè)大于或等于該 hash 值的緩存節(jié)點(diǎn)翁锡,并到這個(gè)節(jié)點(diǎn)中查詢或?qū)懭刖彺骓?xiàng)洽故。如果當(dāng)前節(jié)點(diǎn)掛了,則在下一次查詢或?qū)懭刖彺鏁r(shí)盗誊,為緩存項(xiàng)查找另一個(gè)大于其 hash 值的緩存節(jié)點(diǎn)即可时甚。
比如下面綠色點(diǎn)對(duì)應(yīng)的緩存項(xiàng)將會(huì)被存儲(chǔ)到 cache-2 節(jié)點(diǎn)中。由于 cache-3 掛了哈踱,原本應(yīng)該存到該節(jié)點(diǎn)中的緩存項(xiàng)最終會(huì)存儲(chǔ)到 cache-4 節(jié)點(diǎn)中
下面來(lái)看看一致性 hash 在 Dubbo 中的應(yīng)用荒适。我們把上圖的緩存節(jié)點(diǎn)替換成 Dubbo 的服務(wù)提供者,于是得到了下圖:
這里相同顏色的節(jié)點(diǎn)均屬于同一個(gè)服務(wù)提供者开镣,比如 Invoker1-1刀诬,Invoker1-2,……, Invoker1-160邪财。這樣做的目的是通過(guò)引入虛擬節(jié)點(diǎn)陕壹,讓 Invoker 在圓環(huán)上分散開(kāi)來(lái)质欲,避免數(shù)據(jù)傾斜問(wèn)題。所謂數(shù)據(jù)傾斜是指糠馆,由于節(jié)點(diǎn)不夠分散嘶伟,導(dǎo)致大量請(qǐng)求落到了同一個(gè)節(jié)點(diǎn)上,而其他節(jié)點(diǎn)只會(huì)接收到了少量請(qǐng)求的情況又碌。比如:
如上九昧,由于 Invoker-1 和 Invoker-2 在圓環(huán)上分布不均,導(dǎo)致系統(tǒng)中75%的請(qǐng)求都會(huì)落到 Invoker-1 上毕匀,只有 25% 的請(qǐng)求會(huì)落到 Invoker-2 上铸鹰。解決這個(gè)問(wèn)題辦法是引入虛擬節(jié)點(diǎn),通過(guò)虛擬節(jié)點(diǎn)均衡各個(gè)節(jié)點(diǎn)的請(qǐng)求量
5.4 RoundRobinLoadBalance
我們來(lái)看一下 Dubbo 中加權(quán)輪詢負(fù)載均衡的實(shí)現(xiàn) RoundRobinLoadBalance
皂岔。在詳細(xì)分析源碼前蹋笼,我們先來(lái)了解一下什么是加權(quán)輪詢。這里從最簡(jiǎn)單的輪詢開(kāi)始講起
所謂輪詢是指將請(qǐng)求輪流分配給每臺(tái)服務(wù)器躁垛。舉個(gè)例子剖毯,我們有三臺(tái)服務(wù)器 A、B缤苫、C。我們將第一個(gè)請(qǐng)求分配給服務(wù)器 A墅拭,第二個(gè)請(qǐng)求分配給服務(wù)器 B活玲,第三個(gè)請(qǐng)求分配給服務(wù)器 C,第四個(gè)請(qǐng)求再次分配給服務(wù)器 A谍婉。這個(gè)過(guò)程就叫做輪詢舒憾。輪詢是一種無(wú)狀態(tài)負(fù)載均衡算法,實(shí)現(xiàn)簡(jiǎn)單穗熬,適用于每臺(tái)服務(wù)器性能相近的場(chǎng)景下镀迂。
現(xiàn)實(shí)情況下,我們并不能保證每臺(tái)服務(wù)器性能均相近唤蔗。如果我們將等量的請(qǐng)求分配給性能較差的服務(wù)器探遵,這顯然是不合理的。因此妓柜,這個(gè)時(shí)候我們需要對(duì)輪詢過(guò)程進(jìn)行加權(quán)箱季,以調(diào)控每臺(tái)服務(wù)器的負(fù)載。經(jīng)過(guò)加權(quán)后棍掐,每臺(tái)服務(wù)器能夠得到的請(qǐng)求數(shù)比例藏雏,接近或等于他們的權(quán)重比。比如服務(wù)器 A作煌、B掘殴、C 權(quán)重比為 5:2:1赚瘦。那么在8次請(qǐng)求中,服務(wù)器 A 將收到其中的5次請(qǐng)求奏寨,服務(wù)器 B 會(huì)收到其中的2次請(qǐng)求起意,服務(wù)器 C 則收到其中的1次請(qǐng)求
涉及到平滑輪詢算法,有興趣研究一下
Dubbo 擴(kuò)展點(diǎn)加載機(jī)制
1 Java SPI
SPI 全稱 service Provider Interface
Java SPI 使用了策略模式服爷,一個(gè)接口多種實(shí)現(xiàn)杜恰。具體步驟如下:
- 定義一個(gè)接口和對(duì)應(yīng)的方法
- 編寫接口的實(shí)現(xiàn)類
- 在 META-INF/services/ 目錄下創(chuàng)建一個(gè)以接口全路徑名命名的文件,如 com.test.interface.Service
- 在文件中寫入具體實(shí)現(xiàn)類的全路徑名仍源,若有多個(gè)則用分行符分離心褐,如 com.test.interface.impl.ServiceImpl
- 在代碼中通過(guò)
ServiceLoader
來(lái)加載具體的實(shí)現(xiàn)類
其缺點(diǎn)是:
- JDK 自帶的 SPI 會(huì)一次性實(shí)例化所有擴(kuò)展點(diǎn)實(shí)現(xiàn),如果擴(kuò)展點(diǎn)不使用笼踩,那么會(huì)浪費(fèi)資源
- 在擴(kuò)展點(diǎn)加載失敗的情況下逗爹,JDK 擴(kuò)展點(diǎn)加載機(jī)制無(wú)法提供擴(kuò)展點(diǎn)加載失敗的真正原因
- JDK 自帶 SPI 不支持 IOC 和 AOP 的功能
2 Dubbo SPI
相比 Java SPI
,Dubbo SPI
做了一些改進(jìn)和優(yōu)化:
- 相對(duì)于 Java SPI 一次性加載所有實(shí)現(xiàn)嚎于,Dubbo SPI 是按需加載掘而,只加載配置文件中的類,并分成不同的種類緩存在內(nèi)存中于购,不會(huì)立即初始化袍睡,在性能上有更好的表現(xiàn)
- 更為詳細(xì)的擴(kuò)展加載失敗信息
- 增加了對(duì)擴(kuò)展 IOC 和 AOP的支持。一個(gè)擴(kuò)展類可以直接 setter 注入其他擴(kuò)展
Dubbo SPI 還兼容了 Java SPI 的配置路徑和內(nèi)容配置方式肋僧。在Dubbo啟動(dòng)的時(shí)候斑胜,會(huì)默認(rèn)掃描這三個(gè)目錄下的配置文件:META-INF/services
、META-INF/dubbo/
嫌吠、META-INF/dubbo/internal
Dubbo SPI 配置規(guī)范如下表:
規(guī)范名 | 規(guī)范說(shuō)明 |
---|---|
SPI配置文件路徑 | META-INF/services止潘、META-INF/dubbo/、META-INF/dubbo/internal |
SPI配置文件名稱 | 全路徑類名 |
文件內(nèi)容格式 | key=value辫诅,多個(gè)用換行符分隔 |
2.1 分類與緩存
Dubbo SPI 可分為 Class 緩存 和 實(shí)例緩存凭戴。這兩種緩存又根據(jù)擴(kuò)展類的分類分為 普通擴(kuò)展類、包裝擴(kuò)展類 和 自適應(yīng)擴(kuò)展類
- Class 緩存:Dubbo SPI 獲取擴(kuò)展類時(shí)炕矮,先從緩存中讀取么夫,若緩存中不存在則加載配置文件,根據(jù)配置將 Class 緩存到內(nèi)存中肤视,并不會(huì)全部初始化
- 實(shí)例緩存:基于性能考慮魏割,Dubbo SPI 也將 Class 初始化后的實(shí)例進(jìn)行緩存。每次獲取實(shí)例會(huì)先在緩存中查找钢颂,若緩存中不存在則重新加載并進(jìn)行緩存钞它。這也是 Dubbo SPI 性能優(yōu)于 Java SPI 的原因,即 Dubbo SPI 緩存的 Class 是按需實(shí)例化的
2.2 擴(kuò)展點(diǎn)注解
@SPI
@Adaptive
3 ExtensionLoader 的工作原理
ExtensionLoader
的邏輯入口可以分為 getExtension
、getAdptiveExtension
遭垛、getActivateExtension
三個(gè)尼桶,分別是獲取普通擴(kuò)展類、獲取自適應(yīng)擴(kuò)展類锯仪、獲取自動(dòng)激活的擴(kuò)展類泵督。總體邏輯都是從調(diào)用這三個(gè)方法開(kāi)始的庶喜,每個(gè)方法可能會(huì)有不同的重載方法小腊,根據(jù)不同的傳入?yún)?shù)進(jìn)行調(diào)整:
3.1 getExtension
3.2 getAdaptiveExtension
在 getAdaptiveExtension
方法中,會(huì)為擴(kuò)展點(diǎn)接口自動(dòng)生成實(shí)現(xiàn)類字符串久窟,實(shí)現(xiàn)類主要包含以下邏輯:
- 為接口中每個(gè)有
@Adaptive
注解的方法生成默認(rèn)實(shí)現(xiàn)(沒(méi)有注解的方法則生成空實(shí)現(xiàn)) - 每個(gè)默認(rèn)實(shí)現(xiàn)都會(huì)從 URL 中提取
Adaptive
參數(shù)值秩冈,并以此為依據(jù)動(dòng)態(tài)加載擴(kuò)展點(diǎn) - 使用不同的編譯器,把實(shí)現(xiàn)類字符串編譯為自適應(yīng)類并返回斥扛,詳見(jiàn)第 4 節(jié)
4 擴(kuò)展點(diǎn)動(dòng)態(tài)編譯實(shí)現(xiàn)
Dubbo SPI 使用了生成字符串代碼再編譯為 Class 的方式入问,支持字符串代碼編譯的有 JDK 編譯器 和 Javassist 編譯器
Dubbo 中有三種代碼編譯器,分別是 JDK 編譯器
稀颁、Javassist 編譯器
和 AdaptiveCompiler 編譯器
芬失。這幾種編譯器都實(shí)現(xiàn)了 Compiler 接口,編譯器類之間的關(guān)系如下圖:
Compiler
接口上含有一個(gè) @SPI
注解匾灶,注解的默認(rèn)值是 @SPI("javassist")
棱烂,很明顯,Javassist 編譯器將作為默認(rèn)編譯器阶女。如果用戶想改變默認(rèn)編譯器颊糜,則可以通過(guò) <dubbo:application compiler="jdk" />
標(biāo)簽進(jìn)行設(shè)置
AdaptiveCompiler
上面有 @Adaptive
注解,說(shuō)明 AdaptiveCompiler
會(huì)固定為默認(rèn)實(shí)現(xiàn)张肾,這個(gè) Compiler 的主要作用和 AdaptiveExtensionFactory
相似芭析,就是為了管理其他 compiler
@Adaptive
public class AdaptiveCompiler implements Compiler {
...
public static void setDefaultCompiler(String compiler) {
DEFAULT_COMPILER = compiler;
}
public Class<?> compile(String code, ClassLoader classLoader) {
...
return compiler.compile(code, classLoader);
}
}
AdaptiveCompiler#setDefaultCompiler
方法會(huì)在 ApplicationConfig
中被調(diào)用锚扎,也就是 Dubbo 在啟動(dòng)時(shí)吞瞪,會(huì)解析 <dubbo:application compiper="jdk" />
標(biāo)簽,獲取設(shè)置的值驾孔,初始化對(duì)應(yīng)的編譯器芍秆。如果沒(méi)有標(biāo)簽設(shè)置,則使用 @SPI("javassist")
中的設(shè)置翠勉,即 javassistCompiler
4.1 Javassist 動(dòng)態(tài)代碼編譯
Javassist 動(dòng)態(tài)代碼編譯示例:
@Test
public void test_javassist() throws CannotCompileException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
//初始化Javassist類池
ClassPool classPool = ClassPool.getDefault();
//創(chuàng)建一個(gè)Hello World類
CtClass ctClass = classPool.makeClass("Hello World");
//添加一個(gè)test方法妖啥,會(huì)打印Hello World,直接傳入方法的字符串
CtMethod method = CtMethod.make("" +
"public static void test(){" +
"System.out.println(\"Hello World\");" +
"}", ctClass);
ctClass.addMethod(method);
//生成類
Class aClass = ctClass.toClass();
//通過(guò)反射調(diào)用這個(gè)類實(shí)例
Object object = aClass.newInstance();
Method m = aClass.getDeclaredMethod("test", null);
m.invoke(object, null);
}
由于之前已經(jīng)生成了代碼字符串对碌,因此在 JavassistCompiler 中荆虱,就是不斷通過(guò)正則表達(dá)式匹配不同部位的代碼,然后調(diào)用 Javassist 庫(kù)中的 API 生成不同部位的代碼,最后得到一個(gè)完整的 Class 對(duì)象
具體步驟如下:
- 初始化 Javassist怀读,設(shè)置默認(rèn)參數(shù)诉位,如設(shè)置當(dāng)前的classpath
- 通過(guò)正則匹配出所有 import 的包,并使用 Javassist 添加 import
- 通過(guò)正則匹配出所有 extends 的包菜枷,創(chuàng)建 Class 對(duì)象苍糠,并使用 Javassist 添加 extends
- 通過(guò)正則匹配出所有 implements 包,并使用 Javassist 添加 implements
- 通過(guò)正則匹配出類里面所有內(nèi)容啤誊,即得到 {} 中的內(nèi)容岳瞭,再通過(guò)正則匹配出所有方法,并使用 Javassist 添加類方法
- 生成 Class 對(duì)象
4.2 JDK 動(dòng)態(tài)代碼編譯
JDKCompiler 是 Dubbo 編譯器的另一種實(shí)現(xiàn)蚊锹,使用了 JDK 自帶的編譯器瞳筏,原生JDK編譯器包位于 java.tools
下。主要使用了三個(gè)東西:JavaFileObject 接口
枫耳、ForwardingJavaFileManager 接口
乏矾、JavaCompiler.CompilationTask 方法
整個(gè)動(dòng)態(tài)編譯過(guò)程可以簡(jiǎn)單地總結(jié)為:首先初始化一個(gè) JavaFileObject 對(duì)象,并把字符串作為參數(shù)傳入構(gòu)造方法迁杨,然后調(diào)用 JavaCompiler.CompilationTask 方法編譯出具體的類钻心。JavaFileManager 負(fù)責(zé)管理類文件輸入/輸出的位置
-
JavaFileObject接口:
字符串代碼會(huì)被包裝成一個(gè)文件對(duì)象,并提供獲取二進(jìn)制流的接口铅协。Dubbo框架中的JavaFileObjectImpl類可以看做該接口的一種擴(kuò)展實(shí)現(xiàn)捷沸,構(gòu)造方法中需要傳入生成好的字符串代碼,此文件對(duì)象的輸入和輸入都是ByteArray流狐史。 -
JavaFileManager接口:
主要管理文件的讀取和輸出位置痒给。JDK中沒(méi)有可以直接使用的實(shí)現(xiàn)類,唯一的實(shí)現(xiàn)鱷梨ForwardingJavaFileManager構(gòu)造器又是protect類型骏全。因此Dubbo中定制化實(shí)現(xiàn)了一個(gè)JavaFileManagerImpl類苍柏,并通過(guò)一個(gè)自定義類加載器ClassLoaderImpl完成資源加載。 -
JavaCompiler.CompilationTask:
把JavaFileObject對(duì)象編譯層具體的類
Dubbo 配置覆蓋優(yōu)先級(jí)
以 timeout 為例姜贡,下圖顯示了配置的查找順序试吁,其它 retries, loadbalance, actives 等類似:
- 方法級(jí)優(yōu)先,接口級(jí)次之楼咳,全局配置再次之
- 如果級(jí)別一樣熄捍,則消費(fèi)方優(yōu)先,提供方次之
其中母怜,服務(wù)提供方配置余耽,通過(guò) URL 經(jīng)由注冊(cè)中心傳遞給消費(fèi)方