接下來一段時間敖丙將帶大家開啟緊張刺激的 Dubbo 之旅!是的要開始寫 Dubbo 系列的文章了,之前我已經(jīng)寫過一篇架構(gòu)演進的文章,也說明了微服務(wù)的普及化以及重要性,服務(wù)化場景下隨之而來的就是服務(wù)之間的通信問題,那服務(wù)間的通信腦海中想到的就是 RPC,說到 RPC 就離不開咱們的 Dubbo。
這篇文章敖丙先帶著大家來總覽全局,一般而言熟悉一個框架你要先知道這玩意是做什么的,能解決什么痛點,核心的模塊是什么,大致運轉(zhuǎn)流程是怎樣的。
你要一來就扎入細節(jié)之中無法自拔,一波 DFS 直接被勸退的可能性高達99.99%,所以本暖男敖丙將帶大家先過一遍 Dubbo 的簡介、總體分層、核心組件以及大致調(diào)用流程。
不僅如此我還會帶著大家過一遍如果要讓你設(shè)計一個 RPC 框架你看看都需要什么功能?這波操作之后你會發(fā)現(xiàn)嘿嘿 Dubbo 怎么設(shè)計的和我想的一樣呢?真是英雄所見略同啊!
而且我還會寫一個簡單版 RPC 框架實現(xiàn),讓大家明白 RPC 到底是如何工作的。
如果看了這篇文章你要還是不知道 Dubbo 是啥,我可以要勸退了。
我們先來談一談什么叫 RPC ,我發(fā)現(xiàn)有很多同學不太了解這個概念,還有人把 RPC 和 HTTP 來進行對比。所以咱們先來說說什么是 RPC。
RPC,Remote Procedure Call 即遠程過程調(diào)用,遠程過程調(diào)用其實對標的是本地過程調(diào)用,本地過程調(diào)用你熟悉吧?
想想那青蔥歲月,你在大學趕著期末大作業(yè),正在攻克圖書管理系統(tǒng),你奮筆疾書瘋狂地敲擊鍵盤,實現(xiàn)了圖書借閱、圖書歸還等等模塊,你實現(xiàn)的一個個方法之間的調(diào)用就叫本地過程調(diào)用。
你要是和我說你實現(xiàn)圖書館里系統(tǒng)已經(jīng)用了服務(wù)化,搞了遠程調(diào)用了,我只能和你說你有點東西。
簡單的說本機上內(nèi)部的方法調(diào)用都可以稱為本地過程調(diào)用,而遠程過程調(diào)用實際上就指的是你本地調(diào)用了遠程機子上的某個方法,這就是遠程過程調(diào)用。
所以說 RPC 對標的是本地過程調(diào)用,至于 RPC 要如何調(diào)用遠程的方法可以走 HTTP、也可以是基于 TCP 自定義協(xié)議。
所以說你討論 RPC 和 HTTP 就不是一個層級的東西。
而 RPC 框架就是要實現(xiàn)像那小助手一樣的東西,目的就是讓我們使用遠程調(diào)用像本地調(diào)用一樣簡單方便,并且解決一些遠程調(diào)用會發(fā)生的一些問題,使用戶用的無感知、舒心、放心、順心,它好我也好,快樂沒煩惱。
在明確了什么是 RPC,以及 RPC 框架的目的之后,咱們想想如果讓你做一款 RPC 框架你該如何設(shè)計?
我們先從消費者方(也就是調(diào)用方)來看需要些什么,首先消費者面向接口編程,所以需要得知有哪些接口可以調(diào)用,可以通過公用 jar 包的方式來維護接口。
現(xiàn)在知道有哪些接口可以調(diào)用了,但是只有接口啊,具體的實現(xiàn)怎么來?這事必須框架給處理了!所以還需要來個代理類,讓消費者只管調(diào),啥事都別管了,我代理幫你搞定。
對了,還需要告訴代理,你調(diào)用的是哪個方法,并且參數(shù)的值是什么。
雖說代理幫你搞定但是代理也需要知道它到底要調(diào)哪個機子上的遠程方法,所以需要有個注冊中心,這樣調(diào)用方從注冊中心可以知曉可以調(diào)用哪些服務(wù)提供方,一般而言提供方不止一個,畢竟只有一個掛了那不就沒了。
所以提供方一般都是集群部署,那調(diào)用方需要通過負載均衡來選擇一個調(diào)用,可以通過某些策略例如同機房優(yōu)先調(diào)用啊啥的。
當然還需要有容錯機制,畢竟這是遠程調(diào)用,網(wǎng)絡(luò)是不可靠的,所以可能需要重試什么的。
還要和服務(wù)提供方約定一個協(xié)議,例如我們就用 HTTP 來通信就好啦,也就是大家要講一樣的話,不然可能聽不懂了。
當然序列化必不可少,畢竟我們本地的結(jié)構(gòu)是“立體”的,需要序列化之后才能傳輸,因此還需要約定序列化格式。
并且這過程中間可能還需要摻入一些 Filter,來作一波統(tǒng)一的處理,例如調(diào)用計數(shù)啊等等。
這些都是框架需要做的,讓消費者像在調(diào)用本地方法一樣,無感知。
服務(wù)提供者肯定要實現(xiàn)對應的接口這是毋庸置疑的。
然后需要把自己的接口暴露出去,向注冊中心注冊自己,暴露自己所能提供的服務(wù)。
然后有消費者請求過來需要處理,提供者需要用和消費者協(xié)商好的協(xié)議來處理這個請求,然后做反序列化。
序列化完的請求應該扔到線程池里面做處理,某個線程接受到這個請求之后找到對應的實現(xiàn)調(diào)用,然后再將結(jié)果原路返回。
上面其實我們都提到了注冊中心,這東西就相當于一個平臺,大家在上面暴露自己的服務(wù),也在上面得知自己能調(diào)用哪些服務(wù)。
當然還能做配置中心,將配置集中化處理,動態(tài)變更通知訂閱者。
面對眾多的服務(wù),精細化的監(jiān)控和方便的運維必不可少。
這點很多開發(fā)者在開發(fā)的時候察覺不到,到你真正上線開始運行維護的時候,如果沒有良好的監(jiān)控措施,快速的運維手段,到時候就是睜眼瞎!手足無措,等著挨批把!
那種痛苦不要問我為什么知道,我就是知道!
讓我們小結(jié)一下,大致上一個 RPC 框架需要做的就是約定要通信協(xié)議,序列化的格式、一些容錯機制、負載均衡策略、監(jiān)控運維和一個注冊中心!
沒錯就是簡單的實現(xiàn),上面我們在思考如何設(shè)計一個 RPC 框架的時候想了很多,那算是生產(chǎn)環(huán)境使用級別的功能需求了,我們這是 Demo,目的是突出 RPC框架重點功能 - 實現(xiàn)遠程調(diào)用。
所以啥七七八八的都沒,并且我用偽代碼來展示,其實也就是刪除了一些保護性和約束性的代碼,因為看起來太多了不太直觀,需要一堆 try-catch 啥的,因此我刪減了一些,直擊重點。
Let's Do It!
首先我們定義一個接口和一個簡單實現(xiàn)。
public interface AobingService {
String hello(String name);
}
public class AobingServiceImpl implements AobingService {
public String hello(String name) {
return 'Yo man Hello,I am' + name;
}
}
然后我們再來實現(xiàn)服務(wù)提供者暴露服務(wù)的功能。
public class AobingRpcFramework {
public static void export(Object service, int port) throws Exception {
ServerSocket server = new ServerSocket(port);
while(true) {
Socket socket = server.accept();
new Thread(new Runnable() {
//反序列化
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
String methodName = input.read(); //讀取方法名
Class<?>[] parameterTypes = (Class<?>[]) input.readObject(); //參數(shù)類型
Object[] arguments = (Object[]) input.readObject(); //參數(shù)
Method method = service.getClass().getMethod(methodName, parameterTypes); //找到方法
Object result = method.invoke(service, arguments); //調(diào)用方法
// 返回結(jié)果
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
output.writeObject(result);
}).start();
}
}
public static <T> T refer (Class<T> interfaceClass, String host, int port) throws Exception {
return (T) Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class<?>[] {interfaceClass},
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] arguments) throws Throwable {
Socket socket = new Socket(host, port); //指定 provider 的 ip 和端口
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
output.write(method.getName()); //傳方法名
output.writeObject(method.getParameterTypes()); //傳參數(shù)類型
output.writeObject(arguments); //傳參數(shù)值
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
Object result = input.readObject(); //讀取結(jié)果
return result;
}
});
}
}
好了,這個 RPC 框架就這樣好了,是不是很簡單?就是調(diào)用者傳遞了方法名、參數(shù)類型和參數(shù)值,提供者接收到這樣參數(shù)之后調(diào)用對于的方法返回結(jié)果就好了!這就是遠程過程調(diào)用。
我們來看看如何使用
//服務(wù)提供者只需要暴露出接口
AobingService service = new AobingServiceImpl ();
AobingRpcFramework.export(service, 2333);
//服務(wù)調(diào)用者只需要設(shè)置依賴
AobingService service = AobingRpcFramework.refer(AobingService.class, '127.0.0.1', 2333);
service.hello();
看起來好像好不錯喲,不過這很是簡陋,用作 demo 有助理解還是極好的!
接下來就來看看 Dubbo 吧!上正菜!
Dubbo 是阿里巴巴 2011年開源的一個基于 Java 的 RPC 框架,中間沉寂了一段時間,不過其他一些企業(yè)還在用 Dubbo 并自己做了擴展,比如當當網(wǎng)的 Dubbox,還有網(wǎng)易考拉的 Dubbok。
但是在 2017 年阿里巴巴又重啟了對 Dubbo 維護。在 2017 年榮獲了開源中國 2017 最受歡迎的中國開源軟件 Top 3。
在 2018 年和 Dubbox 進行了合并,并且進入 Apache 孵化器,在 2019 年畢業(yè)正式成為 Apache 頂級項目。
目前 Dubbo 社區(qū)主力維護的是 2.6.x 和 2.7.x 兩大版本,2.6.x 版本主要是 bug 修復和少量功能增強為準,是穩(wěn)定版本。
而 2.7.x 是主要開發(fā)版本,更新和新增新的 feature 和優(yōu)化,并且 2.7.5 版本的發(fā)布被 Dubbo 認為是里程碑式的版本發(fā)布,之后我們再做分析。
它實現(xiàn)了面向接口的代理 RPC 調(diào)用,并且可以配合 ZooKeeper 等組件實現(xiàn)服務(wù)注冊和發(fā)現(xiàn)功能,并且擁有負載均衡、容錯機制等。
我們先來看下官網(wǎng)的一張圖。
本丙再暖心的給上圖內(nèi)每個節(jié)點的角色說明一下。
節(jié)點 | 角色說明 |
---|---|
Consumer | 需要調(diào)用遠程服務(wù)的服務(wù)消費方 |
Registry | 注冊中心 |
Provider | 服務(wù)提供方 |
Container | 服務(wù)運行的容器 |
Monitor | 監(jiān)控中心 |
我再來大致說一下整體的流程,首先服務(wù)提供者 Provider 啟動然后向注冊中心注冊自己所能提供的服務(wù)。
服務(wù)消費者 Consumer 啟動向注冊中心訂閱自己所需的服務(wù)。然后注冊中心將提供者元信息通知給 Consumer, 之后 Consumer 因為已經(jīng)從注冊中心獲取提供者的地址,因此可以通過負載均衡選擇一個 Provider 直接調(diào)用 。
之后服務(wù)提供方元數(shù)據(jù)變更的話注冊中心會把變更推送給服務(wù)消費者。
服務(wù)提供者和消費者都會在內(nèi)存中記錄著調(diào)用的次數(shù)和時間,然后定時的發(fā)送統(tǒng)計數(shù)據(jù)到監(jiān)控中心。
首先注冊中心和監(jiān)控中心是可選的,你可以不要監(jiān)控,也不要注冊中心,直接在配置文件里面寫然后提供方和消費方直連。
然后注冊中心、提供方和消費方之間都是長連接,和監(jiān)控方不是長連接,并且消費方是直接調(diào)用提供方,不經(jīng)過注冊中心。
就算注冊中心和監(jiān)控中心宕機了也不會影響到已經(jīng)正常運行的提供者和消費者,因為消費者有本地緩存提供者的信息。
總的而言 Dubbo 分為三層,如果每一層再細分下去,一共有十層。別怕也就十層,本丙帶大家過一遍,大家先有個大致的印象,之后的文章丙會帶著大家再深入。
大的三層分別為 Business(業(yè)務(wù)層)、RPC 層、Remoting,并且還分為 API 層和 SPI 層。
分為大三層其實就是和我們知道的網(wǎng)絡(luò)分層一樣的意思,只有層次分明,職責邊界清晰才能更好的擴展。
而分 API 層和 SPI 層這是 Dubbo 成功的一點,采用微內(nèi)核設(shè)計+SPI擴展,使得有特殊需求的接入方可以自定義擴展,做定制的二次開發(fā)。
接下來咱們再來看看每一層都是干嘛的。
Service,業(yè)務(wù)層,就是咱們開發(fā)的業(yè)務(wù)邏輯層。
Config,配置層,主要圍繞 ServiceConfig 和 ReferenceConfig,初始化配置信息。
Proxy,代理層,服務(wù)提供者還是消費者都會生成一個代理類,使得服務(wù)接口透明化,代理層做遠程調(diào)用和返回結(jié)果。
Register,注冊層,封裝了服務(wù)注冊和發(fā)現(xiàn)。
Cluster,路由和集群容錯層,負責選取具體調(diào)用的節(jié)點,處理特殊的調(diào)用要求和負責遠程調(diào)用失敗的容錯措施。
Monitor,監(jiān)控層,負責監(jiān)控統(tǒng)計調(diào)用時間和次數(shù)。
Portocol,遠程調(diào)用層,主要是封裝 RPC 調(diào)用,主要負責管理 Invoker,Invoker代表一個抽象封裝了的執(zhí)行體,之后再做詳解。
Exchange,信息交換層,用來封裝請求響應模型,同步轉(zhuǎn)異步。
Transport,網(wǎng)絡(luò)傳輸層,抽象了網(wǎng)絡(luò)傳輸?shù)慕y(tǒng)一接口,這樣用戶想用 Netty 就用 Netty,想用 Mina 就用 Mina。
Serialize,序列化層,將數(shù)據(jù)序列化成二進制流,當然也做反序列化。
我再稍微提一下 SPI(Service Provider Interface),是 JDK 內(nèi)置的一個服務(wù)發(fā)現(xiàn)機制,它使得接口和具體實現(xiàn)完全解耦。我們只聲明接口,具體的實現(xiàn)類在配置中選擇。
具體的就是你定義了一個接口,然后在META-INF/services
目錄下放置一個與接口同名的文本文件,文件的內(nèi)容為接口的實現(xiàn)類,多個實現(xiàn)類用換行符分隔。
這樣就通過配置來決定具體用哪個實現(xiàn)!
而 Dubbo SPI 還做了一些改進,篇幅有限留在之后再談。
上面我已經(jīng)介紹了每個層到底是干嘛的,我們現(xiàn)在再來串起來走一遍調(diào)用的過程,加深你對 Dubbo 的理解,讓知識點串起來,由點及面來一波連連看。
我們先從服務(wù)提供者開始,看看它是如何工作的。
首先 Provider 啟動,通過 Proxy 組件根據(jù)具體的協(xié)議Protocol 將需要暴露出去的接口封裝成 Invoker,Invoker 是 Dubbo 一個很核心的組件,代表一個可執(zhí)行體。
然后再通過 Exporter 包裝一下,這是為了在注冊中心暴露自己套的一層,然后將 Exporter 通過 Registry 注冊到注冊中心。這就是整體服務(wù)暴露過程。
接著我們來看消費者調(diào)用流程(把服務(wù)者暴露的過程也在圖里展示出來了,這個圖其實算一個挺完整的流程圖了)。
首先消費者啟動會向注冊中心拉取服務(wù)提供者的元信息,然后調(diào)用流程也是從 Proxy 開始,畢竟都需要代理才能無感知。
Proxy 持有一個 Invoker 對象,調(diào)用 invoke 之后需要通過 Cluster 先從 Directory 獲取所有可調(diào)用的遠程服務(wù)的 Invoker 列表,如果配置了某些路由規(guī)則,比如某個接口只能調(diào)用某個節(jié)點的那就再過濾一遍 Invoker 列表。
剩下的 Invoker 再通過 LoadBalance 做負載均衡選取一個。然后再經(jīng)過 Filter 做一些統(tǒng)計什么的,再通過 Client 做數(shù)據(jù)傳輸,比如用 Netty 來傳輸。
傳輸需要經(jīng)過 Codec 接口做協(xié)議構(gòu)造,再序列化。最終發(fā)往對應的服務(wù)提供者。
服務(wù)提供者接收到之后也會進行 Codec 協(xié)議處理,然后反序列化后將請求扔到線程池處理。某個線程會根據(jù)請求找到對應的 Exporter ,而找到 Exporter 其實就是找到了 Invoker,但是還會有一層層 Filter,經(jīng)過一層層過濾鏈之后最終調(diào)用實現(xiàn)類然后原路返回結(jié)果。
完成整個調(diào)用過程!
這次敖丙帶著大家先了解了下什么是 RPC,然后規(guī)劃了一波 RPC 框架需要哪些組件,然后再用代碼實現(xiàn)了一個簡單的 RPC 框架。
然后帶著大家了解了下 Dubbo 的發(fā)展歷史、總體架構(gòu)、分層設(shè)計架構(gòu)以及每個組件是干嘛的,再帶著大伙走了一遍整體調(diào)用過程。
我真的是太暖了啊!
dubbo近期我會安排幾個章節(jié)繼續(xù)展開,最后會出一個面試版本的dubbo,我們拭目以待吧。
我是敖丙,你知道的越多,你不知道的越多,我們下期見!