关于SpringCloud中灰度路由的使用

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

1 灰度路由的简介

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

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

整理思路为:

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

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

3 微服务调用选择时,根据指定的策略(如唯一标识版本等)从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);

    }
}

灰度路由属性类

@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;

}

路由属性类

@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 # 服务名称

验证测试

1 启动本地nacos服务

2 启动五个项目服务

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

3 使用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服务没有指定,所以采用默认轮询机制,灰度路由生效.

参考资料:

https://segmentfault.com/a/1190000017412946

https://www.cnblogs.com/linyb-geek/p/12774014.html

猜你喜欢

转载自blog.csdn.net/ABestRookie/article/details/121547190
今日推荐