本文介紹如何利用dubbo自定義負載實現簡單灰度(用戶緯度,部分用戶訪問一個服務,其余訪問剩余服務)。
其實在這之前,對dubbo了解的也不是很多,只是簡單的使用過,跑了幾個demo而已,但是得知接下來可能要用dubbo進行開發,還是趕緊補了一下相關的知識,看了看官網,另外買了一本書《深入理解Apache Dubbo實戰》,看了一大半,感覺還是很不錯的。
1.dubbo負載均衡介紹
因為官網介紹的很詳細了,這里只簡單的說一下。dubbo負載均衡包含如下四種:
- RandomLoadBalance:默認的負載策略,隨機負載。
- ConsistentHashLoadBalance:一致性 Hash負載。
- LeastActiveLoadBalance:最少活躍數負載。
- RoundRobinLoadBalance:輪詢負載。
可以查看官方:http://dubbo.apache.org/zh-cn/docs/user/demos/loadbalance.html
這四個類都繼承了AbstractLoadBalance抽象類,源碼相關分析可以查看官方:http://dubbo.apache.org/zh-cn/docs/source_code_guide/loadbalance.html
2.springboot-dubbo實現自定義負載方法
springboot-dubbo使用自定義負載其實很簡單,大致分為如下幾步:
- 1.創建自定義負載類,繼承AbstractLoadBalance,重寫doSelect方法,這個方法就是定義算法規則的地方。
- 2.添加dubbo負載擴展點,在src/main/resources目錄下創建META-INFO/dubbo目錄,在目錄下創建org.apache.dubbo.rpc.cluster.LoadBalance文件,里面配置對應的負載算法類,如下:
gray=com.dalaoyang.balance.GrayLoadBalance
- 3.配置文件中使用,如下:
dubbo.provider.loadbalance=gray
3.模擬灰度方案及具體實現
3.1 灰度場景
現在模擬一個這樣的方案,比如有4個服務提供者,端口分別是9001,9002,9003,9004,將其中9002端口的服務設置為灰度服務,當請求消費者接口testUser的userid為1-10時,強制轉發到到灰度狀態的提供者去,其余的還是請求到正常的服務,如圖所示。
3.2 代碼實現
接下來使用代碼簡單實現如上場景。
3.2.1 服務提供者
首先看一下pom文件,都是一些springboot-dubbo的依賴,如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.dalaoyang</groupId>
<artifactId>springboot_dubbo_provider</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot_dubbo_provider</name>
<description>springboot_dubbo_provider</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Aapche Dubbo -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.1</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>2.7.2</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring</artifactId>
</exclusion>
<exclusion>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.8.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.curator/curator-recipes -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<exclusions>
<exclusion>
<artifactId>log4j</artifactId>
<groupId>log4j</groupId>
</exclusion>
<exclusion>
<artifactId>slf4j-log4j12</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
<version>3.4.10</version>
<type>pom</type>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
然后暴露一個接口,供服務消費者使用,如下:
package com.dalaoyang.api;
public interface UserService {
String testUser(Long userId, String version);
}
實現類,接口返回對應的端口,dubbo的端口,如下:
package com.dalaoyang.api.impl;
import com.dalaoyang.api.UserService;
import org.apache.dubbo.config.annotation.Service;
import org.springframework.beans.factory.annotation.Value;
@Service
public class UserServiceImpl implements UserService {
@Value("${server.port}")
private String port;
@Value("${dubbo.protocol.port}")
private String dubboPort;
@Override
public String testUser(Long userId, String version) {
return "調用成功,端口是:" + port +
"。版本號是:" + version +
",用戶id:" + userId +
",dubbo端口:" + dubboPort;
}
}
創建一個GrayLoadBalance繼承AbstractLoadBalance類,其中包含如下配置:
- 當前請求的userId使用的dubbo隱式傳參(也可以選用其他方式)。
- 灰度用戶名單配置在了消費者的配置中
- 服務提供者配置中配置了一個屬性status用於區分是prod服務還是gray服務。
- 沒有匹配對象的話,使用隨機負載策略進行分發。
看完上面的簡介,在看代碼就容易了很多,大致就是取出請求的用戶id和灰度用戶id集合,判斷是否是灰度用戶,如果是,則選擇灰度服務,如下:
package com.dalaoyang.balance;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.cluster.loadbalance.AbstractLoadBalance;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
@Component
public class GrayLoadBalance extends AbstractLoadBalance {
public static final String NAME = "gray";
public GrayLoadBalance() {
}
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
List<Invoker<T>> list = new ArrayList<>();
for (Invoker invoker : invokers) {
list.add(invoker);
}
Map<String, String> map = invocation.getAttachments();
String userId = map.get("userId");
Iterator<Invoker<T>> iterator = list.iterator();
String grayUserIds = url.getParameter("grayUserids", "");
String[] arrs = grayUserIds.split(",");
while (iterator.hasNext()) {
Invoker<T> invoker = iterator.next();
String providerStatus = invoker.getUrl().getParameter("status", "prod");
if (Objects.equals(providerStatus, NAME)) {
if (Arrays.asList(arrs).contains(userId)) {
return invoker;
} else {
iterator.remove();
}
}
}
return this.randomSelect(list, url, invocation);
}
/**
* 重寫了一遍隨機負載策略
*
* @param invokers
* @param url
* @param invocation
* @param <T>
* @return
*/
private <T> Invoker<T> randomSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
int length = invokers.size();
boolean sameWeight = true;
int[] weights = new int[length];
int firstWeight = this.getWeight((Invoker) invokers.get(0), invocation);
weights[0] = firstWeight;
int totalWeight = firstWeight;
int offset;
int i;
for (offset = 1; offset < length; ++offset) {
i = this.getWeight((Invoker) invokers.get(offset), invocation);
weights[offset] = i;
totalWeight += i;
if (sameWeight && i != firstWeight) {
sameWeight = false;
}
}
if (totalWeight > 0 && !sameWeight) {
offset = ThreadLocalRandom.current().nextInt(totalWeight);
for (i = 0; i < length; ++i) {
offset -= weights[i];
if (offset < 0) {
return (Invoker) invokers.get(i);
}
}
}
return (Invoker) invokers.get(ThreadLocalRandom.current().nextInt(length));
}
}
接下來在src/main/resources/META-INF/dubbo下添加org.apache.dubbo.rpc.cluster.LoadBalance新增擴展點,內容如下:
gray=com.dalaoyang.balance.GrayLoadBalance
這里使用了多配置文件來啟動多個服務提供者,主配置文件application.properties內容如下:
spring.profiles.active=test3
dubbo.provider.loadbalance=gray
application-test1.properties內容如下:
##端口號
server.port=9001
## Dubbo配置
dubbo.application.name=dubbo_provider
dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.protocol.name=dubbo
dubbo.protocol.port=9011
dubbo.scan.base-packages=com.dalaoyang
dubbo.provider.version=2.0.0
application-test2.properties內容如下,這里多配置了dubbo.provider.parameters.status=gray屬性用於區分灰度服務:
##端口號
server.port=9002
## Dubbo配置
dubbo.application.name=dubbo_provider
dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.protocol.name=dubbo
dubbo.protocol.port=9012
dubbo.scan.base-packages=com.dalaoyang
dubbo.provider.version=2.0.0
dubbo.provider.parameters.status=gray
application-test3.properties內容如下:
##端口號
server.port=9003
## Dubbo配置
dubbo.application.name=dubbo_provider
dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.protocol.name=dubbo
dubbo.protocol.port=9013
dubbo.scan.base-packages=com.dalaoyang
dubbo.provider.version=2.0.0
application-test4.properties內容如下:
##端口號
server.port=9004
## Dubbo配置
dubbo.application.name=dubbo_provider
dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.protocol.name=dubbo
dubbo.protocol.port=9014
dubbo.scan.base-packages=com.dalaoyang
dubbo.provider.version=2.0.0
到這里,服務提供者就創建完成了。
3.2.2 服務消費者
服務消費者就簡單很多,pom文件除dubbo對應包以外,引入服務提供者的包,如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.dalaoyang</groupId>
<artifactId>springboot_dubbo_consumer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot_dubbo_consumer</name>
<description>springboot_dubbo_consumer</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Aapche Dubbo -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.1</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>2.7.2</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring</artifactId>
</exclusion>
<exclusion>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.dalaoyang</groupId>
<artifactId>springboot_dubbo_provider</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.8.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.curator/curator-recipes -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<exclusions>
<exclusion>
<artifactId>log4j</artifactId>
<groupId>log4j</groupId>
</exclusion>
<exclusion>
<artifactId>slf4j-log4j12</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
<version>3.4.10</version>
<type>pom</type>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
配置文件中配置上述需要的灰度userid名單,如下:
## 端口號
server.port=8881
##Dubbo配置
dubbo.application.name=dubbo_consumer
dubbo.registry.address=zookeeper://localhost:2181
dubbo.scan.base-packages=com.dalaoyang.api
dubbo.consumer.version=2.0.0
dubbo.consumer.parameters.grayUserids=1,2,3,4,5,6,7,8,9,10
dubbo.provider.loadbalance=gray
dubbo.protocol.port=10000
創建一個TestController,編寫一個簡單的測試類,調用dubbo服務,內容如下:
package com.dalaoyang.controller;
import com.dalaoyang.api.UserService;
import org.apache.dubbo.config.annotation.Reference;
import org.apache.dubbo.rpc.RpcContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Objects;
@RestController
public class TestController {
@Reference
private UserService userService;
//灰度用戶 http://localhost:8881/testUser?userId=3333&version=2.0.0
//正常用戶 http://localhost:8881/testUser?userId=10&version=2.0.0
@GetMapping("/testUser")
public String testUser(Long userId, String version) {
RpcContext.getContext().setAttachment("userId", Objects.nonNull(userId) ? userId.toString() : "");
return userService.testUser(userId, version);
}
}
服務消費者到這里也完成了。
4.測試
4.1 啟動項目
- 1.啟動zookeeper
- 2.啟動服務提供者,可以使用idea啟動多服務,也可以打包,分別制定不同配置文件啟動,任何方式都可以。
- 3.服務提供者啟動完成后,啟動服務消費者。
4.2 頁面請求
如果灰度狀態的服務啟動的話,訪問http://localhost:8881/testUser?userId=10&version=2.0.0,如圖所示。
如果灰度狀態的服務沒有啟動,或者userid不在1-10之間的話會顯示如下圖所示。
5.源碼
本文相關源碼全部上傳到了碼雲上,地址是https://gitee.com/dalaoyang/springboot_dubbo