SpringCloud中的灰度路由使用详解
作者:韩_师兄
1 灰度路由的简介
在微服务中, 通常为了高可用, 同一个服务往往采用集群方式部署, 即同时存在几个相同的服务,而灰度的核心就 是路由, 通过我们特定的策略去调用目标服务线路
灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。
在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。
灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度.
关于SpringCloud微服务+nacos的灰度发布实现, 首先微服务中之间的调用通常使用Feign方式和Resttemplate方式(较少使用),因此 , 我们需要指定服务之间的调用, 首先要给各个服务添加唯一标识, 我们可是使用一些特殊的标记, 如版本号version等, 其次,要干预微服务中Ribbon的默认轮询调用机制, 我们需要根据微服务的版本等不同, 来进行调用, 最后, 在服务之间, 需要传递调用链路的信息, 我们可以在请求头中,添加调用链路的信息.
整理思路为:
- 在请求头中添加调用链路信息
- 微服务之间调用时,使用feign拦截器,增强请求头
- 微服务调用选择时,根据指定的策略(如唯一标识版本等)从nacos中获取指定的服务,调用
2 灰度路由的使用
基础服务
一个父服务,一个工具服务
父服务
pom依赖
<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拦截器
@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); } }
灰度路由属性类
@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); } }
路由属性类
@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方法即可.
@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控制器
@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接口
@FeignClient(value = "consumer-a") public interface ConsumerFeign { @ResponseBody @GetMapping("/consumer") String list(); }
Application启动器
@SpringBootApplication @EnableFeignClients({"com.cf.client.feign"}) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
application.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依赖
<!--自定义commons工具包--> <dependencies> <dependency> <groupId>com.cf</groupId> <artifactId>commons</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> </dependencies>
consumer1服务
Controller控制器
@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接口
@FeignClient(value = "provider-a") public interface ProviderFeign { @ResponseBody @GetMapping("/provider") String list(); }
Application启动类
@EnableDiscoveryClient @SpringBootApplication @EnableFeignClients({"com.cf.consumer.feign"}) public class AConsumerApplication { public static void main(String[] args) { SpringApplication.run(AConsumerApplication.class, args); } }
application.yml
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依赖
<dependencies> <dependency> <groupId>com.cf</groupId> <artifactId>commons</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> </dependencies>
consumer2服务
consumer2服务和consumer1服务一样,只是灰度路由版本不一样(同一个服务器时,其端口也不一致)
application.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控制器
@RestController @Slf4j public class AProController { @GetMapping("/provider") public String list() { String info = "我是 providerA,9091 "; log.info(info); return JSON.toJSONString(info); } }
Application启动类
@EnableDiscoveryClient @SpringBootApplication public class AProviderApplication { public static void main(String[] args) { SpringApplication.run(AProviderApplication.class, args); } }
application.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
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 # 服务名称
验证测试
- 启动本地nacos服务
- 启动五个项目服务
- 此时,在nacos中,存在服务列表中存在三个, 分别是client-test服务(1个),provider-a服务(2个实例),consumer-a服务(2个实例)
- 使用postman进行测试
1 不指定请求头灰度路由
"我是客户端,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"}
"我是客户端,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"}}
"我是客户端,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"}}
"我是客户端,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"}}
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerB,9092 \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerA,9091 \\\"\""
"我是客户端,8000 \"我是consumerA,8081 \\\"我是 providerB,9092 \\\"\""
四次测试结果, consumer服务都是v1版本, provider服务没有指定,所以采用默认轮询机制,灰度路由生效
到此这篇关于SpringCloud中的灰度路由使用详解的文章就介绍到这了,更多相关SpringCloud灰度路由内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!