Dubbo 采用全 Spring 配置方式,透明化接入應用,對應用沒有任何 API 侵入,隻需用 Spring 加載 Dubbo 的配置即可。本文列舉了 Dubbo 的一些常見的使用場景:例如負載均衡,叢集容錯,逾時等。
github 位址:
https://github.com/cr7258/dubbo-lab/tree/master/dubbo-tuling-demo配置檔案
配置檔案使用 properties 或者 yaml 格式都可以。
服務端配置檔案
# Spring boot application
spring.application.name=dubbo-provider-demo
server.port=8081
# Base packages to scan Dubbo Component: @org.apache.dubbo.config.annotation.Service
dubbo.scan.base-packages=com.tuling.provider.service
dubbo.application.name=${spring.application.name}
## Dubbo Registry
dubbo.registry.address=zookeeper://127.0.0.1:2181
# Dubbo Protocol
dubbo.protocols.p1.id=dubbo1
dubbo.protocols.p1.name=dubbo
dubbo.protocols.p1.port=20881
dubbo.protocols.p1.host=0.0.0.0
dubbo.protocols.p2.id=dubbo2
dubbo.protocols.p2.name=dubbo
dubbo.protocols.p2.port=20882
dubbo.protocols.p2.host=0.0.0.0
dubbo.protocols.p3.id=dubbo3
dubbo.protocols.p3.name=dubbo
dubbo.protocols.p3.port=20883
dubbo.protocols.p3.host=0.0.0.0
# REST Protocol
dubbo.protocols.p4.id=rest1
dubbo.protocols.p4.name=rest
dubbo.protocols.p4.port=8083
dubbo.protocols.p4.host=0.0.0.0
消費端配置檔案
spring:
application:
name: dubbo-consumer-demo
server:
port: 8082
dubbo:
registry:
address: zookeeper://127.0.0.1:2181
Zookeeper 配置
zookeeper 下載下傳連結:
https://zookeeper.apache.org/releases.html解壓後,進入目錄,使用如下指令啟動:
bin/zkServer.sh start
消費端服務注冊
服務端有 6 個實作類實作了 DemoService 接口:
啟動服務端,在 zookeeper 上可以看到服務端總共注冊了 3 * 6 = 18 個服務。 3 是 在 application.properties 中配置了 3 個 dubbo 服務的端口,6是 Provider 有 6 個 DemoService 的實作類。
以下介紹的負載均衡,服務逾時等特性既可以在服務端配置,也可以在消費端配置,如果兩邊都配置了,以消費端的為準。
負載均衡
在叢集負載均衡時,Dubbo 提供了多種均衡政策,預設為 random 随機調用。 Dubbo 支援以下負載均衡政策:
- Random LoadBalance 随機,按權重設定随機機率。 在一個截面上碰撞的機率高,但調用量越大分布越均勻,而且按機率使用權重後也比較均勻,有利于動态調整提供者權重。
- RoundRobin LoadBalance 輪詢,按公約後的權重設定輪詢比率。 存在慢的提供者累積請求的問題,比如:第二台機器很慢,但沒挂,當請求調到第二台時就卡在那,久而久之,所有請求都卡在調到第二台上。
- LeastActive LoadBalance 最少活躍調用數,相同活躍數的随機,活躍數指調用前後計數差。 使慢的提供者收到更少請求,因為越慢的提供者的調用前後計數差會越大。
- ConsistentHash LoadBalance 一緻性 Hash,相同參數的請求總是發到同一提供者。 當某一台提供者挂時,原本發往該提供者的請求,基于虛拟節點,平攤到其它提供者,不會引起劇烈變動。
輪詢算法
依次按順序輪詢請求後端服務。
package com.tuling.consumer;
import com.tuling.DemoService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import java.io.IOException;
/**
* 負載均衡示例消費端
*/
@EnableAutoConfiguration
public class LoadBalanceDubboConsumerDemo {
// 輪詢算法測試
@Reference(version = "default", loadbalance = "roundrobin")
private DemoService demoService;
public static void main(String[] args) throws IOException {
ConfigurableApplicationContext context = SpringApplication.run(LoadBalanceDubboConsumerDemo.class);
DemoService demoService = context.getBean(DemoService.class);
// 輪詢算法測試
for (int i = 0; i < 1000; i++) {
System.out.println((demoService.sayHello("chengzw")));
try {
Thread.sleep(1 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
請求結果,按順序輪詢請求每一個後端服務:
dubbo:20882, Hello, chengzw
dubbo:20881, Hello, chengzw
dubbo:20883, Hello, chengzw
dubbo:20882, Hello, chengzw
dubbo:20881, Hello, chengzw
dubbo:20883, Hello, chengzw
dubbo:20882, Hello, chengzw
dubbo:20881, Hello, chengzw
dubbo:20883, Hello, chengzw
一緻性 Hash 算法
一緻性 Hash,相同參數的請求總是發到同一提供者。 當某一台提供者挂時,原本發往該提供者的請求,基于虛拟節點,平攤到其它提供者,不會引起劇烈變動。
package com.tuling.consumer;
import com.tuling.DemoService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import java.io.IOException;
/**
* 負載均衡示例消費端
*/
@EnableAutoConfiguration
public class LoadBalanceDubboConsumerDemo {
// 一緻性hash算法測試
@Reference(version = "default", loadbalance = "consistenthash")
private DemoService demoService;
public static void main(String[] args) throws IOException {
ConfigurableApplicationContext context = SpringApplication.run(LoadBalanceDubboConsumerDemo.class);
DemoService demoService = context.getBean(DemoService.class);
// 一緻性hash算法測試
for (int i = 0; i < 1000; i++) {
System.out.println((demoService.sayHello(i%5+"chengzw")));
try {
Thread.sleep(1 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
請求結果,可以看到相同參數的請求都是發往同一個服務:
dubbo:20881, Hello, 0chengzw
dubbo:20882, Hello, 1chengzw
dubbo:20882, Hello, 2chengzw
dubbo:20883, Hello, 3chengzw
dubbo:20882, Hello, 4chengzw
dubbo:20881, Hello, 0chengzw
dubbo:20882, Hello, 1chengzw
dubbo:20882, Hello, 2chengzw
dubbo:20883, Hello, 3chengzw
dubbo:20882, Hello, 4chengzw
最小連接配接數
Dubbo 的最少活躍數是在消費者提供者端進行統計的,邏輯如下:
- 消費者會緩存所調用服務的所有提供者,比如記為 p1、p2、p3 三個服務提供者,每個提供者内都有一個屬性記為 active,預設位 0。
- 消費者在調用次服務時,如果負載均衡政策是leastactive,消費者端會判斷緩存的所有服務提供者的 active,選擇最小的,如果都相同,則随機。
- 選出某一個服務提供者後,假設為 p2,Dubbo 就會對 p2.active+1 然後真正送出請求調用該服務。
- 消費端收到響應結果後,對 p2.active-1。
- 這樣就完成了對某個服務提供者目前活躍調用數進行了統計,并且并不影響服務調用的性能。
逾時
在服務提供者和服務消費者上都可以配置服務逾時時間,這兩者是不一樣的。 消費者調用一個服務,分為三步:
- 消費者發送請求(網絡傳輸)
- 服務端執行服務
- 服務端傳回響應(網絡傳輸)
如果在服務端和消費端隻在其中一方配置了 timeout,那麼沒有歧義,表示消費端和服務端的逾時時間,消費端如果超過時間還沒有收到響應結果,則消費端會抛逾時異常,但是服務端不會抛異常,服務端在執行服務後,會檢查執行該服務的時間,如果超過 timeout,則會列印一個逾時日志,服務會正常的執行完。
如果在服務端和消費端各配了一個timeout,那就比較複雜了,假設
- 服務執行為5s
- 消費端timeout=3s
- 服務端timeout=6s
那麼消費端調用服務時,消費端會收到逾時異常(因為消費端逾時了),服務端一切正常(服務端沒有逾時)。逾時用戶端預設會重試 2 次,加上第 1 次調用,總共會有 3 次請求。
服務端代碼
package com.tuling.provider.service;
import com.tuling.DemoService;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.config.annotation.Service;
import org.apache.dubbo.rpc.RpcContext;
import java.util.concurrent.TimeUnit;
/**
* 逾時示例服務端
*/
@Service(version = "timeout", timeout = 6000, protocol = {"p1", "p2", "p3"})
public class TimeoutDemoService implements DemoService {
@Override
public String sayHello(String name) {
System.out.println("執行了timeout服務" + name);
// 服務執行5秒
// 服務逾時時間為3秒,但是執行了5秒,服務端會把任務執行完的
// 服務的逾時時間,是指如果服務執行時間超過了指定的逾時時間則會抛一個warn(例如把修改timeout = 4000)
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行結束" + name);
URL url = RpcContext.getContext().getUrl();
return String.format("%s:%s, Hello, %s", url.getProtocol(), url.getPort(), name); // 正常通路
}
}
消費端代碼
package com.tuling.consumer;
import com.tuling.DemoService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import java.io.IOException;
/**
* 逾時示例消費端
*/
@EnableAutoConfiguration
public class TimeoutDubboConsumerDemo {
@Reference(version = "timeout", timeout = 3000)
private DemoService demoService;
public static void main(String[] args) throws IOException {
ConfigurableApplicationContext context = SpringApplication.run(TimeoutDubboConsumerDemo.class);
DemoService demoService = context.getBean(DemoService.class);
// 服務調用逾時時間為1秒,預設為3秒
// 如果這1秒内沒有收到服務結果,則會報錯
System.out.println((demoService.sayHello("chengzw"))); //xxservestub
}
}
檢視輸出,用戶端抛出了逾時異常:
在叢集調用失敗時,Dubbo 提供了多種容錯方案,預設為 failover 重試。 dubbo 叢集容錯政策如下:
- Failover Cluster 失敗自動切換,當出現失敗,重試其它伺服器。通常用于讀操作,但重試會帶來更長延遲。可通過 retries="2" 來設定重試次數(不含第一次)。
- Failfast Cluster 快速失敗,隻發起一次調用,失敗立即報錯。通常用于非幂等性的寫操作,比如新增記錄。
- Failsafe Cluster 失敗安全,出現異常時,直接忽略。通常用于寫入審計日志等操作。
- Failback Cluster 失敗自動恢複,背景記錄失敗請求,定時重發。通常用于消息通知操作。
- Forking Cluster 并行調用多個伺服器,隻要一個成功即傳回。通常用于實時性要求較高的讀操作,但需要浪費更多服務資源。可通過 forks="2" 來設定最大并行數。
- Broadcast Cluster 廣播調用所有提供者,逐個調用,任意一台報錯則報錯。通常用于通知所有提供者更新緩存或日志等本地資源資訊。
本例使用 Failfast Cluster 模式,隻發起一次調用,失敗立即報錯。
package com.tuling.consumer;
import com.tuling.DemoService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import java.io.IOException;
/**
* 叢集容錯示例消費端
*/
@EnableAutoConfiguration
public class ClusterDubboConsumerDemo {
@Reference(version = "timeout", timeout = 1000, cluster = "failfast")
private DemoService demoService;
public static void main(String[] args) throws IOException {
ConfigurableApplicationContext context = SpringApplication.run(ClusterDubboConsumerDemo.class);
DemoService demoService = context.getBean(DemoService.class);
System.out.println((demoService.sayHello("chengzw")));
}
}
檢視消費端日志,可以看到請求失敗一次立即報錯,而不是和前面逾時的例子中一樣還重試 2 次:
服務降級表示:服務消費者在調用某個服務提供者時,如果該服務提供者報錯了,所采取的措施。 叢集容錯和服務降級的差別在于:
- 叢集容錯是整個叢集範圍内的容錯。
- 服務降級是單個服務提供者的自身容錯。
可以通過服務降級功能臨時屏蔽某個出錯的非關鍵服務,并定義降級後的傳回政策:
- mock=force:return+null 表示消費方對該服務的方法調用都直接傳回 null 值,不發起遠端調用。用來屏蔽不重要服務不可用時對調用方的影響。
- 還可以改為 mock=fail:return+null 表示消費方對該服務的方法調用在失敗後,再傳回 null 值,不抛異常。用來容忍不重要服務不穩定時對調用方的影響。
package com.tuling.consumer;
import com.tuling.DemoService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import java.io.IOException;
/**
* 服務降級示例消費端
*/
@EnableAutoConfiguration
public class MockDubboConsumerDemo {
//如果消費者調用服務端失敗,不抛出異常,而是傳回 123
@Reference(version = "timeout", timeout = 1000, mock = "fail: return 123")
private DemoService demoService;
public static void main(String[] args) throws IOException {
ConfigurableApplicationContext context = SpringApplication.run(MockDubboConsumerDemo.class);
DemoService demoService = context.getBean(DemoService.class);
System.out.println((demoService.sayHello("chengzw")));
}
}
本地存根,名字很抽象,但實際上不難了解,本地存根就是一段邏輯,這段邏輯是在服務消費端執行的,這段邏輯一般都是由服務提供者提供,服務提供者可以利用這種機制在服務消費者遠端調用服務提供者之前或之後再做一些其他事情,比如結果緩存,請求參數驗證,錯誤處理等等。
本地存根(Stub) 比 前面的 Mock(服務降級) 功能更強大。
下面示例在消費端調用 sayHello() 方法時,實際上是調用 DemoServiceStub 類的 sayHello() 方法。 消費端代碼
package com.tuling.consumer;
import com.tuling.DemoService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import java.io.IOException;
/**
* 本地存根示例消費端
*/
@EnableAutoConfiguration
public class StubDubboConsumerDemo {
//寫法一:
//@Reference(version = "timeout", timeout = 1000, stub = "com.tuling.DemoServiceStub")
//寫法二:會用 demoService 的類全名 com.tuling.DemoService 拼接上 Stub,然後去找這個類
//隻要這個類在消費端的classpath中能找到就行
@Reference(version = "timeout", timeout = 1000, stub = "true")
private DemoService demoService;
public static void main(String[] args) throws IOException {
ConfigurableApplicationContext context = SpringApplication.run(StubDubboConsumerDemo.class);
DemoService demoService = context.getBean(DemoService.class);
System.out.println((demoService.sayHello("chengzw")));
}
}
實作類代碼
package com.tuling;
/**
* 本地存根示例真正的實作類
*/
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 "容錯資料";
}
}
}
檢視消費端日志,出現錯誤時會實行 try catch 的邏輯:
Dubbo的REST也是Dubbo所支援的一種協定。
當我們用 Dubbo 提供了一個服務後,如果消費者沒有使用 Dubbo 也想調用服務,那麼這個時候我們就可以讓我們的服務支援 REST 協定,這樣消費者就可以通過 REST 形式調用我們的服務了。
package com.tuling.provider.service;
import com.tuling.DemoService;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.config.annotation.Service;
import org.apache.dubbo.rpc.RpcContext;
import org.apache.dubbo.rpc.protocol.rest.support.ContentType;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
/**
* REST示例服務端
*/
@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); // 正常通路
}
}
用戶端通路 rest 服務暴露的位址 + @Path 的路徑來通路: