【51CTO.com原創(chuàng)稿件】分布式應用場景有高并發(fā),高可擴展和高性能的要求。還涉及到,序列化/反序列化,網(wǎng)絡,多線程以及設計模式的問題。幸好 Dubbo 框架將上述知識進行了封裝,讓程序員能夠把注意力放到業(yè)務上。
圖片來自 Pexels
為了更好地了解和使用 Dubbo,今天來介紹一下 Dubbo 的主要組件和實現(xiàn)原理。
Dubbo 分層
Dubbo 是一款高性能 Java RPC 架構(gòu)。它實現(xiàn)了面向接口代理的 RPC 調(diào)用,服務注冊和發(fā)現(xiàn),負載均衡,容錯,擴展性等等功能。
Dubbo 大致上分為三層,分別是:
- 業(yè)務層
- RPC 層
- Remoting 層
Dubbo 的三層結(jié)構(gòu)
從上圖中可以看到,三層結(jié)構(gòu)中包含了 Dubbo 的核心組件。他們的基本功能如下,對于比較常用的組件,會在后面的篇幅中詳細講解。
組件功能列表
這里將這些組件羅列出來,能有一個感性的認識。具體開發(fā)的時候,知道運用哪些組件。
Dubbo 調(diào)用工作流
Dubbo 框架是用來處理分布式系統(tǒng)中,服務發(fā)現(xiàn)與注冊以及調(diào)用問題的,并且管理調(diào)用過程。
上面介紹了 Dubbo 的框架分層,下圖的工作流就展示了他們是如何工作的。
Dubbo 服務調(diào)用流程圖
工作流涉及到服務提供者(Provider),注冊中心(Registration),網(wǎng)絡(Network)和服務消費者(Consumer):
- 服務提供者在啟動的時候,會通過讀取一些配置將服務實例化。
- Proxy 封裝服務調(diào)用接口,方便調(diào)用者調(diào)用。客戶端獲取 Proxy 時,可以像調(diào)用本地服務一樣,調(diào)用遠程服務。
- Proxy 在封裝時,需要調(diào)用 Protocol 定義協(xié)議格式,例如:Dubbo Protocol。
- 將 Proxy 封裝成 Invoker,它是真實服務調(diào)用的實例。
- 將 Invoker 轉(zhuǎn)化成 Exporter,Exporter 只是把 Invoker 包裝了一層,是為了在注冊中心中暴露自己,方便消費者使用。
- 將包裝好的 Exporter 注冊到注冊中心。
- 服務消費者建立好實例,會到服務注冊中心訂閱服務提供者的元數(shù)據(jù)。元數(shù)據(jù)包括服務 IP 和端口以及調(diào)用方式(Proxy)。
- 消費者會通過獲取的 Proxy 進行調(diào)用。通過服務提供方包裝過程可以知道,Proxy 實際包裝了 Invoker 實體,因此需要使用 Invoker 進行調(diào)用。
- 在 Invoker 調(diào)用之前,通過 Directory 獲取服務提供者的 Invoker 列表。在分布式的服務中有可能出現(xiàn)同一個服務,分布在不同的節(jié)點上。
- 通過路由規(guī)則了解,服務需要從哪些節(jié)點獲取。
- Invoker 調(diào)用過程中,通過 Cluster 進行容錯,如果遇到失敗策略進行重試。
- 調(diào)用中,由于多個服務可能會分布到不同的節(jié)點,就要通過 LoadBalance 來實現(xiàn)負載均衡。
- Invoker 調(diào)用之前還需要經(jīng)過 Filter,它是一個過濾鏈,用來處理上下文,限流和計數(shù)的工作。
- 生成過濾以后的 Invoker。
- 用 Client 進行數(shù)據(jù)傳輸。
- Codec 會根據(jù) Protocol 定義的協(xié)議,進行協(xié)議的構(gòu)造。
- 構(gòu)造完成的數(shù)據(jù),通過序列化 Serialization 傳輸給服務提供者。
- Request 已經(jīng)到達了服務提供者,它會被分配到線程池(ThreadPool)中進行處理。
- Server 拿到請求以后查找對應的 Exporter(包含有 Invoker)。
- 由于 Export 也會被 Filter 層層包裹
- 通過 Filter 以后獲得 Invoker
- 最后,對服務提供者實體進行調(diào)用。
上面調(diào)用步驟經(jīng)歷了這么多過程,其中出現(xiàn)了 Proxy,Invoker,Exporter,F(xiàn)ilter。
實際上都是調(diào)用實體在不同階段的不同表現(xiàn)形式,本質(zhì)是一樣的,在不同的使用場景使用不同的實體。
例如 Proxy 是用來方便調(diào)用者調(diào)用的。Invoker 是在調(diào)用具體實體時使用的。Exporter 用來注冊到注冊中心的等等。
后面我們會對具體流程進行解析。如果時間不夠無法閱讀完全文,可以把上面的圖保存。
服務暴露實現(xiàn)原理
上面講到的服務調(diào)用流程中,開始服務提供者會進行初始化,將暴露給其他服務調(diào)用。服務消費者也需要初始化,并且在注冊中心注冊自己。
服務提供者和服務消費者暴露服務
首先來看看服務提供者暴露服務的整體機制:
服務提供者暴露服務流程
開篇的大圖中列舉了 Config 核心組件,在服務提供者初始化的時候,會通過 Config 組件中的 ServiceConfig 讀取服務的配置信息。
這個配置信息有三種形式,分別是 XML 文件,注解(Annoation)和屬性文件(Properties 和 yaml)。
在讀取配置文件生成服務實體以后,會通過 ProxyFactory 將 Proxy 轉(zhuǎn)換成 Invoker。
此時,Invoker 會被定義 Protocol,之后會被包裝成 Exporter。最后,Exporter 會發(fā)送到注冊中心,作為服務的注冊信息。上述流程主要通過 ServiceConfig 中的 doExport 完成。
下面是針對多協(xié)議多注冊中心進行源代碼分析:
doExportUrls 方法
doExportUrlsFor1Protocol 方法-1
doExportUrlsFor1Protocol 方法-2
上面截取了服務提供者暴露服務的代碼片段,從注釋上看整個暴露過程分為七個步驟:
- 讀取其他配置信息到 map 中,用來后面構(gòu)造 URL。
- 讀取全局配置信息。
- 配置不是 remote,也就是暴露本地服務。
- 如果配置了監(jiān)控地址,則服務調(diào)用信息會上報。
- 通過 Proxy 轉(zhuǎn)化成 Invoker,RegistryURL 存放的是注冊中心的地址。
- 暴露服務以后,向注冊中心注冊服務信息。
- 沒有注冊中心直接暴露服務。
一旦服務注冊到注冊中心以后,注冊中心會通過 RegistryProtocol 中的 Export 方法將服務暴露出去,并依次做以下操作:
- 委托具體協(xié)議進行服務暴露,創(chuàng)建 NettyServer 監(jiān)聽端口,并保持服務實例。
- 創(chuàng)建注冊中心對象,創(chuàng)建對應的 TCP 連接。
- 注冊元數(shù)據(jù)到注冊中心。
- 訂閱 Configurators 節(jié)點。
- 如果需要銷毀服務,需要關(guān)閉端口,注銷服務信息。
說完了服務提供者的暴露再來看看服務消費者。
服務消費者消費服務機制
服務消費者首先持有遠程服務實例生成的 Invoker,然后把 Invoker 轉(zhuǎn)換成用戶接口的動態(tài)代理引用。
框架進行服務引用的入口點在 ReferenceBean 中的 getObject 方法,會將實體轉(zhuǎn)換成 ReferenceBean,它是集成與 ReferenceConfig 類的。
這里一起來看看 createProxy 的源代碼:
getProxy 代碼片段 1
getProxy 代碼片段 2
從上面代碼片段可以看出,消費者服務在調(diào)用服務提供者時,做了以下動作:
- 檢查是否是同一個 JVM 內(nèi)部引用。
- 如果是同一個 JVM 的引用,直接使用 injvm 協(xié)議從內(nèi)存中獲取實例。
- 注冊中心地址后,添加 refer 存儲服務消費元數(shù)據(jù)信息。
- 單注冊中心消費。
- 依次獲取注冊中心的服務,并且添加到 Invokers 列表中。
- 通過 Cluster 將多個 Invoker 轉(zhuǎn)換成一個 Invoker。
- 把 Invoker 轉(zhuǎn)換成接口代理。
注冊中心
說完服務暴露,再回頭來看看注冊中心。Dubbo 通過注冊中心實現(xiàn)了分布式環(huán)境中服務的注冊和發(fā)現(xiàn)。
配置中心
其主要作用如下:
- 動態(tài)載入服務。服務提供者通過注冊中心,把自己暴露給消費者,無須消費者逐個更新配置文件。
- 動態(tài)發(fā)現(xiàn)服務。消費者動態(tài)感知新的配置,路由規(guī)則和新的服務提供者。
- 參數(shù)動態(tài)調(diào)整。支持參數(shù)的動態(tài)調(diào)整,新參數(shù)自動更新到所有服務節(jié)點。
- 服務統(tǒng)一配置。統(tǒng)一連接到注冊中心的服務配置。
配置中心工作流
注冊調(diào)用流程圖
先看看注冊中心調(diào)用的流程圖:
- 提供者(Provider)啟動時,會向注冊中心寫入自己的元數(shù)據(jù)信息(調(diào)用方式)。
- 消費者(Consumer)啟動時,也會在注冊中心寫入自己的元數(shù)據(jù)信息,并且訂閱服務提供者,路由和配置元數(shù)據(jù)的信息。
- 服務治理中心(duubo-admin)啟動時,會同時訂閱所有消費者,提供者,路由和配置元數(shù)據(jù)的信息。
- 當提供者離開或者新提供者加入時,注冊中心發(fā)現(xiàn)變化會通知消費者和服務治理中心。
注冊中心工作原理
Dubbo 有四種注冊中心的實現(xiàn),分別是 ZooKeeper,Redis,Simple 和 Multicast。
這里著重介紹一下 ZooKeeper 的實現(xiàn)。ZooKeeper 是負責協(xié)調(diào)服務式應用的。
它通過樹形文件存儲的 ZNode 在 /dubbo/Service 目錄下面建立了四個目錄,分別是:
- Providers 目錄下面,存放服務提供者 URL 和元數(shù)據(jù)。
- Consumers 目錄下面,存放消費者的 URL 和元數(shù)據(jù)。
- Routers 目錄下面,存放消費者的路由策略。
- Configurators 目錄下面,存放多個用于服務提供者動態(tài)配置 URL 元數(shù)據(jù)信息。
客戶端第一次連接注冊中心的時候,會獲取全量的服務元數(shù)據(jù),包括服務提供者和服務消費者以及路由和配置的信息。
根據(jù) ZooKeeper 客戶端的特性,會在對應 ZNode 的目錄上注冊一個 Watcher,同時讓客戶端和注冊中心保持 TCP 長連接。
如果服務的元數(shù)據(jù)信息發(fā)生變化,客戶端會接受到變更通知,然后去注冊中心更新元數(shù)據(jù)信息。變更時根據(jù) ZNode 節(jié)點中版本變化進行。
Dubbo 集群容錯
Cluster,Directory,Router,LoadBalance 核心接口
分布式服務多以集群形式出現(xiàn),Dubbo 也不例外。在消費服務發(fā)起調(diào)用的時候,會涉及到 Cluster,Directory,Router,LoadBalance 幾個核心組件。
Cluster,Directory,Router,LoadBalance 調(diào)用流程
先看看他們是如何工作的:
①生成 Invoker 對象。根據(jù) Cluster 實現(xiàn)的不同,生成不同類型的 ClusterInvoker 對象。通過 ClusertInvoker 中的 Invoker 方法啟動調(diào)用流程。
②獲取可調(diào)用的服務列表,可以通過 Directory 的 List 方法獲取。這里有兩類服務列表的獲取方式。
分別是 RegistryDirectory 和 StaticDirectory:
- RegistryDirectory:屬于動態(tài) Directory 實現(xiàn),會自動從注冊中心更新 Invoker 列表,配置信息,路由列表。
- StaticDirectory:它是 Directory 的靜態(tài)列表實現(xiàn),將傳入的 Invoker 列表封裝成靜態(tài)的 Directory 對象。
在 Directory 獲取所有 Invoker 列表之后,會調(diào)用路由接口(Router)。其會根據(jù)用戶配置的不同策略對 Invoker 列表進行過濾,只返回符合規(guī)則的 Invoker。
假設用戶配置接口 A 的調(diào)用,都使用了 IP 為 192.168.1.1 的節(jié)點,則 Router 會自動過濾掉其他的 Invoker,只返回 192.168.1.1 的 Invoker。
這里介紹一下 RegistryDirectory 的實現(xiàn),它通過 Subscribe 和 Notify 方法,訂閱和監(jiān)聽注冊中心的元數(shù)據(jù)。
Subscribe,訂閱某個 URL 的更新信息。Notify,根據(jù)訂閱的信息進行監(jiān)聽。包括三類信息,配置 Configurators,路由 Router,以及 Invoker 列表。
管理員會通過 dubbo-admin 修改 Configurators 的內(nèi)容,Notify 監(jiān)聽到該信息,就更新本地服務的 Configurators 信息。
同理,路由信息更新了,也會更新服務本地路由信息。如果 Invoker 的調(diào)用信息變更了(服務提供者調(diào)用信息),會根據(jù)具體情況更新本地的 Invoker 信息。
Notify 監(jiān)聽三類信息
通過前面三步生成的 Invoker 需要調(diào)用最終的服務,但是服務有可能分布在不同的節(jié)點上面。所以,需要經(jīng)過 LoadBalance。
Dubbo 的負載均衡策略有四種:
- Random LoadBalance,隨機,按照權(quán)重設置隨機概率做負載均衡。
- RoundRobinLoadBalance,輪詢,按照公約后的權(quán)重設置輪詢比例。
- LeastActiveLoadBalance,按照活躍數(shù)調(diào)用,活躍度差的被調(diào)用的次數(shù)多。活躍度相同的 Invoker 進行隨機調(diào)用。
- ConsistentHashLoadBalance,一致性 Hash,相同參數(shù)的請求總是發(fā)到同一個提供者。
最后進行 RPC 調(diào)用。如果調(diào)用出現(xiàn)異常,針對不同的異常提供不同的容錯策略。Cluster 接口定義了 9 種容錯策略,這些策略對用戶是完全透明的。
用戶可以在,,, 標簽上通過 Cluster 屬性設置:
- Failover,出現(xiàn)失敗,立即重試其他服務器。可以設置重試次數(shù)。
- Failfast,請求失敗以后,返回異常結(jié)果,不進行重試。
- Failsafe,出現(xiàn)異常,直接忽略。
- Failback,請求失敗后,將失敗記錄放到失敗隊列中,通過定時線程掃描該隊列,并定時重試。
- Forking,嘗試調(diào)用多個相同的服務,其中任意一個服務返回,就立即返回結(jié)果。
- Broadcast,廣播調(diào)用所有可以連接的服務,任意一個服務返回錯誤,就任務調(diào)用失敗。
- Mock,響應失敗時返回偽造的響應結(jié)果。
- Available,通過遍歷的方式查找所有服務列表,找到第一個可以返回結(jié)果的節(jié)點,并且返回結(jié)果。
- Mergable,將多個節(jié)點請求合并進行返回。
Dubbo 遠程調(diào)用
服務消費者經(jīng)過容錯,Invoker 列表,路由和負載均衡以后,會對 Invoker 進行過濾,之后通過 Client 編碼,序列化發(fā)給服務提供者。
過濾,發(fā)送請求,編碼,序列化發(fā)送給服務提供者
從上圖可以看出在服務消費者調(diào)用服務提供者的前后,都會調(diào)用 Filter(過濾器)。
可以針對消費者和提供者配置對應的過濾器,由于過濾器在 RPC 執(zhí)行過程中都會被調(diào)用,所以為了提高性能需要根據(jù)具體情況配置。
Dubbo 系統(tǒng)有自帶的系統(tǒng)過濾器,服務提供者有 11 個,服務消費者有 5 個。過濾器的使用可以通過 @Activate 的注釋,或者配置文件實現(xiàn)。
配置文件實現(xiàn)過濾器
過濾器的使用遵循以下幾個規(guī)則:
- 過濾器順序,過濾器執(zhí)行是有順序的。例如,用戶定義的過濾器的過濾順序默認會在系統(tǒng)過濾器之后。
- 又例如,上圖中 filter=“filter01, filter02”,filter01 過濾器執(zhí)行就在 filter02 之前。
- 過濾器失效,如果針對某些服務或者方法不希望使用某些過濾器,可以通過“-”(減號)的方式使該過濾器失效。例如,filter=“-filter01”。
過濾器疊加,如果服務提供者和服務消費者都配置了過濾器,那么兩個過濾器會被疊加生效。
由于,每個服務都支持多個過濾器,而且過濾器之間有先后順序。因此在設計上 Dubbo 采用了裝飾器模式,將 Invoker 進行層層包裝,每包裝一層就加入一層過濾條件。在執(zhí)行過濾器的時候就好像拆開一個一個包裝一樣。
調(diào)用請求經(jīng)過過濾以后,會以 Invoker 的形式對 Client 進行調(diào)用。Client 會交由底層 I/O 線程池處理,其包括處理消息讀寫,序列化,反序列化等邏輯。
同時會對 Dubbo 協(xié)議進行編碼和解碼操作。Dubbo 協(xié)議基于 TCP/IP 協(xié)議,包括協(xié)議頭和協(xié)議體。
協(xié)議體包含了傳輸?shù)闹饕獌?nèi)容,其意義不言而喻,它是由 16 字節(jié)長的報文組成,每個字節(jié)包括 8 個二進制位。
內(nèi)容如下:
- 0-7 位,“魔法數(shù)”高位。
- 8-15 位,“魔法數(shù)”低位。前面兩個字節(jié)的“魔法數(shù)”,是用來區(qū)別兩個不同請求。好像編程中使用的“;”“/”之類的符號將兩條記錄分開。PS:魔法數(shù)用固定的“0xdabb”表示,
- 16 位,數(shù)據(jù)包的類型,因為 RPC 調(diào)用是雙向的,0 表示 Response,1 表示 Request。
- 17 位,調(diào)用方式,0 表示單項,1 表示雙向。
- 18 位,時間標識,0 表示請求/響應,1 表示心跳包。
- 19-23 位,序列化器編號,就是告訴協(xié)議用什么樣的方式進行序列化。例如:Hessian2Serialization 等等。
- 24-31 位,狀態(tài)位。20 表示 OK,30 表示 CLIENT_TIMEOUT 客戶端超時,31 表示 SERVER_TIMEOUT 服務端超時,40 表示 BAD_REQUEST 錯誤的請求,50 表示 BAD_RESPONSE 錯誤的響應。
- 32-95 位,請求的唯一編號,也就是 RPC 的唯一 ID。
- 96-127,消息體包括 Dubbo 版本號,服務接口名,服務接口版本,方法名,參數(shù)類型,方法名,參數(shù)類型,方法參數(shù)值和請求額外參數(shù)。
服務消費者在調(diào)用之前會將上述服務消息體,根據(jù) Dubbo 協(xié)議打包好。框架內(nèi)部會調(diào)用 DefaultFuture 對象的 get 方法進行等待。
在準備發(fā)送請求的時候,才創(chuàng)建 Request 對象,這個對象會保存在一個靜態(tài)的 HashMap 中,當服務提供者處理完 Request 之后,將返回的 Response 放回到 Futures 的 HashMap 中。
在 HashMap 中會找到對應的 Request 對象,并且返回給服務消費者。
服務消費者請求和響應圖
協(xié)議打包好以后就需要給協(xié)議編碼和序列化。這里需要用到 Dubbo 的編碼器,其過程是將信息傳化成字節(jié)流。
Dubbo 協(xié)議編碼請求對象分為使用 ExchangeCodec 中的兩個方法,encodeRequest 負責編碼協(xié)議頭和 encodeRequestData 編碼協(xié)議體。
同樣通過 encodeResponse 編碼協(xié)議頭,encodeResponseData 編碼協(xié)議體。
服務消費者和提供者都通過 decode 和 decodeBody 兩個方法進行解碼,不同的是解碼有可能在 IO 線程或者 Dubbo 線程池中完成。
雖然,編碼和解碼的細節(jié)在這里不做展開,但是以下幾點需要注意:
- 構(gòu)造 16 字節(jié)的協(xié)議頭,特別是需要創(chuàng)建前面兩個字節(jié)的魔法數(shù),也就是“0xdabb”,它是用來分割兩個不同請求的。
- 生成唯一的請求/響應 ID,并且根據(jù)這個 ID 識別請求和響應協(xié)議包。
- 通過協(xié)議頭中的 19-23 位的描述,進行序列化/反序列化操作。
- 為了提高處理效率,每個協(xié)議都會放到 Buffer 中處理。
當服務提供者收到請求協(xié)議包以后,先將其放到 ThreadPool 中,然后依次處理。
由于服務提供者在注冊中心是通過 Exporter 的方式暴露服務的,服務消費者也是通過 Exporter 作為接口進行調(diào)用的。
Exporter 是將 Invoker 進行了包裝,將拆開的 Invoker 進行 Filter 過濾鏈條進行過濾以后,再去調(diào)用服務實體。最后,將信息返回給服務消費者。
總結(jié)
我們首先了解 Dubbo 的分層和幾個核心模塊,分別介紹他們的職責。然后通過一個簡單的例子,服務消費者調(diào)用服務提供者,用 Dubbo 的工作流程將各個模塊串起來。
在這 22 步的流程中,以服務提供者和服務消費者的初始化為起點,用到了 Config 和 Proxy 以及 Protocol,Invoker。
注冊中心作為兩者的連接橋梁,起到了服務發(fā)現(xiàn)和注冊的作用,并且著重講了如何通過 ZooKeeper 實現(xiàn)注冊中心的原理。
在服務消費者調(diào)用提供者之前,需要通過 Cluster 容錯機制,Directory 獲取 Invoker 列表,Router 找到路由信息,再使用 LoadBalance 知道具體服務。
在調(diào)用服務提供者之間還不忘通過 Filter 進行過濾,通過裝飾者模式實現(xiàn)的 Filter 可以形成過濾鏈條,依次對條件進行過濾。
對于遠程調(diào)用,需要調(diào)用打包協(xié)議,針對 Dubbo 協(xié)議進行了描述,并且針對該協(xié)議進行了編碼/解碼和序列化/反序列化的操作。
服務提供者收到請求以后,會將請求放到 ThreadPool 中逐一處理。通過 Exporter,Invoker,F(xiàn)ilter 的逐級轉(zhuǎn)換,最后響應請求。
由于篇幅有限很多功能例如 SPI,Merger 等沒有介紹到,有時間再和大家細聊。
作者:崔皓
簡介:十六年開發(fā)和架構(gòu)經(jīng)驗,曾擔任過惠普武漢交付中心技術(shù)專家,需求分析師,項目經(jīng)理,后在創(chuàng)業(yè)公司擔任技術(shù)/產(chǎn)品經(jīng)理。善于學習,樂于分享。目前專注于技術(shù)架構(gòu)與研發(fā)管理。
【51CTO原創(chuàng)稿件,合作站點轉(zhuǎn)載請注明原文作者和出處為51CTO.com】