定制小程序开发费用Spring Cloud灰度发布方案----自定义路由规则

Spring Cloud定制小程序开发费用灰度发布方案----定制小程序开发费用自定义路由规则

一、简介

1.1 定制小程序开发费用不停机部署服务策略介绍

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

  • 滚动部署
    和蓝绿部署不同的是,滚动部署对外提供服务的版本并不是非此即彼,而是在更细的粒度下平滑完成版本的升级。
    滚动部署只需要一个集群,集群下的不同节点可以独立进行版本升级。比如在一个16节点的集群中,我们选择每次升级4个节点,过程如下图:

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

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

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

2.2 实现思路、流程

  • 网关层设计思路
1. 用户请求首先到达Nginx然后转发到网关zuul,此时zuul拦截器会根据用户携带请求token解析出对应的userId,然后从路由规则表中获取路由转发规则。2. 如果该用户配置了路由策略,则该用户是灰度用户,转发用户请求到配置的灰度服务。否则转发到正常服务。
  • 1
  • 2
  • 3
  • 服务间调用设计思路
3. zuul网关将请求转发到服务a后,可能还会通过fegin调用其他服务。所以需要拦截请求,将请求头version=xxx给带上,然后存入线程变量。此处不能用Threadlocal存储线程变量,因为SpringCloud用hystrix做线程池隔离,而线程池是无法获取到ThreadLocal中的信息的! 所以这个时候我们可以参考Sleuth做分布式链路追踪的思路或者使用阿里开源的TransmittableThreadLocal方案。此处使用HystrixRequestVariableDefault实现跨线程池传递线程变量。4. 服务间调用时会经过ribbon组件从服务实例列表中获取一个实例选择转发。Ribbon默认的IRule规则为ZoneAvoidanceRule`。而此处我们继承该类,重写了其父类选择服务实例的方法。5. 根据自定义IRule规则将灰度用户请求路由到灰度服务,非灰度用户请求路由到正常服务。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

2.3 资源准备

  • spring cloud微服务准备
    调用链路:用户==>zuul-server==>abTest==> provider-server
服务名端口eureka元数据描述
zuul-server9000网关服务
abTest8083version: v1新版本金丝雀服务
abTest8084老版本服务
abTest8085老版本旧服务
provider-server8093version: v1新版本金丝雀服务
provider-server8094老版本服务
provider-server8095老版本旧服务
  • 路由规则库表
# 用户表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);
  • 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

三、 代码实现

灰度服务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@Componentpublic 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;        }    }}
  • 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
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 自定义规则加入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 *///@Configurationpublic class GrayRibbonConfiguration {    @Bean    public IRule ribbonRule(){        return new GrayRule();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

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);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

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);    }}
  • 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
  • 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@Componentpublic 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());    }}
  • 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
  • 自定义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@Componentpublic 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
  • 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
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 自定义路由规则生效
    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 *///@Configurationpublic class GrayRibbonConfiguration {    @Bean    public IRule ribbonRule(){        return new GrayRule();    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

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);    }}
  • 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

四、灰度接口测试

调用链:用户==》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);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

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();}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • hystrix断路器
@Slf4j@Componentpublic class ManagerPreFallbackImpl implements RemoteManagerPreService {    @Override    public HttpResult<Integer> getPort() {        log.error("获取provider服务端口异常");        return null;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 服务间调用
@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);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

五、验证

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
  • 1
  • 2
  • 3
  • 4
  • 验证eureka元数据信息是否已添加

  • 灰度用户调用测试

  • 正常用户请求测试



至此灰度发布验证完成,

网站建设定制开发 软件系统开发定制 定制软件开发 软件开发定制 定制app开发 app开发定制 app开发定制公司 电商商城定制开发 定制小程序开发 定制开发小程序 客户管理系统开发定制 定制网站 定制开发 crm开发定制 开发公司 小程序开发定制 定制软件 收款定制开发 企业网站定制开发 定制化开发 android系统定制开发 定制小程序开发费用 定制设计 专注app软件定制开发 软件开发定制定制 知名网站建设定制 软件定制开发供应商 应用系统定制开发 软件系统定制开发 企业管理系统定制开发 系统定制开发