关于SpringCloud中灰度路由的使用 1 灰度路由的简

这是我参与11月更文挑战的第16天,活动详情查看:11月更文挑战

在微服务中, 通常为了高可用, 同一个服务往往采用集群方式部署, 即同时存在几个相同的服务,而灰度的核心就 是路由, 通过我们特定的策略去调用目标服务线路

1 灰度路由的简介

灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度.

关于SpringCloud微服务+nacos的灰度发布实现, 首先微服务中之间的调用通常使用Feign方式和Resttemplate方式(较少使用),因此 , 我们需要指定服务之间的调用, 首先要给各个服务添加唯一标识, 我们可是使用一些特殊的标记, 如版本号version等, 其次,要干预微服务中Ribbon的默认轮询调用机制, 我们需要根据微服务的版本等不同, 来进行调用, 最后, 在服务之间, 需要传递调用链路的信息, 我们可以在请求头中,添加调用链路的信息.

整理思路为:

1 在请求头中添加调用链路信息

2 微服务之间调用时,使用feign拦截器,增强请求头

3 微服务调用选择时,根据指定的策略(如唯一标识版本等)从nacos中获取指定的服务,调用

2 灰度路由的使用

案列

基础服务

一个父服务,一个工具服务

父服务

pom依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
xml复制代码   <parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<!--spring cloud 版本-->
<spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
</properties>

<dependencies>

<!--nacos-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>0.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>1.1.0</version>
</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>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>


<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>


<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>


</dependencies>

工具服务

feign拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码@Slf4j
public class FeignInterceptor implements RequestInterceptor {

/**
* feign接口拦截, 添加上灰度路由请求头
* @param template
*/
@Override
public void apply(RequestTemplate template) {

String header = null;

try {
header = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest().getHeader("gray-route");
if (null == header || header.isEmpty()) {
return;
}
} catch (Exception e) {
log.info("请求头获取失败, 错误信息为: {}", e.getMessage());
}
template.header("gray-route", header);

}
}

灰度路由属性类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
java复制代码@ConfigurationProperties(prefix = "spring.cloud.nacos.discovery.metadata.gray-route", ignoreUnknownFields = false)
@Data
@RefreshScope
public class GrayRouteProp {

/**
* 逗号
*/
public final static String COMMA_SEP = ".";
/**
* 灰度路由
*/
public final static String GRAY_ROUTE = "gray-route";
/**
* 版本
*/
public final static String VERSION = "version";
/**
* 全链路版本
*/
public final static String ALL = "all";
/**
* 用户自定义版本
*/
public final static String CUSTOM = "custom";

/**
* 版本key, 可用于Redis等中存储
*/
public final static String VERSION_KEY = GRAY_ROUTE + COMMA_SEP + VERSION;


/**
* 是否开启灰度路由
*/
private boolean enable = false;
/**
* 本服务的版本
*/
private String version;

/**
* 本服务到下一跳服务的版本路由规则
*/
private RouteProp route;

}

路由属性类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Data
@ConfigurationProperties(prefix = "spring.cloud.nacos.discovery.metadata.gray-route.route", ignoreUnknownFields = false)
@RefreshScope
public class RouteProp {

/**
* 本服务直接调用的所有服务的统一版本号
*/
private String all;

/**
* 指定调用服务的版本 serviceA:v1 表示在调用时只会调用v1版本服务
*/
private Map<String,String> custom;

}

灰度路由规则类(继承ZoneAvoidanceRule类)

微服务在拦截处理后, Ribbon组件会从服务实例列表中获取一个实现进行转发, 且Ribbon默认的规则是ZoneAvoidanceRule类, 我们定义自己的规则, 只需要继承该类,重写choose方法即可.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
java复制代码@Slf4j
public class GrayRouteRule extends ZoneAvoidanceRule {

@Autowired
protected GrayRouteProp grayRouteProperties;

/**
* 参考 {@link PredicateBasedRule#choose(Object)}
*
*/
@Override
public Server choose(Object key) {
// 根据灰度路由规则,过滤出符合规则的服务 this.getServers()
// 再根据负载均衡策略,过滤掉不可用和性能差的服务,然后在剩下的服务中进行轮询 getPredicate().chooseRoundRobinAfterFiltering()
Optional<Server> server = getPredicate()
.chooseRoundRobinAfterFiltering(this.getServers(), key);
return server.isPresent() ? server.get() : null;
}

/**
* 灰度路由过滤服务实例
*
* 如果设置了期望版本, 则过滤出所有的期望版本 ,然后再走默认的轮询 如果没有一个期望的版本实例,则不过滤,降级为原有的规则,进行所有的服务轮询。(灰度路由失效) 如果没有设置期望版本
* 则不走灰度路由,按原有轮询机制轮询所有
*/
protected List<Server> getServers() {
// 获取spring cloud默认负载均衡器
ZoneAwareLoadBalancer lb = (ZoneAwareLoadBalancer) getLoadBalancer();
// 获取本次请求生效的灰度路由规则
RouteProp routeRule = this.getGrayRoute();
// 获取本次请求期望的服务版本号
String version = getDesiredVersion(routeRule, lb.getName());
// 获取所有待选的服务
List<Server> allServers = lb.getAllServers();
if (CollectionUtils.isEmpty(allServers)) {
return new ArrayList<>();
}
// 如果没有设置要访问的版本,则不过滤,返回所有,走原有默认的轮询机制
if (StringUtils.isEmpty(version)) {
return allServers;
}

// 开始灰度规则匹配过滤
List<Server> filterServer = new ArrayList<>();
for (Server server : allServers) {
// 获取服务实例在注册中心上的元数据
Map<String, String> metadata = ((NacosServer) server).getMetadata();
// 如果注册中心上服务的版本标签和期望访问的版本一致,则灰度路由匹配成功
if (null != metadata && version.equals(metadata.get(GrayRouteProp.VERSION_KEY))) {
filterServer.add(server);
}
}
// 如果没有匹配到期望的版本实例服务,为了保证服务可用性,让灰度规则失效,走原有的轮询所有可用服务的机制
if (CollectionUtils.isEmpty(filterServer)) {
log.warn(String.format("没有找到版本version[%s]的服务[%s],灰度路由规则降级为原有的轮询机制!", version,
lb.getName()));
filterServer = allServers;
}
return filterServer;
}

/**
* 获取本次请求 期望的服务版本号
*
* @param routeRule 生效的配置规则
* @param appName 服务名
*/
protected String getDesiredVersion(RouteProp routeRule, String appName) {
// 取路由规则里指定要访问的微服务的版本号
String version = null;
if (routeRule != null) {
if (routeRule.getCustom() != null) {
// 优先取custom里指定版本
version = routeRule.getCustom().get(appName);
} else {
// custom里没有指定就找all里面设置的统一版本
version = routeRule.getAll();
}
}
return version;
}

/**
* 获取设置的灰度路由规则
*/
protected RouteProp getGrayRoute() {
// 确定路由规则(请求头优先,yml配置其次)
RouteProp routeRule;
String route_header = null;

try {
route_header = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest().getHeader(GrayRouteProp.GRAY_ROUTE);
} catch (Exception e) {
log.error("灰度路由从上下文获取路由请求头异常!");
}

if (!StringUtils.isEmpty(route_header)) {//header
routeRule = JSONObject.parseObject(route_header, RouteProp.class);
} else {
// yml配置
routeRule = grayRouteProperties.getRoute();
}
return routeRule;
}

}

业务服务

一个client服务;两个consumer服务,分版本v1和v2;两个provider服务,分版本v1和v2

client服务

Controller控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@RestController
@Slf4j
public class ACliController {

@Autowired
private ConsumerFeign consumerFeign;

@GetMapping("/client")
public String list() {
String info = "我是客户端,8000 ";
log.info(info);
String result = consumerFeign.list();
return JSON.toJSONString(info + result);
}

}

Feign接口

1
2
3
4
5
6
7
8
java复制代码@FeignClient(value = "consumer-a")
public interface ConsumerFeign {

@ResponseBody
@GetMapping("/consumer")
String list();

}

Application启动器

1
2
3
4
5
6
7
java复制代码@SpringBootApplication
@EnableFeignClients({"com.cf.client.feign"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yml复制代码server:
port: 8000
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # 配置nacos 服务端地址
namespace: public
metadata:
# gray-route是灰度路由配置的开始
gray-route:
enable: true
version: v1
application:
name: client-test # 服务名称

pom依赖

1
2
3
4
5
6
7
8
xml复制代码  <!--自定义commons工具包-->
<dependencies>
<dependency>
<groupId>com.cf</groupId>
<artifactId>commons</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>

consumer1服务

Controller控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@RestController
@Slf4j
public class AConController {

@Autowired
private ProviderFeign providerFeign;

@GetMapping("/consumer")
public String list() {
String info = "我是consumerA,8081 ";
log.info(info);
String result = providerFeign.list();
return JSON.toJSONString(info + result);
}

}

Feign接口

1
2
3
4
5
6
7
8
java复制代码@FeignClient(value = "provider-a")
public interface ProviderFeign {

@ResponseBody
@GetMapping("/provider")
String list();

}

Application启动类

1
2
3
4
5
6
7
8
9
10
java复制代码@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients({"com.cf.consumer.feign"})
public class AConsumerApplication {

public static void main(String[] args) {
SpringApplication.run(AConsumerApplication.class, args);
}

}

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码server:
port: 8081
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # 配置nacos 服务端地址
namespace: public
metadata:
# gray-route是灰度路由配置的开始
gray-route:
enable: true
version: v1
application:
name: consumer-a # 服务名称

pom依赖

1
2
3
4
5
6
7
xml复制代码  <dependencies>
<dependency>
<groupId>com.cf</groupId>
<artifactId>commons</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>

consumer2服务

consumer2服务和consumer1服务一样,只是灰度路由版本不一样(同一个服务器时,其端口也不一致)

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yml复制代码server:
port: 8082
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # 配置nacos 服务端地址
namespace: public
metadata:
# gray-route是灰度路由配置的开始
gray-route:
enable: true
version: v2
application:
name: consumer-a # 服务名称

provider1服务

Controller控制器

1
2
3
4
5
6
7
8
9
10
11
java复制代码@RestController
@Slf4j
public class AProController {

@GetMapping("/provider")
public String list() {
String info = "我是 providerA,9091 ";
log.info(info);
return JSON.toJSONString(info);
}
}

Application启动类

1
2
3
4
5
6
7
8
java复制代码@EnableDiscoveryClient
@SpringBootApplication
public class AProviderApplication {

public static void main(String[] args) {
SpringApplication.run(AProviderApplication.class, args);
}
}

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yml复制代码server:
port: 9091
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # 配置nacos 服务端地址
namespace: public
metadata:
# gray-route是灰度路由配置的开始
gray-route:
enable: true
version: v1
application:
name: provider-a # 服务名称

provider2服务

provider2服务和provider1服务相比, 就是灰度路由版本不一致(同一个服务器时,其端口也不一致)

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yml复制代码server:
port: 9091
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848 # 配置nacos 服务端地址
namespace: public
metadata:
# gray-route是灰度路由配置的开始
gray-route:
enable: true
version: v2
application:
name: provider-a # 服务名称

验证测试

1 启动本地nacos服务

2 启动五个项目服务

此时,在nacos中,存在服务列表中存在三个, 分别是client-test服务(1个),provider-a服务(2个实例),consumer-a服务(2个实例)

3 使用postman进行测试

1 不指定请求头灰度路由

1
2
3
4
5
6
7
txt复制代码"我是客户端,8000  \"我是consumerA,8081    \\\"我是 providerA,9091  \\\"\""

"我是客户端,8000 \"我是consumerB,8082 \\\"我是 providerA,9091 \\\"\""

"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerB,9092 \\\"\""

"我是客户端,8000 \"我是consumerB,8082 \\\"我是 providerB,9092 \\\"\""

调用四次, 采用的是Ribbon中默认的轮询策略.

2 指定请求头灰度路由

请求头中设置gray-route = {"all":"v1"}

1
2
3
4
txt复制代码"我是客户端,8000  \"我是consumerA,8081    \\\"我是 providerA,9091  \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""

四次测试结果, 每个服务都是v1版本, 灰度路由生效.

请求头中设置{custom":{"consumer-a":"v1","provider-a":"v1"}}

1
2
3
4
txt复制代码"我是客户端,8000  \"我是consumerA,8081    \\\"我是 providerA,9091  \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""

四次测试结果, 每个服务都是v1版本, 灰度路由生效.

请求头中设置{custom":{"consumer-a":"v1","provider-a":"v2"}}

1
2
3
4
txt复制代码"我是客户端,8000  \"我是consumerA,8081    \\\"我是 providerB,9092     \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerB,9092 \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerB,9092 \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerB,9092 \\\"\""

四次测试结果, consumer服务都是v1版本, provider服务都是版本2,灰度路由生效.

请求头中设置{custom":{"consumer-a":"v1"}}

1
2
3
4
txt复制代码"我是客户端,8000  \"我是consumerA,8081    \\\"我是 providerA,9091  \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerB,9092 \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerB,9092 \\\"\""

四次测试结果, consumer服务都是v1版本, provider服务没有指定,所以采用默认轮询机制,灰度路由生效.

参考资料:

segmentfault.com/a/119000001…

www.cnblogs.com/linyb-geek/…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%