一. Dubbo誕生背景 隨著互聯網的發展和網站規模的擴大,系統架構也從單點的垂直結構往分散式服務架構演進,如下圖所示: 單一應用架構:一個應用部署所有功能,此時簡化CRUD的ORM框架是關鍵 垂直應用架構:應用拆分為不相干的幾個應用,前後端分離,此時用於加速前端頁面開發的Web MVC框架是關鍵 ...
一. 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:
① 定義服務介面:
public interface DemoService { String sayHello(String name); }
② 服務提供者代碼:
public class DemoServiceImpl implements DemoService { public String sayHello(String name) { return "Hello " + name; } }
ServiceConfig 類拿到對外提供服務的實際類 ref(如:DemoServiceImpl)通過 ProxyFactory.getInvoker 方法使用 ref 生成一個 AbstractProxyInvoker 實例,然後 通過 Protocol.export 方法新生成一個 Exporter 實例
當網路通訊層收到一個請求後,會找到對應的 Exporter 實例,並調用它所對應的 AbstractProxyInvoker 實例,從而真正調用了服務提供者的代碼
③ 服務消費者代碼:
public class DemoClientAction {
private DemoService demoService;
public void setDemoService(DemoService demoService) {
this.demoService = demoService;
}
public void start() {
String hello = demoService.sayHello("world");
} }
首先通過 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 為過期時間,用於判斷臟數據,臟數據由監控中心刪除
調用過程:
- provider 啟動時,向 Key:/dubbo/com.foo.BarService/providers 下,添加當前 provider 的地址
- 並向 Channel:/dubbo/com.foo.BarService/providers 發送 register 事件
- consumer 啟動時,向 Key:/dubbo/com.foo.BarService/providers 下,添加當前 consumer 的地址
- 並從 Channel:/dubbo/com.foo.BarService/providers 訂閱 register 和 unregister 事件
- consumer 收到 register 和 unregister 事件後,從 Key:/dubbo/com.foo.BarService/providers 下獲取 provider 地址列表
- 服務監控中心啟動時,從 Channel:/dubbo/* 訂閱 register 和 unregister,以及 subscribe 和 unsubscribe 事件
- 監控中心收到 register 和 unregister 事件後,從 Key:/dubbo/com.foo.BarService/providers 下獲取 provider 地址列表
- 監控中心收到 subscribe 和 unsubscribe 事件後,從 Key:/dubbo/com.foo.BarService/comsumers 下獲取 consumer 地址列表