文章目錄
- 1. Dubbo與RPC的關系
- 2. Dubbo的基本使用
-
- 2.1 Dubbo是什麼?
- 2.2 負載均衡
- 2.3 服務逾時
- 2.4 叢集容錯
- 2.5 服務降級
- 2.6 本地存根
- 2.7 參數回調
- 2.8 異步調用
- 2.9 泛化調用、泛化服務
- 3. dubbo的REST協定
- 4. dubbo的控制台
- 5. dubbo的服務路由
1. Dubbo與RPC的關系
1.1 什麼是RPC?
維基百科這樣解釋:
遠端過程調用(英語:Remote Procedure Call,縮寫為 RPC)是一個計算機通信協定。該協定允許運作于一台計算機的程式調用另一個位址空間(通常為一個開放網絡的一台計算機)的子程式,而程式員就像調用本地程式一樣,無需額外地為這個互動作用程式設計(無需關注細節)。RPC是一種伺服器-用戶端(Client/Server)模式,經典實作是一個通過發送請求-接受回應進行資訊互動的系統。
如果涉及的軟體采用面向對象程式設計,那麼遠端過程調用亦可稱作遠端調用或遠端方法調用,是以,對于Java程式員而言,RPC就是遠端方法調用。
如何了解RPC是一個計算機通信協定呢?我們已經知道RPC是專注于遠端方法調用,如果實作遠端方法調用,基本的就是通過網絡,通過傳輸資料來進行調用。如下圖所示

可以看到遠端方法A 想要調用遠端方法B,需要定義 資料類型 和 傳輸協定。而這些需要定義的東西作為一個協定存在于調用方和接收方,後續所有調用都遵守這個已制定的協定,這就是RPC通信協定。是以,我們其實可以看到RPC的自定義性是很高的,各個公司内部都可以實作自己的一套RPC架構,而Dubbo就是阿裡所開源出來的一套RPC架構。
RPC和 HTTP、TCP的關系就是:RPC是基于HTTP、TCP協定來傳輸資料的,對于所傳輸的資料,可以交由RPC的雙方來協商定義,但基本都會包括:
- 調用的是哪個類或接口
- 調用的是哪個方法,方法名和方法參數類型(考慮方法重載)
- 調用方法的入參
1.2 Dubbo與RPC的關系
上面說到實作RPC架構需要定義 資料類型 和 傳輸協定。而Dubbo作為阿裡開源出來的RPC架構,已經制定好了對應的 傳輸資料類型 和傳輸協定,使用Dubbo必須遵循Dubbo制定好的規則。
Dubbo的傳輸協定見下文!
3. 自定義RPC架構思路
服務端:
-
注冊服務到zk或redis。以map的形式儲存起來,key = 服務名,value = List<伺服器位址>。用戶端請求可以負載到value的某個位址上。
注意:如果隻把服務放在本地緩存中,那麼其他的服務将調用失敗,因為不同的服務屬于不同的jvm,其他服務将無法感覺另一個服務中的本地緩存。
- 把服務和服務的實作類注冊到本地緩存。以map的形式儲存起來,key = 服務名,value = 服務的實作類。目的是:當服務端接受到用戶端請求,可以根據用戶端傳來的接口名,從本地緩存中拿到其實作類,然後通過反射調用用戶端想要調用的方法
- 根據不同的協定啟動不同的伺服器。如果是Http協定則啟動Tomcat,如果是Dubbo協定則啟動Netty。
用戶端:
- 指定傳輸的資料類型,包括接口(服務)名、方法名、參數類型、參數名,并封裝成一個類Invocation。
-
當用戶端調用某個接口時,采用jdk動态代理的方式,調用invoke代理方法,在代理方法中做增強邏輯。邏輯如下:
2.1:填充資料類型Invocation
2.2:從zk或redis中根據服務名拉取伺服器位址,并負載均衡到某一個伺服器位址下
2.3:擷取用戶端協定(dubbo 或 http),并根據協定向服務端發送資料Invocation
- 用戶端DispartchServlet攔截到用戶端發過來的請求。通過JSON序列化二進制資料為Invocation 對象。根據對象中的接口名,從本地緩存中拿到對應的實作類,利用反射調用用戶端用戶端想要調用的方法,并輸出。完成了遠端服務調用!
2. Dubbo的基本使用
首先附上dubbo官方使用文檔:https://dubbo.apache.org/zh/docs/v2.7/user/examples/loadbalance/
2.1 Dubbo是什麼?
Apache Dubbo 是一款高性能、輕量級的開源 Java 服務架構,提供了六大核心能力:面向接口代理的高性能RPC調用,智能容錯和負載均衡,服務自動注冊和發現,高度可擴充能力,運作期流量排程,可視化的服務治理與運維。
其中有以下幾個關鍵點
-
:Dubbo使用zookeeper做服務的注冊中心,就是服務的提供者以臨時節點的形式将服務Server資訊注冊儲存到Zookeeper的dubbo目錄下的provider的節點下,供消費者發現調用。注冊與發現
-
: Dubbo支援負載均衡政策,就是同一個Dubbo服務被多台伺服器啟用後,會在在Zookeeper提供者節點下顯示多個相同接口名稱節點。消費者在調用Dubbo負載均衡服務時,采用權重的算法政策選擇具體某個伺服器上的服務,權重政策以*2倍數設定。負載均衡
-
:Dubbo的提供者在Zookeeper上使用的是臨時節點,一旦提供者所在服務挂掉,該節點的客服端連接配接将會關閉,故節點自動消失。是以消費者調用接口時将不會輪詢到已經挂掉的接口上(延遲例外)。容錯機制
-
:Dubbo在java jvm中有自己的容器,和Spring IOC的bean一樣,将服務對象儲存到自己的容器中。Dubbo容器
-
:監控中心主要是用來服務監控和服務治理。服務治理包含:負載均衡政策、服務狀态、容錯、路由規則限定、服務降級等。具體可以下載下傳Dubbo監控中心用戶端檢視與設定。監控中心
-
:點選連結擷取更多協定的詳細資訊Dubbo的協定
①:dubbo協定: Dubbo預設協定是dubbo協定,采用單一長連接配接和 NIO 異步通訊,基于hessian作為序列化協定,适合于資料量小但并發高的服務調用,以及服務消費者機器數遠大于服務提供者機器數的情況。
②:hessian協定: Hessian底層采用Http通訊(同步),走hessian序列化協定。适用于提供者數量比消費者數量還多,适用于檔案的傳輸,一般較少用
③:http協定: 走json序列化,适用于浏覽器檢視,同時給應用程式和浏覽器JS使用的服務。
④:rmi協定:走java二進制序列化,多個短連接配接,适合消費者和提供者數量差不多,适用于檔案的傳輸,一般較少用
⑤:webservice協定:采用SOAP文本序列化,适用HTTP傳輸,常用于系統內建,跨語言調用
⑥:redis協定:基于 Redis實作的 RPC 協定。
⑦:rest協定:基于标準的Java REST API實作的REST調用支援
2.2 負載均衡
生産者在為某個接口暴露服務時,可以根據協定、ip、端口号、服務、group、version等六要素暴露多個接口執行個體,達到類似于叢集的形式。如下所示:任意修改某個要素就算是這個接口已暴露的執行個體!在代碼中可以通過修改@Service注解的值來暴露不同的服務執行個體
@Service(interfaceName = "com.tuling.DemoService", version = "generic")
這樣就會暴露
http://ip:port/DemoService + generic
服務,消費時要根據生産者暴露的規則來進行消費。
如果在
application.properties
配置檔案中,配置了多個協定,Dubbo會預設會根據配置暴露多個服務執行個體,如果做下面的配置,那麼上面的DemoService接口在zookeeper上就會有兩個服務執行個體,一個Http的,一個Dubbo的!
# 配置多協定
# dubbo協定
dubbo.protocols.p1.id=dubbo1
dubbo.protocols.p1.name=dubbo
dubbo.protocols.p1.port=20881
dubbo.protocols.p1.host=0.0.0.0
# http協定
dubbo.protocols.p2.id=dubbo2
dubbo.protocols.p2.name=http
dubbo.protocols.p2.port=20882
dubbo.protocols.p2.host=0.0.0.0
那麼面對多個服務執行個體,消費端調用時是如何進行選擇的呢?Dubbo為我們提供了四種負載均衡政策,可以通過負載均衡政策來選擇一個服務執行個體進行調用!預設的負載政策為
random
随機調用。四種政策如下:
-
:按權重設定随機機率,可通過配置權重修改機率Random 随機
-
:存在慢的提供者累積請求的問題,比如:第二台機器很慢,但沒挂,當請求調到第二台時就卡在那,久而久之,所有請求都卡在調到第二台上。RoundRobin 輪詢
-
:活躍數是指調用前後的計數差,服務調用越快,活躍數越小。提供者越慢,接收的請求就越少,因為越慢的提供者的調用前後計數差會越大,活躍數也會變大LeastActive 最少活躍數
-
:相同參數的請求總是發到同一提供者。ConsistentHash 一緻性Hash
注意:比較難了解的是LeastActive 最少活躍數,理論上最少活躍數應該是在服務提供者端進行統計的,服務提供者統計有多少個請求正在執行中。但是Dubbo卻選擇
在消費端進行統計最少活躍數
,為什麼能在消費端進行統計?邏輯如下:
- 消費者會緩存所調用服務的所有提供者,比如記為p1、p2、p3三個服務提供者,每個提供者内都有一個屬性記為active,預設位0
- 消費者在調用次服務時,如果負載均衡政策是leastactive
- 消費者端會判斷緩存的所有服務提供者的active,選擇最小的,如果都相同,則随機選出某一個服務提供者後,假設位p2,Dubbo就會對p2.active+1
- 然後真正送出請求調用該服務
- 消費端收到響應結果後,對p2.active-1
- 這樣就完成了對某個服務提供者目前活躍調用數進行了統計,并且并不影響服務調用的性能,下次調用會再次判斷最小的active,這就解釋了為什麼服務提供者越慢,接收的請求就越少!因為它的active值大!
配置方式
- Provider端配置:生産者通過在暴露服務的@Servic注解上進行配置:
,配置時需要注意負載均衡方式均為小寫!@Service(loadbalance = "roundrobin")
- Consumer端配置:消費端通過
@Reference(loadbalance = "leastactive ")
如果Provider和Consumer都配置,則以Consumer端配置的為準!
2.3 服務逾時
在服務提供者(服務端)和服務消費者上都可以配置服務逾時時間,這兩者是不一樣的。
@Service(version = "timeout", timeout = 4000) //服務提供者端逾時時間
@Reference(version = "timeout", timeout = 3000,retries = 1) //服務消費者端逾時時間
消費者調用一個服務,分為三步:
- 消費者發送請求(網絡傳輸)
- 服務端執行服務
- 服務端傳回響應(網絡傳輸)
如果在服務端和消費端隻在其中一方配置了timeout
那麼沒有歧義,表示消費端調用服務的逾時時間,消費端如果超過時間還沒有收到響應結果,則消費端會抛逾時異常,但,服務端不會抛異常,服務端在執行服務後,會檢查執行該服務的時間,如果超過timeout,則會列印一個逾時日志。服務會正常的執行完。
如果在服務端和消費端各配了一個timeout
那情況就比較複雜了,假設
- 服務執行為5s
- 消費端timeout=3s
- 服務端timeout=6s
那麼消費端調用服務時,消費端會收到逾時異常(因為消費端逾時了),服務端一切正常(服務端沒有逾時)。
如果
配置服務端timeout=4s,那麼由于服務執行為5s,是以服務端也會列印警告,辨別服務端也逾時了!
2.4 叢集容錯
一個服務提供多個執行個體(叢集),叢集容錯是指:叢集容錯表示:服務消費者在調用某個服務時,這個服務有多個服務提供者,在經過負載均衡後選出其中一個服務提供者之後進行調用,但調用報錯後,Dubbo所采取的後續處理政策。如圖:如果服務執行個體1調用失敗,則會嘗試調用服務執行個體2或者3,預設重試2次。
叢集容錯可以在@Service 和 @Reference注解上進行配置:如果兩者都配置,以消費端為主!
@Service( cluster = "failfast") //服務端超叢集容錯
@Reference(cluster = "failfast") //消費端叢集容錯
Dubbo提供了六種叢集容錯方案:
-
當出現失敗,重試其它伺服器。通常用于讀操作,但重試會帶來更長延遲。可通過 retries=“2” 來設定重試次數(不含第一次)。Failover Cluster:失敗自動切換
-
隻發起一次調用,失敗立即報錯。通常用于非幂等性的寫操作,比如新增記錄Failfast Cluster:快速失敗
-
出現異常時,不抛異常,直接忽略。通常用于寫入審計日志等操作Failsafe Cluster:失敗安全
-
背景記錄失敗請求,定時重發。通常用于消息通知操作Failback Cluster:失敗自動恢複
-
隻要一個成功即傳回。通常用于實時性要求較高的讀操作,但需要浪費更多服務資源。可通過 forks=“2” 來設定最大并行數Forking Cluster:并行調用多個伺服器
-
逐個調用,任意一台報錯則報錯。通常用于通知所有提供者更新緩存或日志等本地資源資訊Broadcast Cluster:廣播調用所有提供者
2.5 服務降級
服務降級表示:服務消費者在調用某個服務提供者時,如果該服務提供者報錯了,所采取的措施。可以通過服務降級功能臨時屏蔽某個出錯的非關鍵服務,并定義降級後的傳回政策。叢集容錯和服務降級的差別在于:
- 叢集容錯是整個叢集範圍内的容錯
- 服務降級是單個服務提供者的自身容錯
服務降級可以在消費端的 @Reference注解上使用mock來指定降級方案
-
表示消費方對該服務的方法調用都直接傳回 null 值,不發起遠端調用。用來屏蔽不重要服務不可用時對調用方的影響。mock=force:return+null
-
表示消費方對該服務的方法調用在失敗後,再傳回 null 值,不抛異常。用來容忍不重要服務不穩定時對調用方的影響。mock=fail:return+null
//服務降級:如果調用失敗傳回123
@Reference(version = "timeout", timeout = 1000, mock = "fail: return 123")
更多服務降級方案可參考本地僞裝:https://dubbo.apache.org/zh/docs/v2.7/user/examples/local-mock/
本地僞裝其實也是對Mock的應用,便于服務端在用戶端執行容錯邏輯
2.6 本地存根
消費端通過Dubbo遠端調用服務端,其業務實作基本都在服務端。但有些時候想在消費端也執行部分邏輯,比如:做
ThreadLocal 緩存(這個用處最大),提前驗證參數,調用失敗後僞造容錯資料
等等,此時就需要在@Reference中帶上 Stub,消費端生成服務的代理 Proxy 執行個體,會把 Proxy 通過構造函數傳給 Stub,然後把 Stub 暴露給使用者,Stub 可以決定要不要去調 Proxy。
//本地存根,開啟stub
@Reference(stub = "true")
或者
@Reference(stub = "com.foo.DemoServiceStub") //指定stub對象
還需要自定義一個類實作DemoService接口,表示為DemoService做的本地存根,這個類是放在消費端的
public class DemoServiceStub implements DemoService {
private final DemoService demoService;
// 構造函數傳入真正的遠端代理對象
public DemoServiceStub(DemoService demoService){
this.demoService = demoService;
}
@Override
public String sayHello(String name) {
// 此代碼在用戶端執行, 你可以在用戶端做ThreadLocal本地緩存,或預先驗證參數是否合法,等等
try {
return demoService.sayHello(name); // safe null
} catch (Exception e) {
// 你可以容錯,可以做任何AOP攔截事項
return "容錯資料";
}
}
}
注意:
實作類中必須有一個傳入遠端 DemoService 執行個體的構造函數
使用上述存根代碼執行後,如果調用失敗,則會執行DemoServiceStub中的容錯方案,控制台列印”容錯資料“!
2.7 參數回調
參數回調是指:當消費端調用服務成功後,希望服務端能夠回調一下消費端的邏輯
既然是服務端回調消費端的邏輯,那麼這個邏輯一定是存在消費端的!以DemoService服務為例
消費端調用:
@Reference(version = "callback")
private DemoService demoService;
//調用服務
demoService.sayHello("aaa", "d1", new DemoServiceListenerImpl())
上述代碼
new DemoServiceListenerImpl()
中要包含着具體的回調邏輯
// 回調邏輯接口
public interface DemoServiceListener {
void changed(String msg);
}
// 回調邏輯實作類
public class DemoServiceListenerImpl implements DemoServiceListener {
@Override
public void changed(String msg) {
System.out.println("被回調了:"+msg);
}
}
服務端回調
// DemoService接口
public interface DemoService {
// 回調方法
default String sayHello(String name, String key, DemoServiceListener listener) {
return null;
};
}
// @Method注明了是sayHello()中索引為2的參數參與了回調,以及最大同時支援3個回調,上述代碼隻有一個,如果寫4個就報錯
@Service(version = "callback", methods = {@Method(name = "sayHello", arguments = {@Argument(index = 2, callback = true)})}, callbacks = 3)
public class CallBackDemoService implements DemoService {
@Override
public String sayHello(String name, String key, DemoServiceListener callback) {
callback.changed(); //代理對象直接回調消費端的changed方法
return ""; // 正常通路
}
}
在服務端回調時,需要注意:
-
方法中的DemoServiceListener為代理對象,并不是消費端傳過來的DemoServiceListenerImpl對象sayHello()
- 需要在
中使用@Service
注明是哪個方法中哪個參數參與了回調,以及最大同時支援幾個回調@Method
結果:
在消費端列印的是changed的内容,但這個方法是在服務端被執行的
2.8 異步調用
上文所講的内容都是依托于同步調用的,Dubbo也提供了異步調用方式,異步調用與同步調用的求别在于:
- 服務端需要使用
開啟一個線程執行任務CompletableFuture.supplyAsync
- 用戶端需要使用
監聽異步線程執行完畢CompletableFuture.whenComplete
消費端代碼示例:
@Reference(version = "async")
private DemoService demoService;
public static void main(String[] args) throws IOException {
ConfigurableApplicationContext context = SpringApplication.run(AsyncDubboConsumerDemo.class);
DemoService demoService = context.getBean(DemoService.class);
// 調用直接傳回CompletableFuture
CompletableFuture<String> future = demoService.sayHelloAsync("異步調用"); // 5
//這個方法隻有等異步線程執行結束才會調用
future.whenComplete((v, t) -> {
if (t != null) {
t.printStackTrace();
} else {
System.out.println("Response: " + v);
}
});
System.out.println("結束了");
}
服務端代碼示例
public interface DemoService {
// 同步調用方法
String sayHello(String name);
// 異步調用方法
default CompletableFuture<String> sayHelloAsync(String name) {
return null;
};
}
@Service(version = "async")
public class AsyncDemoService implements DemoService {
//同步調用
@Override
public String sayHello(String name) {
System.out.println("sayhello方法 " + name);
return name;
}
// 主要關注這個異步調用
@Override
public CompletableFuture<String> sayHelloAsync(String name) {
System.out.println("執行了異步服務" + name);
//相當于在異步線程裡執行sayHello方法!
return CompletableFuture.supplyAsync(() -> {
return sayHello(name);
});
}
}
執行結果如下:
消費端:
服務端:
可以看到他們之間列印的順序也是異步的展現!
2.9 泛化調用、泛化服務
泛化調用: 在Dubbo中,如果某個服務想要支援泛化調用,就可以将該服務的generic屬性設定為true,那對于服務消費者來說,就可以不用依賴該服務的接口,直接利用GenericService接口來進行服務調用。泛化調用可以用來做服務測試。
@EnableAutoConfiguration
public class GenericDubboConsumerDemo {
//調用DemoService服務,并不需要注入DemoService,也不需要引入依賴
@Reference(id = "demoService", version = "default", interfaceName = "com.tuling.DemoService", generic = true)
private GenericService genericService;
public static void main(String[] args) throws IOException {
ConfigurableApplicationContext context = SpringApplication.run(GenericDubboConsumerDemo.class);
GenericService genericService = (GenericService) context.getBean("demoService");
Object result = genericService.$invoke("sayHello", new String[]{"java.lang.String"}, new Object[]{"周瑜"});
System.out.println(result);
}
}
泛化服務: 可以不實作具體的某個接口,而是實作
GenericService
接口,并在
@Service
上标明接口名即可,在調用直接注入DemoService就可以使用!
@Service(interfaceName = "com.tuling.DemoService", version = "generic")
public class GenericDemoService implements GenericService {
@Override
public Object $invoke(String s, String[] strings, Object[] objects) throws GenericException {
System.out.println("執行了generic服務");
return "執行的方法是" + s;
}
}
3. dubbo的REST協定
dubbo支援多種遠端調用方式,例如dubbo RPC(二進制序列化 + tcp協定)、http invoker(二進制序列化 + http協定)、hessian(二進制序列化 + http協定)、WebServices (文本序列化 + http協定)、REST(文本序列化 + http協定)等等的支援。
當我們用Dubbo提供了一個服務後,如果消費者沒有使用Dubbo也想調用服務,那麼這個時候就可以讓我們的服務支援REST協定,這樣消費者就可以通過REST形式調用我們的服務了。更多REST協定内容點選檢視官網!
①:服務端配置檔案修改協定為rest
dubbo.protocol.name=rest
②:服務端實作:使用@Path指定Rest風格的通路路徑(注意:所有暴露的服務都必須加@Path)
@Service(version = "rest")
@Path("demo")
public class RestDemoService implements DemoService {
@GET
@Path("say")
@Produces({ContentType.APPLICATION_JSON_UTF_8, ContentType.TEXT_XML_UTF_8})
@Override
public String sayHello(@QueryParam("name") String name) {
System.out.println("執行了rest服務" + name);
URL url = RpcContext.getContext().getUrl();
return String.format("%s: %s, Hello, %s", url.getProtocol(), url.getPort(), name); // 正常通路
}
}
這樣就可以通過浏覽器通路這個服務了,其他消費端也可以直接通過HttpCliet等非dubbo的形式去調用服務!
4. dubbo的控制台
5. dubbo的服務路由
經過服務路由可以配置黑名單、白名單、讀寫分離、隔離不同機房網段等等,這點在官網有很詳細的解釋 點選檢視官網詳情!!!
dubbo提供的标簽路由還可以用來釋出版本,什麼是藍綠釋出、灰階釋出?