Spring Cloud灰度发布方案----自定义路由规则

Spring Cloud灰度发布方案----自定义路由规则

一、简介

1.1 不停机部署服务策略介绍

  • 蓝绿部署
    蓝绿部署的模型中包含两个集群A和B
    1、在没有上线的正常情况下,集群A和集群B的代码版本是一致的,并且同时对外提供服务。
    2、在系统升级的时候下,我们首先把一个集群(比如集群A)从负载列表中摘除,进行新版本的部署。集群B仍然继续提供服务。
    3、当集群A升级完毕,我们把负载均衡重新指向集群A,再把集群B从负载列表中摘除,进行新版本的部署。集群A重新提供服务。
    4、最后,当集群B也升级完成,我们把集群B也恢复到负载列表当中。这个时候,两个集群的版本都已经升级,并且对外的服务几乎没有间断过。
    详细介绍请参考:https://www.cnblogs.com/aaron911/p/11299422.html

  • 滚动部署
    和蓝绿部署不同的是,滚动部署对外提供服务的版本并不是非此即彼,而是在更细的粒度下平滑完成版本的升级。
    滚动部署只需要一个集群,集群下的不同节点可以独立进行版本升级。比如在一个16节点的集群中,我们选择每次升级4个节点,过程如下图:
    Spring Cloud灰度发布方案----自定义路由规则_第1张图片

  • 灰度发布(金丝雀发布)
    金丝雀发布,与蓝绿部署不同的是,它不是非黑即白的部署方式,所以又称为灰度发布。它能够缓慢的将修改推广到一小部分用户,验证没有问题后,再推广到全部用户,以降低生产环境引入新功能带来的风险。
    灰度发布的重点就是制定引流策略,将请求分发到不同版本服务中。比如内部测试人员的请求分发到金丝雀服务,其他用户分发到旧服务中。测试通过之后在推广到全部用户。

部署方式 优势 劣势 描述
蓝绿部署 同一时间对外服务的只有一个版本,容易定位问题。升级和回滚一集群为粒度,操作相对简单 需要维护两个集群,机器成本要求高 两套环境交替升级,旧版本保留一定时间便于回滚。
滚动部署 只需维护一个集群,成本低 上线过程中,两个版本同时对外服务,不易定位问题,且容易造成数据错乱。升级和回滚操作相对复杂 按批次停止老版本实例,启动新版本实例。
灰度发布 新版本出现问题影响范围很小,允许失败,风险较小 只能适用于兼容迭代的方式,如果是大版本不兼容的场景,就没办法使用这种方式了 根据比例将老版本升级,例如80%用户访问是老版本,20%用户访问是新版本。

1.2 eureka RestFul接口

请求名称 请求方式 HTTP地址 请求描述
注册新服务 POST /eureka/apps/{appID} 传递JSON或者XML格式参数内容,HTTP code为204时表示成功
删除注册服务 DELETE /eureka/apps/{appID}/{instanceID}
发送服务心跳 PUT /eureka/apps/{appID}/{instanceID}
查询所有服务 GET /eureka/apps
查询指定appID的服务列表 GET /eureka/apps/{appID}
查询指定appID&instanceID GET /eureka/apps/{appID}/{instanceID} 获取指定appID以及InstanceId的服务信息
查询指定instanceID服务列表 GET /eureka/apps/instances/{instanceID} 获取指定instanceID的服务列表
变更服务状态 PUT /eureka/apps/{appID}/{instanceID}/status?value=DOWN 服务上线、服务下线等状态变动
变更元数据 PUT /eureka/apps/{appID}/{instanceID}/metadata?key=value 更新eurekametadata元数据

二、灰度发布流程及实现思路

2.1 调用链分析

  • 用户请求==>zuul网关==>服务a==>服务b
    1、首先用户发送请求
    2、经过网关分发请求到具体服务a
    3、服务a 调用服务b接口
    Spring Cloud灰度发布方案----自定义路由规则_第2张图片

灰度发布的核心就是路由转发,如果我们能够自定义网关==>服务a、服务a==>服务b中间的路由策略,就可以实现用户引流,灰度发布。

2.2 实现思路、流程

Spring Cloud灰度发布方案----自定义路由规则_第3张图片

  • 网关层设计思路
1. 用户请求首先到达Nginx然后转发到网关zuul,此时zuul拦截器会根据用户携带请求token解析出对应的userId,然后从路由规则表中获取路由转发规则。

2. 如果该用户配置了路由策略,则该用户是灰度用户,转发用户请求到配置的灰度服务。否则转发到正常服务。
  • 服务间调用设计思路
3. zuul网关将请求转发到服务a后,可能还会通过fegin调用其他服务。所以需要拦截请求,将请求头version=xxx给带上,然后存入线程变量。
此处不能用Threadlocal存储线程变量,因为SpringCloud用hystrix做线程池隔离,而线程池是无法获取到ThreadLocal中的信息的! 
所以这个时候我们可以参考Sleuth做分布式链路追踪的思路或者使用阿里开源的TransmittableThreadLocal方案。
此处使用HystrixRequestVariableDefault实现跨线程池传递线程变量。

4. 服务间调用时会经过ribbon组件从服务实例列表中获取一个实例选择转发。Ribbon默认的IRule规则为ZoneAvoidanceRule`。而此处我们继承该类,重写了其父类选择服务实例的方法。

5. 根据自定义IRule规则将灰度用户请求路由到灰度服务,非灰度用户请求路由到正常服务。

2.3 资源准备

  • spring cloud微服务准备
    调用链路:用户==>zuul-server==>abTest==> provider-server
服务名 端口 eureka元数据 描述
zuul-server 9000 网关服务
abTest 8083 version: v1 新版本金丝雀服务
abTest 8084 老版本服务
abTest 8085 老版本旧服务
provider-server 8093 version: v1 新版本金丝雀服务
provider-server 8094 老版本服务
provider-server 8095 老版本旧服务
  • 路由规则库表
# 用户表
CREATE TABLE `t_user`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `nickname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '用户昵称',
  `head_image` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT 'head_image',
  `city` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '城市',
  `gender` int(2) DEFAULT NULL COMMENT '性别  0:男 1:女',
  `user_type` int(2) DEFAULT 0 COMMENT '用户类型(0:普通用户 1:vip)',
  `mobile` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '用户手机号',
  `status` int(2) DEFAULT 1 COMMENT '用户状态 0:冻结  1:正常',
  `token` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '登录token',
  `token_expires_time` datetime(0) DEFAULT NULL COMMENT 'token过期时间',
  `create_time` datetime(0) DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime(0) DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;

INSERT INTO `t_user` VALUES (1, 'hld', NULL, NULL, 1, 0, 'xxxx', 1, 'nm4p2ouy9ckl20bnnd62acev3bnasdmb', '2021-12-01 15:31:09', '2021-08-31 15:31:18', '2021-09-01 16:15:25');
INSERT INTO `t_user` VALUES (2, 'xxx', NULL, NULL, 1, 0, 'xxxxx', 1, 'lskeu9s8df7sdsue7re890er343rtolzospw', '2021-12-01 15:31:09', '2021-08-31 15:31:18', '2021-09-01 16:15:25');
INSERT INTO `t_user` VALUES (3, 'www', NULL, NULL, 1, 0, 'wwww', 1, 'pamsnxs917823skshwienmal2m3n45mz', '2021-12-01 15:31:09', '2021-08-31 15:31:18', '2021-09-01 16:15:25');

# 灰度路由规则配置表
CREATE TABLE `ab_test`  (
  `id` int(11) NOT NULL,
  `application_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '服务名',
  `version` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '版本',
  `userId` int(11) DEFAULT NULL COMMENT '用户id',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

INSERT INTO `ab_test` VALUES (1, 'abTest', 'v1', 1);
INSERT INTO `ab_test` VALUES (2, 'abTest', 'v2', 3);

三、 代码实现

灰度服务eureka.instance.metadata-map元数据信息添加version: v1。 正常服务设置元数据信息
自定义路由规则IRule时可以根据version来区分是否灰度服务,从而实现不同用户路由到不同的服务中。

3.1 网关路由(zuul-server服务)

本demo使用zuul作为网关层,自定义网关层IRule路由规则实现网关层灰度。

  • 自定义IRule规则
package com.hanergy.out.config;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.hanergy.out.entity.AbTest;
import com.hanergy.out.entity.TUser;
import com.hanergy.out.service.AbTestService;
import com.hanergy.out.service.TUserService;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAvoidanceRule;
import com.netflix.niws.loadbalancer.DiscoveryEnabledServer;
import com.netflix.zuul.context.RequestContext;
import io.jmnarloch.spring.cloud.ribbon.rule.MetadataAwareRule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @description: 此处轮询调用对应服务
 * @author: Han LiDong
 * @create: 2021/11/18 16:12
 * @update: 2021/11/18 16:12
 */
// ZoneAvoidanceRule   AbstractLoadBalancerRule
@Component
public class GrayRule extends MetadataAwareRule {
     

    private AtomicInteger nextServerCyclicCounter;
    private static final boolean AVAILABLE_ONLY_SERVERS = true;
    private static final boolean ALL_SERVERS = false;

    private static Logger log = LoggerFactory.getLogger(GrayRule.class);

    public GrayRule() {
     
        nextServerCyclicCounter = new AtomicInteger(0);
    }
    private Random random = new Random();

    @Autowired
    private AbTestService abTestService;	//灰度规则配置表
    @Autowired
    private TUserService userService;		//用户表

    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {
     

    }

    /**
     * 根据请求头token获取用户信息,然后去ab_test表获取灰度规则。
     * @param lb
     * @param o
     * @return
     */
    @Override
    public Server choose(Object o) {
     
        return choose(getLoadBalancer(),o);
    }

    public Server choose(ILoadBalancer lb, Object o){
     
        if (lb == null) {
     
            log.warn("no load balancer");
            return null;
        }
        RequestContext requestContext =  RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();
        //请求请求头token信息
        String token = request.getHeader("token");
        // 根据token获取用户信息
        TUser user = userService.getOne(new QueryWrapper<TUser>()
                .lambda()
                .eq(TUser::getToken, token));
        // token异常
        if (user == null){
     
            requestContext.setSendZuulResponse(false);
            requestContext.setResponseStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
        }
        // 查询灰度发布配置表,判断此用户是否灰度用户
        AbTest abTest = abTestService.getOne(new QueryWrapper<AbTest>()
                .lambda()
                .eq(AbTest::getUserid, user.getId()));
        String version = null;
        if(abTest != null){
     
            version = abTest.getVersion();
        }
        //该用户可选择的服务列表(灰度用户:灰度服务列表   非灰度用户:非灰度服务列表)
        List<Server> allServers = new ArrayList<>();


        //1.从线程变量获取version信息
        //String version = GrayHolder.getGray();
        //获取所有可达服务
        List<Server> reachableServers = lb.getReachableServers();
        for (Server server : reachableServers){
     
            Map<String, String> metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata();
            String metaVersion = metadata.get("version");
            if (version != null && !version.isEmpty() && version.equals(metaVersion)){
        //是灰度用户并且当前server是灰度服务
                allServers.add(server);
            } else if ((version == null || version.isEmpty()) && metaVersion == null){
         //非灰度用户并且当前server非灰度服务
                allServers.add(server);
            }
        }
        // 轮询选择其中一个服务
        Server choosedServer = choose(lb, o, allServers);

        return choosedServer;
    }
    /**
     * 轮询策略选择一个服务
     * @param lb
     * @param o
     * @param allServers
     * @return
     */
    public Server choose(ILoadBalancer lb, Object o, List<Server> allServers){
     
        Server server = null;
        int count = 0;
        while (server == null && count++ < 10) {
     
            int upCount = allServers.size();

            if (upCount == 0) {
     
                log.warn("No up servers available from load balancer: " + lb);
                return null;
            }
            // 轮询服务下标
            int nextServerIndex = incrementAndGetModulo(upCount);
            server = allServers.get(nextServerIndex);

            if (server == null) {
     
                /* Transient. */
                Thread.yield();
                continue;
            }

            if (server.isAlive() && (server.isReadyToServe())) {
     
                return (server);
            }

            // Next.
            server = null;
        }

        if (count >= 10) {
     
            log.warn("No available alive servers after 10 tries from load balancer: "
                    + lb);
        }
        return server;
    }

    /**
     * Inspired by the implementation of {@link AtomicInteger#incrementAndGet()}.
     *
     * @param modulo The modulo to bound the value of the counter.
     * @return The next value.
     */
    private int incrementAndGetModulo(int modulo) {
     
        for (;;) {
     
            int current = nextServerCyclicCounter.get();
            int next = (current + 1) % modulo;
            if (nextServerCyclicCounter.compareAndSet(current, next))
                return next;
        }
    }
}
  • 自定义规则加入Spring容器(zuul-server服务)
    1、编写config配置类
package com.hanergy.out.config;

import com.netflix.loadbalancer.IRule;
import org.springframework.context.annotation.Bean;

/**
 * @description: 此处无需@Configuration注解,启动类增加@RibbonClient注解注入配置类
 * @author: Han LiDong
 * @create: 2021/11/18 16:53
 * @update: 2021/11/18 16:53
 */
//@Configuration
public class GrayRibbonConfiguration {
     

    @Bean
    public IRule ribbonRule(){
     
        return new GrayRule();
    }
}

2、启动类增加@RibbonClient注解,扫描IRule配置

package com.hanergy.out;

import com.hanergy.out.config.GrayRibbonConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

// 网关
@SpringBootApplication
@EnableZuulProxy
// name为微服务名称,必须和服务提供者的微服务名称一致,configuration配置自定义的负载均衡规则
@RibbonClient(name = "zuul-server",configuration = GrayRibbonConfiguration.class)
public class ZuulServiceApplication {
     

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

3.2 服务间调用路由策略(abTest服务)

由于Hystrix有2个隔离策略:THREAD以及SEMAPHORE,当隔离策略为THREAD时,由于线程切换,会导致无法获取到原线程中的缓存数据。默认就是THREAD策略,所以服务间调用时无法获取到request对象。所以就需要我们提前将灰度信息提前存储起来。
此处不能用Threadlocal存储线程变量,因为SpringCloud用hystrix做线程池隔离,而线程池是无法获取到ThreadLocal中的信息的!
所以这个时候我们可以参考Sleuth做分布式链路追踪的思路或者使用阿里开源的TransmittableThreadLocal方案。
此处使用HystrixRequestVariableDefault实现跨线程池传递线程变量。

  • HystrixRequestVariableDefault实现跨线程池工具类
package com.hanergy.out.config;

import com.netflix.hystrix.strategy.concurrency.HystrixRequestContext;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableDefault;

/**
 * @description:
 * @author: Han LiDong
 * @create: 2021/11/19 09:43
 * @update: 2021/11/19 09:43
 */
public class GrayHolder {
     

    private  static HystrixRequestVariableDefault<String> gray ;
   /* static {
        System.out.println("init holder");
    }*/


    public static String getGray(){
     
        return  gray.get();
    }

    public static void setGray(String token){
     
        HystrixRequestContext.initializeContext();
        gray =  new HystrixRequestVariableDefault<>();
        gray.set(token);
    }


}

  • aop拦截请求,获取灰度信息
package com.hanergy.out.config;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.hanergy.out.entity.AbTest;
import com.hanergy.out.entity.TUser;
import com.hanergy.out.service.AbTestService;
import com.hanergy.out.service.TUserService;
import com.hanergy.out.utils.RibbonParam;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableDefault;
import org.aopalliance.intercept.Joinpoint;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.support.HttpRequestWrapper;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.support.RequestContext;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

/**
 * @description:
 * @author: Han LiDong
 * @create: 2021/11/18 16:31
 * @update: 2021/11/18 16:31
 */
@Aspect
@Component
public class ReqestAspect {
     

    @Autowired
    private TUserService userService;
    @Autowired
    private AbTestService abTestService;
    
    @Before("execution(* com.hanergy.out.controller.*.*(..))")
    public void before(){
     
        HttpServletRequest request =  ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
        String token = request.getHeader("token");
        // 根据token获取用户信息
        TUser user = userService.getOne(new QueryWrapper<TUser>()
                .lambda()
                .eq(TUser::getToken, token));
        if (user == null){
     
            throw new RuntimeException("token异常");
        }
        // 查询灰度发布配置表,判断此用户是否灰度用户
        AbTest abTest = abTestService.getOne(new QueryWrapper<AbTest>()
                .lambda()
                .eq(AbTest::getUserid, user.getId()));

        Map<String,String> map = new HashMap<>();
        map.put("userId",user.getId().toString());
        RibbonParam.set(map);
		// 存储是否灰度用户信息
        GrayHolder.setGray(abTest == null ? "" : abTest.getVersion());

    }

}

  • 自定义ribbon路由规则
package com.hanergy.out.config;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.hanergy.out.entity.AbTest;
import com.hanergy.out.entity.TUser;
import com.hanergy.out.service.AbTestService;
import com.hanergy.out.service.TUserService;
import com.hanergy.out.utils.RibbonParam;
import com.netflix.client.config.IClientConfig;
import com.netflix.hystrix.strategy.concurrency.HystrixLifecycleForwardingRequestVariable;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableDefault;
import com.netflix.loadbalancer.*;
import com.netflix.niws.loadbalancer.DiscoveryEnabledServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.filter.RequestContextFilter;
import org.springframework.web.servlet.support.RequestContextUtils;
import org.springframework.web.servlet.tags.RequestContextAwareTag;
import org.w3c.dom.stylesheets.LinkStyle;
import springfox.documentation.RequestHandler;

import javax.servlet.http.HttpServletRequest;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

/**
 * @description:
 * @author: Han LiDong
 * @create: 2021/11/18 16:12
 * @update: 2021/11/18 16:12
 */
// ZoneAvoidanceRule   AbstractLoadBalancerRule
@Component
public class GrayRule extends ZoneAvoidanceRule {
     

    private AtomicInteger nextServerCyclicCounter;
    private static final boolean AVAILABLE_ONLY_SERVERS = true;
    private static final boolean ALL_SERVERS = false;

    private static Logger log = LoggerFactory.getLogger(GrayRule.class);

    public GrayRule() {
     
        nextServerCyclicCounter = new AtomicInteger(0);
    }
    private Random random = new Random();

    @Autowired
    private AbTestService abTestService;
    @Autowired
    private TUserService userService;

    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {
     

    }

    @Override
    public Server choose(Object o) {
     
        return choose(getLoadBalancer(),o);
    }
    /**
     * 根据请求头token获取用户信息,然后去ab_test表获取灰度规则。
     * @param lb
     * @param o
     * @return
     */
    public Server choose(ILoadBalancer lb, Object o){
     
        if (lb == null) {
     
            log.warn("no load balancer");
            return null;
        }
        //该用户可选择的服务列表(灰度用户:灰度服务列表   非灰度用户:非灰度服务列表)
        List<Server> allServers = new ArrayList<>();

        //1.从线程变量获取version信息
        String version = GrayHolder.getGray();
        //获取所有可达服务
        List<Server> reachableServers = lb.getReachableServers();
        for (Server server : reachableServers){
     
            Map<String, String> metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata();
            String metaVersion = metadata.get("version");
            if (version != null && !version.isEmpty() && version.equals(metaVersion)){
        //是灰度用户并且当前server是灰度服务
                allServers.add(server);
            } else if ((version == null || version.isEmpty()) && metaVersion == null){
         //非灰度用户并且当前server非灰度服务
                allServers.add(server);
            }
        }
        // 轮询选择其中一个服务
        Server choosedServer = choose(lb, o, allServers);

        return choosedServer;
    }

    /**
     * 轮询策略选择一个服务
     * @param lb
     * @param o
     * @param allServers
     * @return
     */
    public Server choose(ILoadBalancer lb, Object o, List<Server> allServers){
     
        Server server = null;
        int count = 0;
        while (server == null && count++ < 10) {
     
            int upCount = allServers.size();

            if (upCount == 0) {
     
                log.warn("No up servers available from load balancer: " + lb);
                return null;
            }
            int nextServerIndex = incrementAndGetModulo(upCount);
            server = allServers.get(nextServerIndex);

            if (server == null) {
     
                /* Transient. */
                Thread.yield();
                continue;
            }

            if (server.isAlive() && (server.isReadyToServe())) {
     
                return (server);
            }

            // Next.
            server = null;
        }

        if (count >= 10) {
     
            log.warn("No available alive servers after 10 tries from load balancer: "
                    + lb);
        }
        return server;
    }

    /**
     * Inspired by the implementation of {@link AtomicInteger#incrementAndGet()}.
     *
     * @param modulo The modulo to bound the value of the counter.
     * @return The next value.
     */
    private int incrementAndGetModulo(int modulo) {
     
        for (;;) {
     
            int current = nextServerCyclicCounter.get();
            int next = (current + 1) % modulo;
            if (nextServerCyclicCounter.compareAndSet(current, next))
                return next;
        }
    }
}
  • 自定义路由规则生效
    1、config配置类
package com.hanergy.out.config;

import com.netflix.loadbalancer.IRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @description:
 * @author: Han LiDong
 * @create: 2021/11/18 16:53
 * @update: 2021/11/18 16:53
 */
//@Configuration
public class GrayRibbonConfiguration {
     

    @Bean
    public IRule ribbonRule(){
     
        return new GrayRule();
    }
}

2、启动类增加@RibbonClient注解

package com.hanergy.out;

import com.hanergy.out.config.GrayRibbonConfiguration;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@SpringBootApplication
@EnableSwagger2
@EnableFeignClients          //feign
@EnableDiscoveryClient
@EnableCircuitBreaker       // 熔断器注解
//name为微服务名称,必须和服务提供者的微服务名称一致,configuration配置自定义的负载均衡规则
@RibbonClient(name = "abTest",configuration = GrayRibbonConfiguration.class)
@MapperScan(basePackages = {
     "com.hanergy.out.dao"})
public class HanergyOutApplication {
     

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

四、灰度接口测试

调用链:用户==》zuul网关==>abTest服务==>provider-server服务

4.1 provider-server服务提供测试接口

@Slf4j
@RestController
@RequestMapping("/v1/test")
public class TestController {
     

    @Value("${server.port}")
    private Integer port;

    @ApiOperation(value="获取端口号",notes="获取端口号")
    @GetMapping("/getPort")
    public HttpResult<Integer> getPort(){
     
        return HttpResult.successResult(port);
    }
}

4.2 abTest服务提供测试接口

  • feign服务间调用
@FeignClient(value = "provider-server",fallback = ManagerPreFallbackImpl.class)
public interface RemoteManagerPreService {
     

    @ApiOperation(value="获取端口号",notes="获取端口号")
    @GetMapping("/v1/test/getPort")
    public HttpResult<Integer> getPort();

}
  • hystrix断路器
@Slf4j
@Component
public class ManagerPreFallbackImpl implements RemoteManagerPreService {
     

    @Override
    public HttpResult<Integer> getPort() {
     
        log.error("获取provider服务端口异常");
        return null;
    }
}
  • 服务间调用
@Slf4j
@RestController
@RequestMapping("/v1/test")
public class TestController {
     

    @Value("${server.port}")
    private Integer port;

    @ApiOperation(value="获取provider服务端口号",notes="获取provider服务端口号")
    @GetMapping("/getProviderPort")
    public HttpResult<Integer> getProviderPort(){
     
    	// feign服务间调用
        HttpResult<Integer> res = remoteManagerPreService.getPort();
        Integer providerPort = res.getData();

        return HttpResult.successResult("port: "+ port + ",providerPort:" + providerPort);
    }
}

五、验证

abTest分别使用8083、8084、8085端口启动,其中8083端口设置元数据信息为 version: v1
provider-server分别使用8093、8094、8095端口启动,其中8093端口设置元数据信息为 version: v1
那么灰度用户的接口请求路由为:zuul==》8083端口服务==》8093端口服务
正常用户接口请求路由为:zuul==》8084/8085端口服务==》8094/8095端口服务

  • 启动所需服务
    启动eureka注册中心、zuul网关、abtest(8083、8084、8085)、provider-server(8093、8094、8095)
  • 调用eureka RestFul接口修改元数据信息
    通过此种方法更改server的元数据后,由于ribbon会缓存实例列表,所以在测试改变服务信息时,ribbon并不会马上从eureka拉去最新信息,需等待一段时间。
//修改8083端口abTest服务元数据信息
PUT  182.92.xxx.xxx:8761/eureka/apps/ABTEST/192.168.199.1:abTest:8083/metadata?version=v1
//修改8093端口provider-server服务元数据信息
PUT  182.92.219.202:8761/eureka/apps/PROVIDER-SERVER/192.168.199.1:provider-server:8093/metadata?version=v1
  • 验证eureka元数据信息是否已添加
    Spring Cloud灰度发布方案----自定义路由规则_第4张图片

  • 灰度用户调用测试
    Spring Cloud灰度发布方案----自定义路由规则_第5张图片

  • 正常用户请求测试

Spring Cloud灰度发布方案----自定义路由规则_第6张图片
Spring Cloud灰度发布方案----自定义路由规则_第7张图片
至此灰度发布验证完成,

你可能感兴趣的