spring-cloud基于Ribbon的服务间灰度

前言

本次还是灰度系列文章, 基于 Ribbon + nacos 实现

基础环境搭建

本项目基于聚合项目搭建, 项目结构如下, 其中provider启动2个实例, 通过consumer调用去模拟负载情况。

1
2
3
├── consumer
├── provider
├── pom.xml

根pom.xml

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
<?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>

<groupId>cn.idea360</groupId>
<artifactId>spring-cloud-demo</artifactId>
<version>0.0.1</version>
<packaging>pom</packaging>

<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
<spring-cloud-alibaba.version>2.2.2.RELEASE</spring-cloud-alibaba.version>
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
</properties>

<modules>
<module>provider</module>
<module>gateway</module>
<module>consumer</module>
</modules>

<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>

provider

  1. application.properties
1
2
3
4
5
6
7
8
9
# 应用名称
spring.application.name=provider
# 应用服务 WEB 访问端口
server.port=9001

spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
# 9001为普通实例; 9002带元数据, 模拟灰度服务
spring.cloud.nacos.discovery.metadata.version = gray
  1. 启动主类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package cn.idea360.provider;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication
public class ProviderApplication {

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

}
  1. 资源提供者
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
package cn.idea360.provider.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @author cuishiying
* @date 2021-01-22
*/
@RefreshScope
@RestController
@RequestMapping("/echo")
public class TestController {

@Autowired
private Environment env;

@Value("${version:0}")
private String version;
/**
* http://localhost:9001/echo/port
* @return
*/
@GetMapping("/port")
public Object port() {
return String.format("port=%s, version=%s", env.getProperty("local.server.port"), version);
}
}
  1. 启动2个实例, 端口分别为9001和9002, 其中9002配置灰度元数据, nacos配置参见 灰度网关

consumer

  1. application.properties
1
2
3
4
5
6
7
# 应用名称
spring.application.name=consumer
# 应用服务 WEB 访问端口
server.port=9007

spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
  1. 启动类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package cn.idea360.consumer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplication {

@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}

}
  1. 消费者
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
package cn.idea360.consumer.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
* @author cuishiying
* @date 2021-01-22
*/
@RefreshScope
@RestController
@RequestMapping("/echo")
public class TestController {

@Autowired
private RestTemplate restTemplate;
/**
* http://localhost:9007/echo/port
* @return
*/
@GetMapping("/port")
public Object port() {
return restTemplate.getForObject("http://provider/echo/port", String.class);
}
}
  1. 测试请求发现,默认负载策略为轮询策略
1
2
3
4
➜  ~ curl http://localhost:9007/echo/port
port=9001, version=2%
➜ ~ curl http://localhost:9007/echo/port
port=9002, version=2%

自定义负载均衡

  1. 自定义负载
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
package cn.idea360.consumer.ribbon;

import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

/**
* @author cuishiying
* @date 2021-01-22
*/
public class GrayRule extends AbstractLoadBalancerRule {

private final Logger logger = LoggerFactory.getLogger(this.getClass());

@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {

}

@Override
public Server choose(Object o) {
logger.info("key:{}", o);
ILoadBalancer loadBalancer = getLoadBalancer();
List<Server> allServers = loadBalancer.getAllServers();
logger.info("allServers:{}", allServers.toString());
return allServers.get(0);
}
}
  1. consumer配置文件添加
1
2
# provider为服务名
provider.ribbon.NFLoadBalancerRuleClassName=cn.idea360.consumer.ribbon.GrayRule
  1. 测试
1
curl http://localhost:9007/echo/port

日志:

1
2
2021-08-28 22:11:14.917  INFO 3620 --- [nio-9007-exec-5] cn.idea360.consumer.ribbon.GrayRule      : key:default
2021-08-28 22:11:14.917 INFO 3620 --- [nio-9007-exec-5] cn.idea360.consumer.ribbon.GrayRule : allServers:[192.168.124.14:9002, 192.168.124.14:9001]

nacos+ribbon负载均衡

consumer配置文件修改

application.properties

1
2
3
4
5
6
7
8
9
10
11
12
# 应用名称
spring.application.name=consumer
# 应用服务 WEB 访问端口
server.port=9007

spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848

# provider为服务名
provider.ribbon.NFLoadBalancerRuleClassName=cn.idea360.consumer.ribbon.GrayRule
# 代表需要请求灰度服务
spring.cloud.nacos.discovery.metadata.version = gray

自定义负载均衡策略

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
package cn.idea360.consumer.ribbon;

import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.ribbon.NacosServer;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.naming.NamingFactory;
import com.alibaba.nacos.api.naming.NamingService;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.Server;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import java.security.SecureRandom;
import java.util.List;
import java.util.stream.Collectors;

/**
* @author cuishiying
* @date 2021-01-22
*/
public class GrayRule extends AbstractLoadBalancerRule {

private final Logger logger = LoggerFactory.getLogger(this.getClass());
private static final String VERSION = "version";
@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;

@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {

}

@Override
public Server choose(Object o) {
logger.info("key:{}", o);
String clusterName = this.nacosDiscoveryProperties.getClusterName();
logger.info("clusterName:{}", clusterName);

String groupName = this.nacosDiscoveryProperties.getGroup();
logger.info("groupName:{}", groupName);

BaseLoadBalancer baseLoadBalancer = (BaseLoadBalancer) getLoadBalancer();
String serviceName = baseLoadBalancer.getName();
logger.info("serviceName:{}", serviceName);

String currentVersion = nacosDiscoveryProperties.getMetadata().get(VERSION);
logger.info("consumer-version:{}", currentVersion);

try {
NamingService namingService = NamingFactory.createNamingService(nacosDiscoveryProperties.getNacosProperties());
List<Instance> instances = namingService.selectInstances(serviceName, groupName, true);
if (instances.isEmpty()) {
logger.warn("no instance in service {}", serviceName);
return null;
}

List<Instance> targetInstances = instances.stream()
.filter(x -> StringUtils.equalsIgnoreCase(x.getMetadata().get(VERSION), currentVersion)
&& StringUtils.equalsIgnoreCase(x.getClusterName(), clusterName)
).collect(Collectors.toList());
logger.info("targetInstances:{}", targetInstances);

if (targetInstances.isEmpty()) {
return null;
} else {
SecureRandom random = new SecureRandom();
int i = random.nextInt(targetInstances.size());
Instance invokedInstance = targetInstances.get(i);
logger.info("invokedInstance:{}", invokedInstance.getInstanceId());
return new NacosServer(invokedInstance);
}
} catch (NacosException e) {
e.printStackTrace();
return null;
}
}
}

测试灰度结果,发现所有请求会负载到9002节点

1
2
➜  ~ curl http://localhost:9007/echo/port
port=9002, version=2%

provider打印日志

1
2
3
4
5
6
7
2021-11-27 13:54:34.210  INFO 5792 --- [nio-9007-exec-2] cn.idea360.consumer.ribbon.GrayRule      : key:default
2021-11-27 13:54:34.210 INFO 5792 --- [nio-9007-exec-2] cn.idea360.consumer.ribbon.GrayRule : clusterName:DEFAULT
2021-11-27 13:54:34.210 INFO 5792 --- [nio-9007-exec-2] cn.idea360.consumer.ribbon.GrayRule : groupName:DEFAULT_GROUP
2021-11-27 13:54:34.210 INFO 5792 --- [nio-9007-exec-2] cn.idea360.consumer.ribbon.GrayRule : serviceName:provider
2021-11-27 13:54:34.210 INFO 5792 --- [nio-9007-exec-2] cn.idea360.consumer.ribbon.GrayRule : consumer-version:gray
2021-11-27 13:54:34.216 INFO 5792 --- [nio-9007-exec-2] cn.idea360.consumer.ribbon.GrayRule : targetInstances:[Instance{instanceId='192.168.124.9#9002#DEFAULT#DEFAULT_GROUP@@provider', ip='192.168.124.9', port=9002, weight=1.0, healthy=true, enabled=true, ephemeral=true, clusterName='DEFAULT', serviceName='DEFAULT_GROUP@@provider', metadata={preserved.register.source=SPRING_CLOUD, version=gray}}]
2021-11-27 13:54:34.217 INFO 5792 --- [nio-9007-exec-2] cn.idea360.consumer.ribbon.GrayRule : invokedInstance:192.168.124.9#9002#DEFAULT#DEFAULT_GROUP@@provider

方案总结

服务间调用灰度方案(Consumer -> Provider)

  • P1、P2、C1、C2服务注册在nacos,并分别在nacos元数据配置版本号V1和V2
  • C用RestTemplate通过服务发现调用P
  • 自定义负载均衡策略
  • C读取自己元数据中的版本号, 代表自己想访问哪个版本的P(负载均衡策略)
  • 获取所有的P实例, 并过滤出符合自己版本的实例(负载均衡策略)
  • 最终的结果是C2调用P2, C1调用P1

最后

本文到此结束,感谢阅读。如果您觉得不错,请关注公众号【当我遇上你】,您的支持是我写作的最大动力。