一. Dubbo誕生背景

隨著互聯網的發展和網站規模的擴大,系統架構也從單點的垂直結構往分散式服務架構演進,如下圖所示:

  • 單一應用架構:一個應用部署所有功能,此時簡化CRUD的ORM框架是關鍵
  • 垂直應用架構:應用拆分為不相干的幾個應用,前後端分離,此時用於加速前端頁面開發的Web MVC框架是關鍵
  • 分散式服務架構:抽取各垂直應用的核心業務作為獨立服務,形成穩定的服務中心,此時用於提高業務復用及整合的分散式服務框架(RPC)是關鍵
  • 流動計算架構:當服務越來越多,容量的評估、小服務資源的浪費等問題逐漸顯現,此時用於提高機器利用率的實時資源調度和治理中心(SOA)是關鍵

當服務比較少時,可以通過 RMI 或 Hession 等工具,簡單的暴露和引用遠程服務,通過配置服務的URL地址來調用,通過F5等硬體負載均衡

當服務越來越多時,服務配置URL變的困難,F5硬體負載均衡的單點壓力越來越大。此時需要服務註冊中心,動態的註冊和發現服務,使服務的位置透明。服務調用實現軟負載均衡和Failover,降低對F5硬體負載均衡器的依賴

當服務間關係越來越複雜時,此時需要自動畫出服務間的依賴關係圖,來幫助架構師理清服務關係

當服務調用量越來越大時,服務需要多少台機器支撐,服務容量的問題就暴露出來了,此時需要統計服務每天的調用量、響應時間等性能指標作為容量規劃的參考。其次,還可以動態調整權重,將某台機器權重一直加大,直到響應時間到閥值,按照此時的訪問量反推服務的總容量

以上是Dubbo的基本需求,如下圖所示:

二. 整體架構

Dubbo的整體架構設計如圖所示:

Dubbo框架一共分10層,各層單向依賴。最上面的 Service 和 Config 為API,其他均為 SPI。左邊淡藍色的為 consumer 使用的介面,右邊淡綠色的為 provider 使用的介面,中間的為雙方都用到的介面。

黑色箭頭代表層之間的依賴關係;藍色虛線為初始化過程,即啟動時組裝鏈;紅色實線為方法調用過程;紫線為繼承關係。線上的文字為調用的方法。

1、介面服務層(Service):該層與業務邏輯相關,根據 provider 和 consumer 的業務設計對應的介面和實現

2、配置層(Config):對外配置介面,以 ServiceConfig 和 ReferenceConfig 為中心

3、服務代理層(Proxy):服務介面透明代理,生成服務的客戶端 Stub 和 服務端的 Skeleton,以 ServiceProxy 為中心,擴展介面為 ProxyFactory

4、服務註冊層(Registry):封裝服務地址的註冊和發現,以服務 URL 為中心,擴展介面為 RegistryFactory、Registry、RegistryService

5、路由層(Cluster):封裝多個提供者的路由和負載均衡,並橋接註冊中心,以Invoker 為中心,擴展介面為 Cluster、Directory、Router和LoadBlancce

6、監控層(Monitor):RPC調用次數和調用時間監控,以 Statistics 為中心,擴展介面為 MonitorFactory、Monitor和MonitorService

7、遠程調用層(Protocal):封裝 RPC 調用,以 Invocation 和 Result 為中心,擴展介面為 Protocal、Invoker和Exporter

8、信息交換層(Exchange):封裝請求響應模式,同步轉非同步。以 Request 和 Response 為中心,擴展介面為 Exchanger、ExchangeChannel、ExchangeClient和ExchangeServer

9、網路傳輸層(Transport):抽象 mina 和 netty 為統一介面,以 Message 為中心,擴展介面為Channel、Transporter、Client、Server和Codec

10、數據序列化層(Serialize):可復用的一些工具,擴展介面為Serialization、 ObjectInput、ObjectOutput和ThreadPool


各層關係說明:

  • Portocol 是核心層,也就是只要有 Protocol + Invoker + Exporter 就可以完成非透明的 RPC 調用,然後在 Invoker 的主過程上 Filter 攔截點
  • Cluster 是外圍概念,目的是將多個 Invoker 偽裝為一個 Invoker,這樣其它人只要關注 Protocol 層 Invoker 即可。只有一個 provider 時,是不需要 Cluster 的
  • Proxy 層封裝了所有介面的透明化代理,而在其它層都以 Invoker 為中心,只有到了暴露給用戶使用時,才用 Proxy 將 Invoker 轉成介面,或將介面實現轉成 Invoker,看起來像調本地服務一樣調遠程服務
  • Remoting 內部再劃為 Transport 傳輸層和 Exchange 信息交換層:Transport 層只負責單向消息傳輸,是對 Mina, Netty, Grizzly 的抽象;而 Exchange 層是在傳輸層之上封裝了 Request-Response 語義

Dubbo核心領域模型:

  • Protocol 是服務域,它是 Invoker 暴露和引用的主功能入口,它負責 Invoker 的生命周期管理
  • Invoker 是實體域,它是 Dubbo 的核心模型,其它模型都向它靠擾,或轉換成它。它代表一個可執行體,可向它發起 invoke 調用,它有可能是一個本地的實現,也可能是一個遠程的實現,也可能一個集群實現
  • Invocation 是會話域,它持有調用過程中的變數,比如方法名,參數等

Dubbo主要包括以下幾個節點:

  • Provider:暴露服務的服務提供方
  • Consumer:調用遠程服務的服務消費方
  • Registry:服務註冊和發現的註冊中心
  • Monitor:統計服務的調用次數和調用時間的監控中心
  • Container:服務運行容器

Consumer, Provider, Registry, Monitor代表邏輯部署節點。圖中只包含 RPC 層,不包含 Remoting層,Remoting整體隱藏在 Protocol 中。

藍色方框代表業務有交互,綠色方框代表只對Dubbo內部交互。藍色虛線為初始化時調用,紅色虛線為運行時非同步調用,紅色實線為運行時同步調用


0、服務在容器中啟動,載入,運行Provider

1、Provider在啟動時,向Registry註冊自己提供的服務

2、Consumer在啟動時,想Registry訂閱自己所需的服務

3、Registry給Consumer返回Provider的地址列表,如果Provider地址有變更(上線/下線機器),Registry將基於長連接推動變更數據給Consumer

4、Consumer從Provider地址列表中,基於軟負載均衡演算法,選一台進行調用,如果失敗,重試另一台調用

5、Consumer和Provider,在內存中累計調用次數和時間,定時每分鐘一次將統計數據發送到Monitor

將上面的服務調用流程展開,如下圖所示:

藍色虛線為初始化過程,即啟動時組裝鏈;紅色實線為方法調用過程,即運行時調用鏈;紫色實線為繼承


三、實現細節

Invoker 是 Dubbo 領域模型中非常重要的一個概念,很多設計思路都是向它靠攏,這就使得 Invoker 滲透在整個實現代碼里。下面用一個精簡的圖來說明最重要的兩種 Invoker:服務提供 Invoker 和服務消費 Invoker:

① 定義服務介面:

② 服務提供者代碼:

ServiceConfig 類拿到對外提供服務的實際類 ref(如:DemoServiceImpl)通過 ProxyFactory.getInvoker 方法使用 ref 生成一個 AbstractProxyInvoker 實例,然後 通過 Protocol.export 方法新生成一個 Exporter 實例

當網路通訊層收到一個請求後,會找到對應的 Exporter 實例,並調用它所對應的 AbstractProxyInvoker 實例,從而真正調用了服務提供者的代碼

③ 服務消費者代碼:

首先通過 ReferenceConfig.init 方法調用 Protocal.refer 方法生成 Invoker 實例,接下來通過 ProxyFactory.getProxy 方法將 Invoker 轉換為客戶端需要的介面(如:DemoService)

DemoService 就是 consumer 端的 proxy,用戶代碼通過這個 proxy 調用其對應的 Invoker,通過 Invoker 實現真正的遠程調用


四. 功能特性

1. 配置

Dubbo可以採用全Spring的配置方式,基於Spring的Schema擴展進行載入,接入對業務透明,無API侵入。配置項可參考:schema 配置參考手冊

除了Spring配置,也可以使用API配置、屬性配置和註解配置方式。

配置之間的關係,如下圖所示:

provider side:

  • <dubbo:protocol/>:協議配置。用於配置提供服務的協議信息,協議由provider指定,consumer被動接受
  • <dubbo:service/>: 服務配置。暴露一個service,定義service的元信息,一個service可以用多個協議暴露,也可以註冊到多個註冊中心
  • <dubbo:provider/>:提供方配置【可選】。當 ProtocolConfig 和 ServiceConfig 某屬性沒有配置時,採用此預設值

consumer side:

  • <dubbo:reference/>:引用配置。用於創建一個遠程服務代理,一個引用可以指向多個註冊中心
  • <dubbo:consumer/>:消費方配置【可選】。當 ReferenceConfig 某屬性沒有配置時,採用此預設值

application shared:

  • <dubbo:application/>:應用配置。配置應用信息,包括provider和consumer
  • <dubbo:registry/>:註冊中心配置。配置連接註冊中心相關信息
  • <dubbo:monitor/>:監控中心配置【可選】。配置連接監控中心相關信息

sub-config:

  • <dubbo:method/>:方法配置。用於 ServiceConfig 和 ReferenceConfig 指定方法級的配置信息
  • <dubbo:argument/>:參數配置。用於指定方法參數配置

2. 集群容錯

服務調用時的過程如下圖:

Invoker:是Provider的一個可調用Service的抽象,封裝了Provider地址和Service介面信息

Directory:代表多個Invoker,可將它看為List<Invoker>,它的值是動態變化的,比如註冊中心推送變更

Cluster:將Directory的多個Invoker偽裝為一個Invoker,對上層透明。偽裝過程中包括容錯邏輯,例如:一個Invoker調用失敗後重試另一個Invoker

Router:從多個Invoker中按路由規則選出子集,例如:讀寫分離、應用隔離等

LoadBlance:從多個Invoker中選出具體的一個Invoker用於本次調用,選的過程包括負載均衡演算法,調用失敗後需要重選

當Cluster集群調用失敗時,Dubbo提供了多種容錯方案:

  • Failover【默認】:失敗時自動切換,重試其它伺服器。通常用於讀操作,可通過 retries="2" 來設置重試次數(不含第一次)
  • Failfast:快速失敗,只調用一次,失敗立即報錯。通常用於非冪等的寫操作,比如:新增記錄
  • Failsafe:失敗安全,失敗時直接忽略。通常用於寫入審計日誌等操作
  • Failback:失敗自動恢復,後台記錄失敗請求,定時重發。通常用於消息通知等操作
  • Forking:並行調用多個伺服器,只要一個成功即返回。通常用於實時性較高的讀操作,但浪費更多服務資源。可通過 forks="2" 設置最大並行數
  • Broadcast:廣播調用者,逐個調用,任意一台報錯則報錯。通常用於通知所有提供者更新本地資源信息,如緩存、日誌等

3. 路由規則

路由規則決定一次dubbo服務調用的目標伺服器,分為腳本路由規則和條件路由規則,支持可擴展。向註冊中心寫入路由規則的操作通常由治理中心的頁面完成

  • 腳本路由規則:支持JDK腳本引擎的所有腳本,例如:javascript, groovy 等
  • 條件路由規則:基於條件的路由規則,例如:host = 10.20.153.10 => hsot = 10.20.153.11。=>之前是consumer匹配條件,所有參數和consumer的URL進行對比,如果consumer滿足匹配條件,則對consumer執行後面的過濾規則。=>之後是provider地址列表的過濾條件,所有參數和provider的URL進行對比,consumer只拿到過濾後的地址列表

4. 負載均衡

如上圖 LoadBlance 模塊所示:在集群負載均衡時,Dubbo提供了不同的策略:

  • Random【默認】:隨機,按權重設置隨機概率。調用量越大越均勻,有利於動態調整權重
  • RoundRobin:輪詢,按公約後的許可權設置輪詢比率。如果有台機器很慢,但沒掛,當請求到那一台時就卡在那兒,久而久之,所有請求都卡在那台機器上
  • LeastActive:最少活躍調用數,活躍數指調用前後計數差,越慢的provider的調用前後計數差越大,使得慢的provider收到更少請求
  • ConsistentHash:一致性Hash,相同參數的請求發往同一台provider,當一台provider掛掉時,原本發往該機器的請求,基於虛擬節點會平攤到其他機器,不會引起劇烈變動

5. 線程派發模型

如果事件處理的邏輯能迅速完成,並且不發生新的IO請求(例如在內存中記個標識),則在IO線程上處理更快,因為減少了線程池調度

如果事件處理的邏輯較慢,或需要發起新的IO請求(例如需要查詢資料庫),則必須派發到線程池,否則 IO 線程阻塞,將導致不能接受其他請求

因此需要不同的派發策略和不同的線程池組合來應對不同的場景:

Dispatcher:

  • all:所有消息派發到 ThreadPool,包括請求、響應、連接事件、斷開事件、心跳等
  • direct:所有消息不派發 ThreadPool,全在 IO 線程上執行
  • message:只有請求響應消息派發到 ThreadPool,其他連接事件、斷開事件、心跳等,在 IO 線程上執行
  • execution:只請求消息派發到 ThreadPool,其他事件包括響應事件、連接斷開事件、心跳等消息,在 IO 線程上執行
  • connection:在 IO 線程上,將連接斷開事件放入隊列,有序逐個執行,其他時間派發到 ThreadPool

ThreadPool:

  • fixed【默認】:固定大小線程池,啟動時建立線程,一直持有不關閉
  • cached:緩存線程池,空閑一分鐘自動刪除,需要時重建
  • limited:可伸縮線程池,線程數只增長不收縮,目的是為了避免收縮時大流量引起的性能問題
  • eager:優先創建Worker線程池,corePoolSize < 任務數量 < maximumPoolSize時,優先創建 Worker 處理任務。任務數量 > maximumPoolSize時,任務放入阻塞隊列中,阻塞隊列充滿時拋出 RejectExecutionException

6. 上下文信息和隱式參數

上下文中存放著當前調用過程中所需的環境信息。RpcContext 是一個 ThreadLocal 的臨時狀態記錄器,當接收或發起 RPC 請求時,RpcContext 都會發生變化。比如:A調用B,B調用C,在B調C之前,B機器上 RpcContext 記錄的是A調用B的信息。

通過 RpcContext 的 setAttachment 和 getAttachment 可以在 provider 和 consumer 之間進行參數的隱式傳遞


7. 非同步調用

基於NIO的非阻塞實現並行調用,客戶端不需要啟動多線程即可完成多個遠程服務的並行調用,相對比多線程開銷較小


8. 註冊中心

對於 provider,它需要發布服務,而且由於應用系統的複雜性,服務的數量、類型也不斷膨脹;對於 consumer,它最關心如何獲取到它所需要的服務,而面對複雜的應用系統,需要管理大量的服務調用

服務註冊中心通過特性協議將服務統一管理起來,有效的優化內部應用對服務發布/使用的流程。Dubbo提供的註冊中心有如下幾種類型可供選擇:

① ZooKeeper註冊中心

ZK是一個樹形的服務目錄,支持變更推送,適合作為Dubbo服務的註冊中心。流程如下:

  • provider啟動時,向 /dubbo/com.foo.BarService/providers 目錄下寫入自己的 URL 地址
  • consumer啟動時,訂閱 /dubbo/com.foo.BarService/providers 目錄下的 providers 地址,並向 /dubbo/com.foo.BarService/consumers 目錄下寫入自己的 URL 地址
  • 監控中心啟動時,訂閱 /dubbo/com.foo.BarService 目錄下的所有 provider 和 consumer URL地址

當 provider 出現斷電等異常停機時,註冊中心能自動刪除 provider 信息。當註冊中心重啟、或會話過期時,能自動恢復註冊數據和訂閱請求


② Multicase註冊中心

Multicast註冊中心不需要啟動任何中心節點,只要廣播地址即可互相發現

  • provider 啟動時廣播自己的地址
  • consumer 啟動時廣播訂閱請求
  • provider 收到訂閱請求時,單播自己的地址給訂閱者,若設置了 unicast=false,則廣播給訂閱者
  • consumer 收到 provider 地址時,連接地址進行 RPC 調用

組播受網路結構限制,只適合小規模應用或開發階段


③ Redis註冊中心

使用 redis 的 Key/Map 結構存儲數據結構:

  • 主 Key 為服務名和類型
  • Map 中的 Key 為 URL 地址
  • Map 中的 Value 為過期時間,用於判斷臟數據,臟數據由監控中心刪除

調用過程:

  1. provider 啟動時,向 Key:/dubbo/com.foo.BarService/providers 下,添加當前 provider 的地址
  2. 並向 Channel:/dubbo/com.foo.BarService/providers 發送 register 事件
  3. consumer 啟動時,向 Key:/dubbo/com.foo.BarService/providers 下,添加當前 consumer 的地址
  4. 並從 Channel:/dubbo/com.foo.BarService/providers 訂閱 register 和 unregister 事件
  5. consumer 收到 register 和 unregister 事件後,從 Key:/dubbo/com.foo.BarService/providers 下獲取 provider 地址列表
  6. 服務監控中心啟動時,從 Channel:/dubbo/* 訂閱 register 和 unregister,以及 subscribe 和 unsubscribe 事件
  7. 監控中心收到 register 和 unregister 事件後,從 Key:/dubbo/com.foo.BarService/providers 下獲取 provider 地址列表
  8. 監控中心收到 subscribe 和 unsubscribe 事件後,從 Key:/dubbo/com.foo.BarService/comsumers 下獲取 consumer 地址列表

大家可以關注我的公眾號:《Java爛豬皮》,平常我也會發寫技術文章,比如:架構,分散式,微服務spring,jvm,MySQL等知識點。面試經驗也會分享給大家。

weixin.qq.com/r/9Sg5If3 (二維碼自動識別)

在此謝謝大家的關注支持~~~~


推薦閱讀:
相关文章