# spring-cloud-gray **Repository Path**: cloud_configuration_file/spring-cloud-gray ## Basic Information - **Project Name**: spring-cloud-gray - **Description**: 基于springcloud的灰度发布 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 1 - **Created**: 2022-11-05 - **Last Updated**: 2024-10-16 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 课程大纲 ## 第1章 课程介绍 ### 灰度说明 类型: - 灰度发布 - 如:新功能上线,就新老系统共存,共存的时间就是灰度时间 - AB测试 - 某些功能有些用户可以用,有些用户不可以用 - 如:用户id为1的可以用功能A,其他用户只能用功能B 蓝绿发布:很烧经费(机器要翻倍) - 就是发布的时候老系统不动,用新机器发布新系统,然后在下掉老系统 - 缺点:机器要翻倍 滚动发布:在蓝绿上省一点经费 - 就是发布的时候老系统关1台,再起1个新服务,再关1台老系统,再起1个新服务 - 缺点:新老混合,如果老系统有问题就很麻烦 灰度发布:又叫金丝雀发布 - 老系统不动,用新机器发布新系统,然后慢慢的放开用户来使用新系统 ## 第2章 灰度解决方案 ### 灰度解决思路分析 实现思路: - 制定灰度规则:区分哪些用户,走哪些服务(可以定个数据库表,我们这里就直接放redis中了) - 灰度规则处理: - 网关调服务(需要处理网关) - 服务调服务(需要处理ribbon ) ### nacos简介 > 介绍下,然后安装 是一个服务注册中心和配置中心,这里我们主要用它的服务注册中心。 官网:https://nacos.io/zh-cn/docs/v2/quickstart/quick-start.html 下载(我们这里用的2.0.1):https://github.com/alibaba/nacos/releases/download/2.0.1/nacos-server-2.0.1.zip 启动:`startup.cmd -m standalone` 访问:http://localhost:8848/nacos 默认账号密码:nacos/nacos ### 服务注册之meta信息 我们看看平时我们注册服务的时候,看看nacos的控制台: ![1667657367561](img/1667657367561.png) 我们做灰度的时候,可以在元数据这里加入我们的服务标识(也就是服务实例的metadata)数据。 如: ~~~java @Bean @ConditionalOnMissingBean @ConditionalOnProperty(value = {"spring.cloud.nacos.discovery.watch.enabled"}, matchIfMissing = true) public NacosWatch nacosWatch(NacosServiceManager nacosServiceManager, NacosDiscoveryProperties nacosDiscoveryProperties) { nacosDiscoveryProperties.getMetadata().put("applicationName",applicationName); return new NacosWatch(nacosServiceManager,nacosDiscoveryProperties); } ~~~ 再次启动服务,看看nacos的控制台: ![1667657649010](img/1667657649010.png) ### gateway网关简介 简介一下 ### gateway之过滤器 简介一下 ### ribbon简介 简介一下 ### ribbon负载均衡规则 ribbon的负载均衡规则接口:`IRule`,主要职责是**选择服务器**,默认实现:`ZoneAvoidanceRule` 我们可以自定义规则: ~~~java @Component public class CusRule extends AbstractLoadBalancerRule { @Override public void initWithNiwsConfig(IClientConfig iClientConfig) { } @Override public Server choose(Object key) { ILoadBalancer loadBalancer = getLoadBalancer(); // 可达服务器列表 List servers = loadBalancer.getReachableServers(); if (servers.isEmpty()) { return null; } // 最后一台服务器 Server targetServer = servers.get(servers.size() - 1); return targetServer; } } ~~~ ## 第3章 代码实现 ### 工程准备 我们准备3的服务: - 网关:spring-cloud-gray-gateway - 服务1:spring-cloud-gray-service1 - 服务2:spring-cloud-gray-service2 这里我们将 服务1 当成灰度服务。 在idea中一个服务启动多次:需要配置一下 ![1667658676735](img/1667658676735.png) ![1667658704338](img/1667658704338.png) ### 灰度规则设计 我们这里的规则比较简单: - 用户id为1的访问新机器(下面统一叫灰度服务) - 其它用户访问老机器 重要属性如下: | 用户id | 灰度服务标识 | | ------ | ------------ | | 1 | v2 | > 注意:这里可以扩展,比如加一个服务的名称,对那些服务做服务 ### 服务注册之meta信息 这里我们将灰度服务的metadata元数据中加入 `gray=v2` 就可以了,spring-cloud-gray-service1加入如下代码: ~~~java /** * nacos客户端注册至服务端时,更改服务详情中的元数据,加入灰度服务的标识 */ @Configuration @ConditionalOnDiscoveryEnabled @AutoConfigureBefore({SimpleDiscoveryClientAutoConfiguration.class, CommonsClientAutoConfiguration.class}) public class NacosDiscoveryClientAutoConfiguration { @Bean @ConditionalOnMissingBean @ConditionalOnProperty(value = {"spring.cloud.nacos.discovery.watch.enabled"}, matchIfMissing = true) public NacosWatch nacosWatch(NacosServiceManager nacosServiceManager, NacosDiscoveryProperties nacosDiscoveryProperties) { nacosDiscoveryProperties.getMetadata().put("gray", "v2"); return new NacosWatch(nacosServiceManager,nacosDiscoveryProperties); } } ~~~ 启动,看一下nacos的控制台: ![1667658897397](img/1667658897397.png) ### gateway实现灰度 我们需要在网关服务中加一个全局过滤器来实现灰度,就是拦截到请求,然后使用我们的灰度规则来实现负载均衡。 这里网关内置了一个 LoadBalancerClientFilter 来处理负载均衡,我们这里走的还是ribbon的逻辑,所以我们只需要在ribbon中加入灰度规则就可以。 ### ribbon实现灰度 在内部调用的时候,我们需要自定义ribbon负载均衡规则来处理我们的灰度规则,在调用放处理,这里我们的调用方是spring-cloud-gray-service2,我们在spring-cloud-gray-service2服务和spring-cloud-gray-gateway服务中来自定义规则,ribbon的默认规则是ZoneAvoidanceRule,我们还是继承这个: ~~~java @Component public class GrayRule extends ZoneAvoidanceRule { @Autowired private StringRedisTemplate stringRedisTemplate; @Override public Server choose(Object key) { return choose(getLoadBalancer(), key); } public Server choose(ILoadBalancer ib, Object key) { Server server = null; // 根据当前线程获取参数(这个参数可以在拦截器中设置进去) Map map = RibbonParaContext.get(); Object grayRule = stringRedisTemplate.opsForHash().get("gray_rule", map.get("userId")); // 获取所有可达的服务 List reachableServers = ib.getReachableServers(); for (Server ser : reachableServers) { String gray = ((NacosServer) ser).getMetadata().get("gray"); if (!StringUtils.isEmpty(gray) && gray.equals(grayRule)){ server = ser; } } // 走默认规则 if (null == server) { ILoadBalancer lb = getLoadBalancer(); List allServers = lb.getAllServers(); // 排除掉我们的灰度服务 List okServers = new ArrayList<>(); for (Server sv : allServers) { String gray = ((NacosServer)sv).getMetadata().get("gray"); boolean isAdd = true; if (!StringUtils.isEmpty(gray)) { List grayRules = stringRedisTemplate.opsForHash().values("gray_rule"); if (!CollectionUtils.isEmpty(grayRules)) { for (Object obj : grayRules) { if (!StringUtils.isEmpty(obj) && gray.equals(obj)) { isAdd = false; } } } } if (isAdd) { okServers.add(sv); } } Optional serverZa = this.getPredicate().chooseRoundRobinAfterFiltering(okServers, key); if (serverZa.isPresent()) { return serverZa.get(); } else { return null; } } return server; } } ~~~ 在spring-cloud-gray-service2服务中我们使用拦截器获取来存储用户的信息: ~~~java public class UserIdInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String userId = request.getHeaders("userId").toString(); Map map = new HashMap<>(); map.put("userId", userId); RibbonParaContext.set(map); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { RibbonParaContext.clear(); } } ~~~ 配置我们的拦截器: ~~~java @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new UserIdInterceptor()).addPathPatterns("/**"); } } ~~~ 在spring-cloud-gray-gateway服务中我们使用全局过滤器获取来存储用户的信息: ~~~java @Component public class GrayFilter implements GlobalFilter, Ordered { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { String userId = exchange.getRequest().getHeaders().get("userId").get(0); Map map = new HashMap<>(); map.put("userId", userId); RibbonParaContext.set(map); return chain.filter(exchange); } @Override public int getOrder() { return 0; } } ~~~ ### ribbon灰度演示 在redis中添加一条规则数据: ``` hset gray_rule 1 'v2' ``` 这里我们还要用到两个服务: - 服务1:spring-cloud-gray-service1 - 服务2:spring-cloud-gray-service2 我们启动: - 服务1(端口:8582)、服务2(端口:8553) - 然后将服务1的添加metadata元数据的代码注释掉 - 修改端口为8572,启动 - 修改端口为8562,启动 启动如下: ![1667660049220](img/1667660049220.png) 两个请求测试: - 请求userId为1的 - 直接定向到端口为8582的服务 - 请求userId为3的 - 在端口是8562、8572的两个服务间负载均衡 测试: ``` ### ribbon-灰度 GET http://localhost:8553/hello userId: 1 ``` 效果: ``` { "port": 8582 } ``` 测试: ``` ### ribbon-无灰度 GET http://localhost:8553/hello userId: 3 ``` 效果: ``` { "port": 8562 } ``` ### gateway网关灰度演示 在redis中添加一条规则数据: ~~~ hset gray_rule 1 'v2' ~~~ 这里我们还要用到两个服务: - 网关:spring-cloud-gray-gateway - 服务1:spring-cloud-gray-service1 我们启动: - 网关(端口:8551)、服务1(端口:8582) - 然后将服务1的添加metadata元数据的代码注释掉 - 修改端口为8572,启动 - 修改端口为8562,启动 启动如下: ![1667659422699](img/1667659422699.png) 两个请求测试: - 请求userId为1的 - 直接定向到端口为8582的服务 - 请求userId为3的 - 在端口是8562、8572的两个服务间负载均衡 测试: ~~~ ### 网关-灰度 GET http://localhost:8551/spring-cloud-gray-service1/index userId: 1 ~~~ 效果: ~~~ { "port": 8582 } ~~~ 测试: ``` ### 网关-无灰度 GET http://localhost:8551/spring-cloud-gray-service1/index userId: 3 ``` 效果: ``` { "port": 8562 } ```