SpringCloud中的灰度路由使用详解

 更新时间:2023年08月31日 11:07:53   作者:韩_师兄  
这篇文章主要介绍了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灰度路由内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • spring boot2.0总结介绍

    spring boot2.0总结介绍

    今天小编就为大家分享一篇关于spring boot2.0总结介绍,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2018-12-12
  • Eclipse中改变默认的workspace的方法及说明详解

    Eclipse中改变默认的workspace的方法及说明详解

    eclipse中改变默然的workspace的方法有哪几种呢?接下来脚本之家小编给大家介绍Eclipse中改变默认的workspace的方法及说明,对eclipse改变workspace相关知识感兴趣的朋友一起学习吧
    2016-04-04
  • 一篇文章带你了解Java中ThreadPool线程池

    一篇文章带你了解Java中ThreadPool线程池

    线程池可以控制运行的线程数量,本文就线程池做了详细的介绍,需要了解的小伙伴可以参考一下
    2021-08-08
  • Java协议字节操作工具类详情

    Java协议字节操作工具类详情

    这篇文章主要介绍了Java协议字节操作工具类详情,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以参考一下
    2022-09-09
  • Logback日志基础及自定义配置代码实例

    Logback日志基础及自定义配置代码实例

    这篇文章主要介绍了Logback日志基础及自定义配置代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-09-09
  • Spring中的事务隔离级别和传播行为

    Spring中的事务隔离级别和传播行为

    这篇文章主要介绍了Spring中的事务隔离级别和传播行为,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-03-03
  • java实现计算器加法小程序(图形化界面)

    java实现计算器加法小程序(图形化界面)

    这篇文章主要介绍了Java实现图形化界面的计算器加法小程序,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-05-05
  • springboot断言异常封装与统一异常处理实现代码

    springboot断言异常封装与统一异常处理实现代码

    异常处理其实一直都是项目开发中的大头,但关注异常处理的人一直都特别少,下面这篇文章主要给大家介绍了关于springboot断言异常封装与统一异常处理的相关资料,需要的朋友可以参考下
    2023-01-01
  • 18个Java8日期处理的实践(太有用了)

    18个Java8日期处理的实践(太有用了)

    这篇文章主要介绍了18个Java8日期处理的实践(太有用了),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-01-01
  • SpringSecurity安全框架在SpringBoot框架中的使用详解

    SpringSecurity安全框架在SpringBoot框架中的使用详解

    在Spring Boot框架中,Spring Security是一个非常重要的组件,它可以帮助我们实现应用程序的安全性,本文将详细介绍Spring Security在Spring Boot框架中的使用,包括如何配置Spring Security、如何实现身份验证和授权、如何防止攻击等
    2023-06-06

最新评论