分布式技術一:RPC通信與服務治理框架——Dubbo

一、什么是Dubbo?

1、Dubbo簡介

  • Dubbo是一款開源的,高性能、輕量級的Java RPC框架
  • Dubbo是SOA時代的產物

2、Dubbo的產生

在我們思考為什么要生產一個工具的時候,我們的前置原因是要解決一個問題。回顧上文,在分布式系統中,我們將不同的服務置放于不同的服務器上,來滿足系統的一些需求時,出現了一個問題:不同服務器上的服務,要如何通信?

在先前的單機系統中(以Spring Boot開發的單機應用為例),我們通常會為一個服務提供至少三段代碼:Controller、Service和ServiceImpl。在單機系統中,應用間的通信,通常利用Spring的@Autowired注解,注入一個被調用的Bean,以達到調用的目的。但是在分布式系統中,我們也想如此調用,但是發現,服務間隔著網絡的通信。在一般情況下,我們會想到,能否建立一個鏈接來實現這種調用,Dubbo就應運而生。


項目結構演變.jpg

作為SOA時代的產物,Dubbo一定程度上滿足了SOA所需的一些內容:粗粒度、松耦合。服務之間,可以通過簡單、精確定義接口進行通訊,并且不涉及底層編程接口和通訊模型。

Dubbo的底層封裝了Netty,利用Netty完成RPC(Remote Procedure Call,遠程過程調用)過程,在操作上簡易快捷,讓開發者僅僅關心服務的提供過程,不必直面服務間到底進行了怎么樣的調用,極大地減少了分布式應用開發工作量。

3、RPC

RPC.jpg

I、首先需要解除的問題是:既然已經能通過HTTP協議(內置了TCP)發起請求,為什么還要自定義封裝TCP構建RPC?

①、針對效率問題

起初在http1.1的協議時代,一個完整的http報文是繁瑣而復雜的,即使編碼協議也就是body是使用二進制編碼協議,報文元數據也就是header頭的鍵值對卻用了文本編碼,無用的字節占據了六到七成,這嚴重影響了通信的效率。服務間調用的過程,不該有那么多冗余字段的出現,也需要一定的時效性,因此RPC被提出和應用

②、針對封裝性問題

現在都http2.0了,效率上做了極大改進,那么為什么還要用rpc或者grpc?首先,需要了解,grpc這種rpc庫使用的就是http2.0協議。但是http容器的性能測試單位通常是kqps,自定義tpc協議則高出其一到兩個兩級。其次,完整的rpc封裝,其實內置了更多的功能,比如負載均衡、服務降級等,相較于http有著更高的可用性。

II、那到底什么是RPC?

實現不同服務器上的服務之間通信的工具。因為服務不在同一個服務器,不在一個內存當中,因此需要遠程調用,即RPC。本質上,是一個封裝了服務請求調用的建立TCP,并且能完成有效的服務器間通信的工具。

一個完整的RPC框架要對使用者和用戶做到低層透明。

一次完整的RPC調用流程如下(此處先不關心異步還是同步):
1)服務消費方(client)調用以本地調用方式調用服務;
2)client stub接收到調用后負責將方法、參數等組裝成能夠進行網絡傳輸的消息體;
3)client stub找到服務地址,并將消息發送到服務端;
4)server stub收到消息后進行解碼;
5)server stub根據解碼結果調用本地的服務;
6)本地服務執行并將結果返回給server stub;
7)server stub將返回結果打包成消息并發送至消費方;
8)client stub接收到消息,并進行解碼;
9)服務消費方得到最終結果。
RPC框架的目標就是要2~8這些步驟都封裝起來,這些細節對用戶來說是透明的,不可見的。

二、快速使用(詳情:請參考官方文檔的《快速開始》

1、注冊中心安裝

Dubbo所推薦的注冊中心為Zookeeper(此處參考Zookeeper的官方文檔和其他博主的安裝說明,不做贅述),也可以用Nacos(在后期Spring Cloud Alibaba的使用中會有所提及,該處不做說明)

2、Dubbo的使用流程

① 創建服務提供者

創建服務,并將服務暴露給注冊中心(如果沒有注冊中心,也可以采用Dubbo的直連)

② 創建服務消費者

創建一個消費者,調用(消費)服務

③ 在Impl內提供服務

在服務方,實現服務,可以利用Stub完成調用前準入操作

④ 在Controller遠程調用,消費Impl內的服務

在消費方,給外界提供服務Api的層級,利用Dubbo的Reference調用已暴露的服務,完成功能服務的對外提供。

三、Dubbo的基本原理

1、Dubbo架構與運行(未來補充圖文)

I、Dubbo的架構

Duubo架構圖.jpg

①、 容器啟動

將應用歸置在容器內啟動服務,作為生產者,是一個初始化活動

② 注冊服務

需要將可被調用的服務注冊到一個注冊中心內,告知系統,服務可被調用。該步驟也是一個生產者服務的初始化。

③ 發現服務

消費者想要調取服務,需要去注冊中心中獲取。該步驟分為兩個步驟,消費者向注冊中心發起調用的請求,是一個初始化調用的過程。注冊中心通知消費者其所請求的服務的狀況,是一個異步的動作。

④ 代理調用

Dubbo內部實現了代理,將被成功發現的服務反饋給消費者進行調用,如果被消費者所需的服務可以被調用,該步驟是一個同步的調用過程。

⑤ 監控服務

Dubbo內部實現了一個監控器,可以監控Dubbo的運行狀況,監聽采用的是異步過程。

II、Dubbo的層級結構

Dubbo運行過程.jpg
  • config 配置層:對外配置接口,以 ServiceConfig, ReferenceConfig 為中心,可以直接初始化配置類,也可以通過 spring 解析配置生成配置類
  • proxy 服務代理層:服務接口透明代理,生成服務的客戶端 Stub 和服務器端 Skeleton, 以 ServiceProxy 為中心,擴展接口為 ProxyFactory
  • registry 注冊中心層:封裝服務地址的注冊與發現,以服務 URL 為中心,擴展接口為 RegistryFactory, Registry, RegistryService
  • cluster 路由層:封裝多個提供者的路由及負載均衡,并橋接注冊中心,以 Invoker 為中心,擴展接口為 Cluster, Directory, Router, LoadBalance
  • monitor 監控層:RPC 調用次數和調用時間監控,以 Statistics 為中心,擴展接口為 MonitorFactory, Monitor, MonitorService
  • protocol 遠程調用層:封裝 RPC 調用,以 Invocation, Result 為中心,擴展接口為 Protocol, Invoker, Exporter
  • exchange 信息交換層:封裝請求響應模式,同步轉異步,以 Request, Response 為中心,擴展接口為 Exchanger, ExchangeChannel, ExchangeClient, ExchangeServer
  • transport 網絡傳輸層:抽象 mina 和 netty 為統一接口,以 Message 為中心,擴展接口為 Channel, Transporter, Client, Server, Codec
  • serialize 數據序列化層:可復用的一些工具,擴展接口為 Serialization, ObjectInput, ObjectOutput, ThreadPool
    (該處引用尚硅谷的文檔)

III、Dubbo的運行過程

①、Dubbo的啟動與配置解析

Dubbo的啟動時,需要的是解析和加載配置,通過DubboNamespaceHandler,通過registerBeanDefinitionParse()解析器從Bean容器內提取對應配置標簽的配置對象及配置信息,生成對應的DubboBeanDefinitionParse對象。在DubboBeanDefinitionParse中,會有一處BeanDefinition類型的parse()方法,該處會根據傳入的不同的lConfig配置類型,對不同的標簽所配置的文件進行匹配和注冊(set方法),完成對配置內容的解析和保存。(此處需要注意,解析過程中,根據標簽的解析順序,產生的對同一屬性配置值的覆蓋問題,該處會在Dubbo高級特性中說明)

Dubbo配置加載.JPG

②、Dubbo的暴露服務過程

在關注啟動加載時,我們關注到在DubboNamespaceHandler中有兩個特殊的加載,就是ServiceBean,一個是ReferenceBean。其中服務的暴露過程與ServiceBean相關。在ServiceBean中,其實現了兩個重要的接口:

  • 一個是Spring原生的Bean初始化InitializingBean,在容器創建完對象后,會回調的一個void afterPropertiesSet()方法。該方法,在會對provider的配置信息做保存的工作。
  • 另一個是ApplicationListener<ContextRefreshedEvent>,在整個IOC容器初始化啟動完成后,會回調一個void onApplicationEvent()方法。該方法會進行一次判斷,如果一個服務沒有暴露且需要暴露,那么調用export()方法,進行服務的暴露。

export()中,先會獲取加載服務的信息,然后調用doExport()方法。

doExport()方法是一個加設Synchronized的同步方法。在doExport()中,對服務對象進行一系列的檢查后,調用doExportUrls()doExportUrl()中會首先加載注冊中心LoadRegisteries(),然后循環讀取ProtocolConfig對象,獲取到需暴露服務的協議以及對應的端口,注意,該處為循環讀取,也說明一個服務在提供時,可以構建多份(集群部署)。然后調用doExportUrlsForProtocol()方法。

doExportUrlsForProtocol()方法中,最關鍵的幾行代碼為

Invoke<?> invoker = proxyFactory.getInvoker(ref,(Class),interface...);
DelegateProviderMetaDataInvoker wrapperInvoker = new DelegateProviderMetaDataInvoker(invoker,this);
Exporter<?> exporter = protocol.export(wrapperInvoker);
exporters.add(exporter);

該處首先,利用proxyFactory.getInvoker獲取執行器,即經過包裝的、被調用的服務的實現類以及調用的url等信息的組合,即invoker。然后將invoker封裝進DelegateProviderMetaDataInvoker對象,然后提供給exporter

然后注意protocol.export(wrapperInvoker),該處所能調用的協議,是基于protocol內的方法ExtensionLoader,getExtensionLoader(),而具體該調用什么協議進行暴露服務,是基于Java SPI協議進行的。這里由于是Dubbo的服務,因此調用DubboProtocolRegistryProtocol內的export()方法。其中RegistryProtocol內的export()方法首先初始化了服務的信息(如:url、接口名等),會調用DubboProtocol內的export()方法,首先獲取到Url地址,然后逐漸的將執行器invoker封裝成一個DubboExporter暴露器,然后調用openServer(),初次調用會對其調用創建服務器方法,并綁定url,之后會進入Netty的底層方法,完成建立服務并監聽暴露服務的地址即端口。之后,會將服務進行注冊ProviderConsumerRegTable.registerProvider(),該處存在兩個CurrentHashMap,CurrentHashMap<String,Set<providerInvokers>> providerInvokersCurrentHashMap<String,Set<consumerInvokers>> consumerInvokers,該處向CurrentHashMap<String,Set<providerInvokers>> providerInvokers添加新的服務對象,String為注冊的調用地址,Set<>為調用服務的信息。最后調用注冊器registry(),將服務暴露的地址,注冊到注冊中心,完成服務暴露。

簡要來說就是:首先是ServiceBean的解析,然后ApplicationListener<ContextRefreshedEvent>出發一個事件,然后通過建立服務監聽,并在注冊中心注冊,完成服務的暴露。

③、Dubbo的引用過程

在Dubbo進行引用時,會用到@Reference注解。實際上與@Autowired一樣,引用也意味著需要從容器內部獲取到服務的Bean。Reference進行引用時,會建立一個ReferenceBean(這是一個FactoryBean),因此最后會調用FactoryBeangetObject(),返回標簽配置注入的對象。在getObject()的返回:get()中,會對第一次調用進行一個init()(初始化)。該初始化會操作一個creatProxy(),創建一個代理對象,帶代理器會保存一些信息,如:調用地址、調用屬性、方法等。

在creatProxy方法內部,通過調用refProtocol.refer(interface,urls.get(0)),該refProtocol依舊是基于Java SPI協議進行的。這里由于是Dubbo的服務,因此調用DubboProtocolRegistryProtocol內的refer()方法。

  • RegistryProtocol內的refer()方法內,調用doRefer(),傳入調用所需的信息,其方法內部dictionary.subscribe(),去注冊中心訂閱服務,獲取引用信息。
  • 在訂閱中,會進入DubboProtocol內的refer()方法,在執行refer()過程中會構建DubboInvoker對象時傳入一個getClient()來獲取客戶端(消費端),getClient()內會初始化客戶端,并構建連接,此時進入了Netty的底層,利用Netty構建出客戶端,最后返回DubboInvoker對象。之后,將DubboInvoker對象通過ProviderConsumerRegTable.registerProvider()CurrentHashMap<String,Set<consumerInvokers>> consumerInvokers內注冊消費對象,最后返回ref = creatProxy()這個代理對象。

④、Dubbo的調用過程

Dubbo調用鏈.jpg

在獲取到Proxy對象后,Dubbo會代理執行。首先在InvokerInvocationHandler中執行invoker.invoke,其內部依舊是層層代理以及Filter會獲取到可用的執行器列表,即invoker.invoke執行時會先去注冊中心列表(AbstractClusterInvokerList<Invoker<T>>)內獲取到所有代理執行器,并利用Java SPI協議獲取Dubbo協議。之后加載負載均衡配置load.ExtensionLoader.get.ExtensionLoader(LoadBalance.class),然后利用RpcUtils構建異步請求,并執行doInvokedoInvoke在逐層的代理中,調動負載均衡,然后匹配到一個服務器上的invoker。在該處,其實是圖中Proxy→Filter→Invoker→LoadBalance的過程,在Filter中可以根據不同配置的屬性進行分濾,加載各類配置,然后才開始進入負載均衡。

在加載完負載均衡,會進入下一層Filter實現統計、監聽、備份等功能和,最終會在DubboInvoker中執行currentClient = clients[0]獲取到調用的客戶端。然后currentClient.request向客戶端發起請求并獲取請求結果并封裝成Result對象。該處請求過程會先進行序列號,返回反序列化解碼獲得請求結果。最后根據請求為異步還是同步,分別調用asyncCallBack或者syncCallBack返回代理請求的結果,完成調用。此處對應圖內的Remoting。

2、Dubbo高級特性

I、負載均衡

在集群負載均衡時,Dubbo 提供了多種均衡策略,默認為 random 隨機調用。
其主要由以下幾種負載均衡策略

  • Random LoadBalance
    隨記調用,按權重設置的權重進行隨機概率。
    在一個截面上碰撞的概率高,隨著調用量的增加,調用某臺機器的頻率就越趨于分配的權重比率。將權重置換為概率后,有利于動態調整提供者權重。
  • RoundRobin LoadBalance
    請求時對提供服務的列表進行輪循,按RoundRobin哈希取模后的權重設置輪循比率。
    存在慢的提供者累積請求的問題,比如:某機器沒掛但是執行異常卡頓,當請求調到這臺機器時就會阻塞在此,長時間調用后的結果就是所有請求都卡在調到這臺機器上。
  • LeastActive LoadBalance
    最少活躍調用數,相同活躍數的隨機,活躍數指調用前后計數差。
    使慢的提供者收到更少請求,因為越慢的提供者的調用前后計數差會越大。
  • ConsistentHash LoadBalance
    一致性 Hash,相同參數的請求總是發到同一提供者。
    當某一臺提供者掛時,原本發往該提供者的請求,基于虛擬節點,平攤到其它提供者,不會引起劇烈變動。
    默認只對第一個參數 Hash,如果要修改,請配置 <dubbo:parameter key="hash.arguments" value="0,1" />
    默認用 160 份虛擬節點,如果要修改,請配置 <dubbo:parameter key="hash.nodes" value="320" />

II、服務降級

①、當服務器壓力劇增的情況下,根據實際業務情況及流量,對一些服務和頁面有策略的不處理或換種簡單的方式處理,從而釋放服務器資源以保證核心交易正常運作或高效運作。

可以通過服務降級功能臨時屏蔽某個出錯的非關鍵服務,并定義降級后的返回策略。
其中,主要由兩種配置:

  • mock=force:return+null 表示消費方對該服務的方法調用都直接返回 null 值,不發起遠程調用。用來屏蔽不重要服務不可用時對調用方的影響。
  • mock=fail:return+null 表示消費方對該服務的方法調用在失敗后,再返回 null 值,不拋異常。用來容忍不重要服務不穩定時對調用方的影響。

②、還可以進行集群容錯

主要有以下配置,默認為 failover 重試。

  • Failover Cluster
    失敗自動切換,當出現失敗,重試其它服務器。通常用于讀操作,但重試會帶來更長延遲。可通過 retries="n" 來設置重試n次(不含第一次)。
    重試次數配置如下:<dubbo:service retries="n" />或<dubbo:reference retries="n" />或<dubbo:reference><dubbo:method name="findFoo" retries="n" /></dubbo:reference>
  • Failfast Cluster
    快速失敗,只發起一次調用,失敗立即報錯。可用于非冪等性的操作。
  • Failsafe Cluster
    失敗安全:出現異常時,直接忽略。可用于日志處理中。
  • Failback Cluster
    失敗自動恢復:根據后臺記錄失敗請求,定時重發。例如:消息通知操作。
  • Forking Cluster
    并行聯調:并行調用多個服務器,只要一個成功即返回。用于實時性要求較高的讀操作,但需要浪費更多服務資源。可通過 forks="n" 來設置最大并行數。
  • Broadcast Cluster
    廣播調配:廣播調用所有提供者,逐個調用,任意一臺報錯則報錯 。可在通知所有提供者更新緩存或日志等本地資源信息中配置。

集群模式配置
在服務提供方:<dubbo:service cluster="failsafe" />或消費方配置集群模式:<dubbo:reference cluster="failsafe" />

\color{violet}{推薦:與Hystrix整合}

III、服務熔斷

\color{violet}{推薦:與Hystrix整合}

IV、注冊中心宕機與Dubbo的直連

zookeeper注冊中心宕機,還可以消費dubbo暴露的服務。
該處設計,保證了服務調用的健壯性,其保證了高可用,減少系統不能提供服務的時間。而其能保持可用的原因在于:

  • 監控中心宕掉不影響使用,只是丟失部分采樣數據
  • 數據庫宕掉后,注冊中心仍能通過緩存提供服務列表查詢,但不能注冊新服務
  • 注冊中心對等集群,任意一臺宕掉后,將自動切換到另一臺
  • 服務提供者無狀態,任意一臺宕掉后,不影響使用
  • 服務提供者全部宕掉后,服務消費者應用將無法使用,并無限次重連等待服務提供者恢復

V、超時與重試

設置timeout屬性來配置最大超時時間,超時即返回調用失敗。配置retries標簽來進行重試配置,失敗調用時自動重試。當進行重試時,會對其它服務器進行重試,但重試會帶來更長延遲。可通過 retries="n" 來設置重試次數(不含第一次)。

VI、本地緩存

結果緩存,用于加速熱門數據的訪問速度,Dubbo 提供聲明式緩存,以減少用戶加緩存的工作量。

緩存類型

  • lru 基于最近最少使用原則刪除多余緩存,保持最熱的數據被緩存。
  • threadlocal 當前線程緩存,比如一個頁面渲染,用到很多 portal,每個 portal 都要去查用戶信息,通過線程緩存,可以減少這種多余訪問。
  • jcacheJSR107 集成,可以橋接各種緩存實現。

<dubbo:reference interface="com.foo.BarService" cache="lru" />

VII、配置覆蓋與引用優先級

參考兩張圖片以及官方文檔

配置覆蓋規則:

  • JVM 啟動 -D 參數優先。
  • XML 次之。
  • Properties 最后,相當于默認值,只有 XML 沒有配置時,dubbo.properties 的相應配置項才會生效。
Dubbo配置屬性覆蓋.jpg

配置的引用規則:

  • 方法級配置別優于接口級別,小Scope優先
  • Consumer端配置 優于 Provider配置 優于 全局配置
  • 最后是Dubbo Hard Code的配置值
Dubbo配置引用優先級.jpg

VIII、本地存根

Dubbo本地存根.jpg

遠程服務后,客戶端通常只剩下接口,而實現全在服務器端,但提供方有些時候想在客戶端也執行部分邏輯,比如:做 ThreadLocal 緩存,提前驗證參數,調用失敗后偽造容錯數據等等,此時就需要在 API 中帶上 Stub,客戶端生成 Proxy 實例,會把 Proxy 通過構造函數傳給 Stub 1,然后把 Stub 暴露給用戶,Stub 可以決定要不要去調 Proxy。
<dubbo:service interface="com.foo.BarService" stub="true" />或<dubbo:service interface="com.foo.BarService" stub="com.foo.BarServiceStub" />

IX、多版本

利用version標簽配置新老版本的調用,先配置一個新版本的服務,讓一部分請求落在新版的服務器上,逐漸的將老版本的請求轉接給新版本的服務器,實現服務的無停機式升級。

結束語

Dubbo的介紹到此結束,但是這僅僅是分布式技術一個一個開端,我們離真實的分布式還很遠,Dubbo為我們完成遠程服務的調用封裝,利用成熟的RPC技術,方便我們將部署在不同服務器上的應進行調取。Dubbo也僅僅能為我們做到這些,因此他也足以被稱之為一個輕量級RPC通信框架。但是作為分布式應用,僅僅完成通信和一些簡單的配置是完全不夠的,還有很多事情沒有做完,如何更好的完成一致性?如何更好的維護可用性?在CAP原則上,我們要么追求CP,要么會追求AP,根據項目的不同需求,定制不同的組件拼裝結構(即應用層面的架構)。接下來,我們將介紹一套基于微服務時代的產物,它提供了一套完整的微服務治理方案,即SpringCloud,而由于SpringCloud為維護問題且為了能更好的與Dubbo結合,我們將采取SpringCloudAlibaba進行介紹,逐漸了解,如何構建微服務以及如何進行治理。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
禁止轉載,如需轉載請通過簡信或評論聯系作者。

推薦閱讀更多精彩內容