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&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接口
灰度发布的核心就是路由转发,如果我们能够自定义网关==>服务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-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);
- 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元数据信息是否已添加
-
灰度用户调用测试
-
正常用户请求测试
至此灰度发布验证完成,