一、前言
整體來說,一個公司業務系統的演進流程基本都是從單體應用到多體應用。在單體應用時,不同業務子產品互相調用直接在本地 JVM 程序内就可以完成;而變為多個應用時,互相之間進行通信的方式就不能簡單的進行本地調用了,因為不同業務子產品部署到了不同的 JVM 程序裡面,更常見的是部署到了不同的機器,這時候一個高效、穩定的 RPC 遠端調用架構就變得非常重要。
Dubbo作為阿裡巴巴開發的一個開源的高性能的RPC調用架構,其緻力于提供高性能和透明化的 RPC 遠端調用服務解決方案。作為阿裡巴巴 SOA 服務化治理方案的核心架構,目前它已進入 Apache 孵化器頂級項目,前景可謂無限光明。
二、Dubbo-基礎篇
2.1 Dubbo系統組成概述
使用Dubbo架構搭建的系統架構如下:
image.png
如上圖是 Dubbo 的架構圖,其中:
- 服務提供方在啟動時候會注冊自己提供的服務到服務注冊中心。
- 服務消費方在啟動時候會去服務注冊中心訂閱自己需要的服務的位址清單,然後服務注冊中心異步把消費方需要的服務接口的提供者的位址清單傳回給服務消費方,服務消費方根據路由規則和設定的負載均衡算法選擇一個服務提供者 IP 進行調用。
- 監控平台主要用來統計服務的調用次數和調用耗時,服務消費者和提供者,在記憶體中累計調用次數和調用耗時,并定時每分鐘發送一次統計資料到監控中心,監控中心則使用資料繪制圖表來顯示,監控平台不是分布式系統必須的,但是這些資料有助于系統運維和調優。服務提供者和消費者可以直接配置監控平台的位址,也可以通過服務注冊中心來擷取。
- 服務注冊中心則負責服務注冊與發現,常見的服務注冊中心有zookeeper、etcd。
2.2 Dubbo基礎
本節主要簡單的講解Dubbo如何使用,以及本書中的demo執行個體,建議讀者先閱讀基礎篇在進入後面的章節,因為後面章節基本是基于本章的demo進行講解的。
demo中 Consumer 子產品為服務消費者相關,本書中所有與消費端有關的demo都在該子產品中,包含普通調用、各種異步調用、泛化調用、基于擴充接口實作的自定義負載均衡政策、叢集容錯政策等等。
其中 Provider 子產品為服務提供者相關,本書中所有與服務提供端有關的demo都在該子產品中,包含服務接口的實作類、服務提供方的同步處理請求、各種異步處理請求的實作等等。
其中 SDK 子產品是一個二方包,用來存放服務接口,這是為了代碼複用,在服務提供者和消費者(泛化調用除外)的子產品裡面都需要引入這個二方包。
三、Dubbo-進階篇
3.1 Dubbo分層架構
本節我們從整體上來看看 Dubbo 的分層架構設計,架構分層是一個比較經典的模式,比如網絡中的7層協定,每層執行固定的功能,上層依賴下層提供的功能,下層對上層的提供功能,下層的改變對上層不可見,并且每層都是一個可被替換的元件。
如下圖是 Dubbo 官方提供的Dubbo的整體架構圖:
image.png
Dubbo 官方提供的該架構圖很複雜,一開始我們沒必要深入細節,下面我們簡單講解下其中的主要子產品:
- 其中 Service 和 Config 層為 API接口層,是為了友善的讓Dubbo使用方釋出服務和引用服務;對于服務提供方來說需要實作服務接口,然後使用 ServiceConfig API 來釋出該服務;對于服務消費方來說需要使用ReferenceConfig 對服務接口進行代理。Dubbo服務釋出與引用方可以直接初始化配置類,也可以通過 Spring 配置自動生成配置類。
- 其它各層均為 SPI層,SPI 意味着下面各層都是元件化可以被替換的,這也是 Dubbo 設計的比較好的一點。Dubbo 增強了 JDK 中提供的标準 SPI 功能,在 Dubbo 中除了 Service 和 Config 層外,其它各層都是通過實作擴充點接口來提供服務的;Dubbo 增強的 SPI 增加了對擴充點 IoC 和 AOP 的支援,一個擴充點可以直接 setter 注入其它擴充點;并且不會一次性執行個體化擴充點的所有實作類,這避免了當擴充點實作類初始化很耗時,但目前還沒用上它的功能時仍進行加載執行個體化,浪費資源的情況;增強的 SPI 是在具體用某一個實作類的時候才對具體實作類進行執行個體化。後續會具體講解 Dubbo 增強的 SPI 的實作原理。
- Proxy 服務代理層:該層主要是對服務消費端使用的接口進行代理,把本地調用透明的轉換為遠端調用;另外對服務提供方的服務實作類進行代理,把服務實作類轉換為 Wrapper 類,這是為了減少反射的調用,後面會具體講解到。Proxy層的SPI擴充接口為 ProxyFactory,Dubbo 提供的實作主要有 JavassistProxyFactory(預設使用)和 JdkProxyFactory,使用者可以實作ProxyFactory SPI接口,自定義代理服務層的實作。
- Registry 服務注冊中心層:服務提供者啟動時候會把服務注冊到服務注冊中心,消費者啟動時候會去服務注冊中心擷取服務提供者的位址清單,Registry層主要功能是封裝服務位址的注冊與發現邏輯,擴充接口 Registry 對應的擴充實作為 ZookeeperRegistry、RedisRegistry、MulticastRegistry、DubboRegistry等。擴充接口 RegistryFactory 對應的擴充接口實作為 DubboRegistryFactory、DubboRegistryFactory、RedisRegistryFactory、ZookeeperRegistryFactory。另外該層擴充接口Directory實作類有RegistryDirectory、StaticDirectory用來透明的把invoker清單轉換為一個invoker;使用者可以實作該層的一系列擴充接口,自定義該層的服務實作。
- Cluster 路由層:封裝多個服務提供者的路由規則、負載均衡、叢集容錯的實作,并橋接服務注冊中心;擴充接口 Cluster 對應的實作類有 FailoverCluster(失敗重試)、FailbackCluster(失敗自動恢複)、FailfastCluster(快速失敗)、FailsafeCluster(失敗安全)、ForkingCluster(并行調用)等;負載均衡擴充接口 LoadBalance 對應的實作類為 RandomLoadBalance(随機)、RoundRobinLoadBalance(輪詢)、LeastActiveLoadBalance(最小活躍數)、ConsistentHashLoadBalance(一緻性hash)等。使用者可以實作該層的一系列擴充接口,自定義叢集容錯和負載均衡政策。
- Monitor 監控層:用來統計RPC 調用次數和調用耗時時間,擴充接口為 MonitorFactory,對應的實作類為 DubboMonitorFactroy。使用者可以實作該層的MonitorFactory擴充接口,實作自定義監控統計政策。
- Protocol 遠端調用層:封裝 RPC 調用邏輯,擴充接口為 Protocol, 對應實作有 RegistryProtocol、DubboProtocol、InjvmProtocol 等。
- Exchange 資訊交換層:封裝請求響應模式,同步轉異步,擴充接口 Exchanger,對應擴充實作有 HeaderExchanger 等。
- Transport 網絡傳輸層:抽象 mina 和 netty 為統一接口。擴充接口為 Channel,對應實作有 NettyChannel(預設)、MinaChannel 等;擴充接口Transporter對應的實作類有GrizzlyTransporter、MinaTransporter、NettyTransporter(預設實作);擴充接口Codec2對應實作類有DubboCodec、ThriftCodec等
- Serialize 資料序列化層:提供可以複用的一些工具,擴充接口為 Serialization,對應擴充實作有 DubboSerialization、FastJsonSerialization、Hessian2Serialization、JavaSerialization等,擴充接口ThreadPool對應擴充實作有 FixedThreadPool、CachedThreadPool、LimitedThreadPool 等。
綜上可知Dubbo的分層架構使得Dubbo的每層的功能都是可被替換的,這使得Dubbo的擴充性極強,上面說了那麼多關于擴充點的東西,那麼具體什麼是擴充點呢,下面看下 Dubbo 擴充點一個簡單例子。以擴充點 Protocol 為例:
@SPI("dubbo")public interface Protocol {...}
擴充點接口的類上面都含有@SPI注解,這裡注解裡面的"dubbo"說明Protocol擴充接口SPI的預設實作是DubboProtocol。
如果我們想自己寫一個 Protocol 擴充接口的實作類,那麼我們需要在實作類所在的 Jar 包内的 META-INF/dubbo/ 目錄下建立一個名字為 org.apache.dubbo.rpc.Protocol 的文本檔案,然後配置它的内容為:
myprotocol=com.alibaba.user.MyProtocol
假設該實作類 MyProtocol 的内容如下:
package com.alibaba.user;public class MyProtocol implemenets Protocol {// ...}
那麼如何使用我們自定義的擴充實作呢?Dubbo 配置子產品中,擴充點均有對應配置屬性或标簽,如下代碼通過配置标簽方式指定使用哪個擴充實作:
注意這裡的 name 必須與 jar 包内 META-INF/dubbo/ 目錄下 org.apache.dubbo.rpc.Protocol 檔案中的等号左側的key的名字一緻。
3.2 Dubbo核心原理
在Dubbo中架構的可擴充性是靠擴充卡原理結合增強SPI機制實作的,本書中首先會講解Dubbo的擴充卡原理,什麼是擴充卡模式?比如dubbo提供的擴充接口Protocol,Protocol的定義如下:
@SPI("dubbo")
public interface Protocol {
@Adaptive
Exporter export(Invoker invoker) throws RpcException;
....
}
Dubbo則會使用本書介紹的動态編譯技術為接口Protocol生成一個擴充卡類Protocol$Adaptive的對象執行個體,Dubbo架構中需要使用Protocol的執行個體的時候實際就是使用的Protocol$Adaptive的對象執行個體來擷取具體SPI實作類,其代碼如下:
在dubbo架構中protocol的一個定義為: private static final Protocol protocol =ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();當調用protocol.export(wrapperInvoker)時候,實際是調用的Protocol$Adaptive的對象執行個體的export方法,然後後者根據wrapperInvoker中的url裡面的協定類型參數執行代碼(2)使用Dubbo增強SPI方法getExtension擷取對應的SPI實作類,然後調用代碼(3)執行具體SPI實作類的export方法。
然後本書會講解Dubbo增強的SPI機制,本書中首先會借助 java.sql.Driver擴充接口講解标準JDK中的SPI實作原理以及缺陷,然後講解dubbo的增強SPI如何對其進行改進,如何實作的擴充接口之間自動IOC和擴充接口的功能增強AOP功能。
然後會講解Dubbo使用JavaAssist減少反射調用開銷:Dubbo會給每個服務提供者的實作類生産一個Wrapper類,這個wrapper類裡面最終調用服務提供者的接口實作類,wrapper類的存在是為了減少反射的調用。當服務提供方接受到消費方發來的請求後需要根據消費者傳遞過來的方法名和參數反射調用服務提供者的實作類,而反射本身是有性能開銷的,是以dubbo把每個服務提供者的實作類通過JavaAssist包裝為一個Wrapper類,那麼Wrapper類為何能減少反射調用那?觀看本書就可以找到答案
3.3 Dubbo功能實作原理
講解完畢支撐Dubbo架構的核心原理後,本書會先從整體剖析Dubbo服務提供端如何釋出服務的,這包含釋出本地服務和釋出遠端服務的流程, Dubbo服務導出分 本地導出與遠端導出,本地導出使用了 injvm 協定,是一個僞協定,它不開啟端口,不發起遠端調用,隻在 JVM 内直接關聯,但執行 Dubbo 的 Filter 鍊;預設下 Dubbo 同時支援本地導出與遠端導出協定,可以通過ServiceConfig的setScope方式設定,其中配置為none表示不導出服務,為remote表示隻導出遠端服務,為local表示隻導出本地服務。
你會知道Dubbo如何實作的服務延遲釋出,如何把服務實作類轉換為 Wrapper 類,以便減少反射的調用,什麼時候建構的dubbo的Filter鍊,都有哪些Wrapper類對擴充接口的實作類進行了功能增強?如何啟動的NettyServer對服務進行監聽,同一個機器上的多個服務提供接口是啟動多個NettyServer還是一個?如何做到的?如何注冊服務到服務注冊中心的?服務注冊到zookeeper後,其存儲結構是怎麼樣的?
然後本書會講解當服務提供方接受到請求後,如何進行處理的,這包含Filter鍊對請求的處理,以及如何找到對應的被wrapper類包裝後的服務實作類,并對請求進行處理,如何實作的Dubbo的服務提供端異步執行。
然後會講解Dubbo服務消費端的啟動流程,這個過程,你會知道如何基于Proxy SPI擴充實作對服務接口進行代理。與服務提供端一樣,消費端可以設定是否需要本地服務引用,你會知道在消費端如果沒有指定scope類型,在啟動時候會檢查目前jvm内是否有導出的服務,如果有則自動開啟本地引用(也就是協定類型修改為injvm),則具體調用時候會使用本地暴露的服務來提供服務,而不發起遠端調用。
當具體發起遠端調用時候,你會知道如何動态從服務注冊中心動态訂閱服務資訊的,比如訂閱服務提供者位址清單,服務降級資訊,服務路由資訊,以及Directory目錄與Router路由服務,以及什麼時候建構的路由規則鍊。
如何啟動NettyClient具體發起遠端調用的。然後你會知道同一個服務提供者機器可以提供多個服務,那麼消費者機器需要與同一個服務提供者機器提供的多個共享連接配接還是與每個服務都建立一個?消費端是啟動時候就與服務提供者機器建立好連接配接?
然後會講解具體如何發起一次遠端調用,這個過程你會知道當發起一次rpc調用時候會先經過MockInvoker進行處理,其會看是否設定了 force:return 降級政策,如果設定了則直接傳回 mock 值,并不發起遠端調用;否者發起遠端調用,如果遠端調用結果 OK,則直接傳回遠端調用傳回的結果;如果遠端調用失敗了,則看目前是否設定了 fail:return 的降級政策,如果設定了,則直接傳回 mock 值,否者傳回調用遠端服務失敗的具體原因。
如果沒有設定服務降級政策或者mock服務,則會基于SPI機制選擇具體的叢集容錯政策(本文會詳細講解常見的Failover、Failfast、Failsafe、Forking、Broadcast這幾種叢集容錯實作原理,以及講解如何自己基于SPI實作自己的容錯政策),具體叢集容錯政策内有會根據SPI機制選擇設定的服務負載均衡政策(本文會詳細介紹常見的Random、RoundRobin、LeastActive、ConsistentHash),具體負載均衡政策内會基于SPI選擇設定的服務目錄實作,其内部維護了所有服務提供者的服務提供者清單與路由規則,負載均衡政策則會從符合路由規則的位址清單裡面選擇一個invoker傳回,然後最終有該invoker執行。如果執行失敗了,則根據具體叢集容錯政策重新選擇一個invoker進行執行....
然後本書會講解Dubbo線程模型與線程池政策,Dubbo 預設的底層網絡通訊使用的是 Netty ,服務提供方 NettyServer 使用兩級線程池,其中 EventLoopGroup(boss) 主要用來接受用戶端的連結請求,并把接受的請求分發給 EventLoopGroup(worker) 來處理,boss 和 worker 線程組我們稱之為 IO 線程。
如果服務提供方的邏輯能迅速完成,并且不會發起新的 IO 請求,那麼直接在 IO 線程上處理會更快,因為這減少了線程池排程與上下文切換開銷。但如果處理邏輯較慢,或者需要發起新的 IO 請求,比如需要查詢資料庫,則 IO 線程必須派發請求到新的線程池進行處理,否則 IO 線程會被阻塞,将導緻不能接收其它請求。
Dubbo中在服務提供端與消費端的IO線程對請求處理時候預設是把請求轉交給dubbo架構的内部線程池來進行處理的,以便可以及時釋放IO線程。
根據IO線程把什麼類型的消息或者請求交給内部線程池來處理,dubbo提供了不同的線程模型,本書主要講解Dubbo提供的線程模型AllDispatcher、DirectDispatcher、MessageOnlyDispatcher、ExecutionDispatcher、ConnectionOrderedDispatcher的實作原理,以及線程池政策FixedThreadPool、LimitedThreadPool、EagerThreadPool、CachedThreadPool的實作原理,以及如何基于SPI自定義自己的線程模型與線程池政策。
基礎篇我們講解到,基于Dubbo APi搭建Dubbo服務時候,服務消費端引入了一個 SDK 二方包,裡面存放着服務提供端提供的所有接口類,泛化接口調用方式主要在服務消費端沒有 API 接口類及模型類元(比如入參和出參的 POJO 類)的情況下使用。其參數及傳回值中沒有對應的 POJO 類,是以所有 POJO 均轉換為 Map 表示。使用泛化調用時候服務消費子產品不再需要引入 SDK 二方包,本書會詳細介紹Dubbo中nativejava,true, bean三種泛化調用的實作。
基礎篇我們講到Dubbo提供了隐式參數傳遞的功能,即服務調用方可以通過RpcContext.getContext().setAttachment()方法設定附加屬性鍵值對,然後設定的值對可以在服務提供方服務方法内擷取;本書我們會詳細介紹如何在在消費端設定參數,并且如何通過網絡把參數傳遞到服務提供方,然後服務提供方如何進行擷取。
正如Dubbo官網所說dubbo從2.7.0版本開始支援所有異步程式設計接口以CompletableFuture為基礎,以便解決2.7.0之前版本異步調用的不便與功能缺失。
異步調用實作是基于 NIO 的非阻塞能力實作并行調用,服務消費端不需要啟動多線程即可完成并行調用多個遠端服務,相對多線程開銷較小,如下圖是Dubbo異步調用鍊路概要流程圖圖:
image.png
本書我們首先講解dubbo服務消費端的異步調用,首先講解2.7.0版本前的異步調用實作原理,我們會知道future調用get()方法方式實作異步缺點是當業務線程調用get()方法後業務線程會被阻塞,這不是我們想要的,是以dubbo2.7.0版本提供了在CompletableFuture對象上設定回調函數的方式,讓我們實作真正的異步調用。
在Provider端非異步執行時候,其對調用方發來的請求的處理是在Dubbo内部線程模型的線程池中的線程來執行的,在dubbo中服務提供方提供的所有的服務接口都是使用這一個線程池來執行的,是以當一個服務執行比較耗時時候,可能會占用線程池中很多線程,這可能就會導緻其他服務的處理收到影響。
Provider端異步執行則将服務的處理邏輯從Dubbo内部線程池切換到業務自定義線程,避免Dubbo線程池中線程被過度占用,有助于避免不同服務間的互相影響。
但是需要注意provider端異步執行對節省資源和提升RPC響應性能是沒有效果的,這時是因為如果服務處理比較耗時,雖然不是使用Dubbo架構内部線程處理,但是還是需要業務自己的線程來處理,另外副作用還有會新增一次線程上下文切換(從dubbo内部線程池線程切換到業務線程),模型如下圖11.2.0
image.png
本書首先會講解基于定義CompletableFuture簽名的接口實作異步執行的實作原理,然後講解使用AsyncContext實作異步執行原理,最後講解Dubbo的異步調用與執行引入的新問題以及如何解決的,這包含引入異步調用時候等結果傳回後Filter鍊得不到執行的問題,以及異步執行時候上下文參數傳遞問題。
前面章節我們介紹了服務消費端一次服務調用流程與服務提供端一次服務處理流程,但是還是有一些東西是我們沒有提到的,比如服務消費端如何把服務請求資訊序列化為二進制、服務提供方又是如何把消費端發送的二進制資料反序列化為可識别的POJO對象、比如Dubbo的應用層協定是怎麼樣的。本書我們就來一一來看dubbo是如何做這些的。
本書會首先講解Dubbo協定,在TCP協定棧中,每層協定都有自己的協定封包格式,比如TCP協定是網絡七層模型中的傳輸層,有TCP協定封包格式;在TCP上層是應用層,應用層協定常見的有http協定等,Dubbo協定作為建立在TCP協定之上的一種應用層協定,自然也有自己的協定包格式,Dubbo協定也是參考TCP協定棧中的協定,協定内容由header和body兩部分組成,本書會詳細介紹協定header中每個字段含義。然後講解服務消費方編碼原理,包含當服務消費端發送請求時候,如何把請求内容封裝為Dubbo協定幀的。然後講解服務提供方接受請求後如何對協定幀進行解碼解決半包粘包問題的。
四、Dubbo-實踐篇
實踐篇我們來探讨如何使用Arthas和一些demo來對研究Dubbo架構實作提供便捷,并且基于Netty與CompletableFuture模拟了RPC同步與純異步調用。
首先本書會介紹如何安裝Arthas,然後講解如何使用Arthas檢視檢視擴充接口擴充卡類的源碼,檢視服務提供端Wrapper類的源碼,如何查詢Dubbo啟動後都有哪些Filter,然後通過Demo驗證RoundRobin LoadBalance負載均衡原理,然後探讨如果根據IP動态路由調用Dubbo服務。
Dubbo的服務消費端基于CompletableFuture實作了功能比較豐富的純異步調用,其實還不單單是CompletableFuture的功勞,歸根到底是Netty的NIO非阻塞功能提供的底層實作,本文我們就來基于CompletableFuture與Netty來模拟下如何異步發起遠端調用,以及如何使用CompletableFuture本身的功能,讓多個請求的異步結果進行運算,以便加深對dubbo異步調用實作原理的了解。
五、總結
如何你對上面内容感興趣,想深入研究,但是無從入手,那麼機會來了,