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
- application.properties
1 2 3 4 5 6 7 8 9
| spring.application.name=provider
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
spring.cloud.nacos.discovery.metadata.version = gray
|
- 启动主类
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 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;
@RefreshScope @RestController @RequestMapping("/echo") public class TestController {
@Autowired private Environment env;
@Value("${version:0}") private String version;
@GetMapping("/port") public Object port() { return String.format("port=%s, version=%s", env.getProperty("local.server.port"), version); } }
|
- 启动2个实例, 端口分别为9001和9002, 其中9002配置灰度元数据, nacos配置参见 灰度网关
consumer
- application.properties
1 2 3 4 5 6 7
| spring.application.name=consumer
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 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 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;
@RefreshScope @RestController @RequestMapping("/echo") public class TestController {
@Autowired private RestTemplate restTemplate;
@GetMapping("/port") public Object port() { return restTemplate.getForObject("http://provider/echo/port", String.class); } }
|
- 测试请求发现,默认负载策略为轮询策略
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 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;
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); } }
|
- consumer配置文件添加
1 2
| provider.ribbon.NFLoadBalancerRuleClassName=cn.idea360.consumer.ribbon.GrayRule
|
- 测试
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
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.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;
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
最后
本文到此结束,感谢阅读。如果您觉得不错,请关注公众号【当我遇上你】,您的支持是我写作的最大动力。