一、书写背景:
最近跟一些朋友在讨论技术,他们都提到了微服务下金丝雀发布没有什么头绪,但微服务下金丝雀发布又是现实工作中不过避免的事实。刚好最近不是特别忙,可以抽出一点时间写点东西(微服务下金丝雀发布)。考虑到看文章的读者能够对金丝雀发布有所理解(大概思路:发布过程中,先发一台或者一小部分比例的机器作为金丝雀,用于流量验证。如果金丝雀验证通过则把剩余机器全部发掉。如果金丝雀验证失败,则直接回退金丝雀)
二、整理思路:
1.在每个新版本包中设计相同的标签,此标签可以是基于服务器注册中心的元数据(metadata),也可以是基于配置中心标识配置。
2.自定义loadBalancer,让其在Balancer时能够识别出当前服务的标签与被调用服务的标签是否相同,若相关则选择其他中一个Instance进行路由,若没有到相同的标签,在根据新的算法选择其他一个Instance进行路由
3.让自定义的loadBalancer生效
三、具体实现:
1.pom导入nacos依赖
<!--根节点-->
? ? <parent>
? ? ? ? <groupId>org.springframework.boot</groupId>
? ? ? ? <artifactId>spring-boot-starter-parent</artifactId>
? ? ? ? <version>2.3.12.RELEASE</version>
? ? ? ? <relativePath/>
? ? </parent>
<!--springcloud 子项目nacos服务发现与注册依赖-->
? ? <dependency>
? ? ? ? <groupId>com.alibaba.cloud</groupId>
? ? ? ? <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
? ? </dependency>
<!--多父继承 -->
? ? <dependencyManagement>
? ? ? ? <dependencies>
? ? ? ? ? ? <!--spring-cloud-dependencies-->
? ? ? ? ? ? <dependency>
? ? ? ? ? ? ? ? <groupId>org.springframework.cloud</groupId>
? ? ? ? ? ? ? ? <artifactId>spring-cloud-dependencies</artifactId>
? ? ? ? ? ? ? ? <version>Hoxton.SR9</version>
? ? ? ? ? ? ? ? <type>pom</type>
? ? ? ? ? ? ? ? <scope>import</scope>
? ? ? ? ? ? </dependency>
? ? ? ? ? ? <!--alibaba nacos -->
? ? ? ? ? ? <dependency>
? ? ? ? ? ? ? ? <groupId>com.alibaba.cloud</groupId>
? ? ? ? ? ? ? ? <artifactId>spring-cloud-alibaba-dependencies</artifactId>
? ? ? ? ? ? ? ? <version>2.2.2.RELEASE</version>
? ? ? ? ? ? ? ? <type>pom</type>
? ? ? ? ? ? ? ? <scope>import</scope>
? ? ? ? ? ? </dependency>
? ? ? ? </dependencies>
? ? </dependencyManagement>
2.自定义loadbanlance
//第一步:定义常量类,封装灰度标签,灰度标签值可以通过第三方配置中心,如nacos、apollo、config等,以下只是做实例,故直接写在了常量类
package com.charlie.cloudportal.common.constants;
public class GrayConstant {
????public static final String GRAY_TAG ="version";
}
//第二步:实现loadbanlance,通过loadbalancer源码查看,其顶层为IRule接口,但考虑到复用IRule其他实现类的一些功能,故推荐实现其AbstractLoadBalancerRule 抽象类,具体如下:
package com.charlie.cloudportal.config.rule;
import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.NacosServiceManager;
import com.alibaba.cloud.nacos.ribbon.ExtendBalancer;
import com.alibaba.cloud.nacos.ribbon.NacosServer;
import com.alibaba.nacos.api.naming.NamingService;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.charlie.cloudportal.common.constants.GrayConstant;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.DynamicServerListLoadBalancer;
import com.netflix.loadbalancer.Server;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import java.util.*;
/**
* @Author: charlie
* @CreateTime: 2022/3/13
* Description: 金丝雀版本权重负载均衡策略--优先选择相同版本节点进行通信
*/
@Slf4j
public class CanaryBalancerRule extends AbstractLoadBalancerRule {
? ? @Autowired
? ? private NacosDiscoveryProperties nacosDiscoveryProperties;
? ? @Autowired
? ? private NacosServiceManager nacosServiceManager;
? ? public CanaryBalancerRule() {
? ? }
? ? @Override
? ? public void initWithNiwsConfig(IClientConfig iClientConfig) {
? ? }
? ? @Override
? ? public Server choose(Object key) {
? ? ? ? try {
? ? ? ? ? ? //获取集群名称
? ? ? ? ? ? String clusterName = this.nacosDiscoveryProperties.getClusterName();
? ? ? ? ? ? //获取分组
? ? ? ? ? ? String group = this.nacosDiscoveryProperties.getGroup();
? ? ? ? ? ? //获取当前服务的名称
? ? ? ? ? ? String currServerName = this.nacosDiscoveryProperties.getService();
? ? ? ? ? ? //获取当前服务的metaData数据
? ? ? ? ? ? Map<String,String> ribbonAttributes = this.nacosDiscoveryProperties.getMetadata();
? ? ? ? ? ? //获取被调用的服务名
? ? ? ? ? ? DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer)this.getLoadBalancer();
? ? ? ? ? ? String serviceName = loadBalancer.getName();
? ? ? ? ? ? log.info("[ribbon负载均衡策略] 当前服务名: {}", currServerName);
? ? ? ? ? ? //获取服务发现客户端
? ? ? ? ? ? NamingService namingService = this.nacosServiceManager.getNamingService(this.nacosDiscoveryProperties.getNacosProperties());
? ? ? ? ? ? // 获取被调用的服务实例列表
? ? ? ? ? ? List<Instance> allInstances = namingService.selectInstances(serviceName, group, true);
? ? ? ? ? ? log.info("[ribbon负载均衡策略] 可用的服务实例: {}", allInstances);
? ? ? ? ? ? // 可供选择的灰度服务
? ? ? ? ? ? List<Instance> grayInstances = new ArrayList<>();
? ? ? ? ? ? // 非灰服务
? ? ? ? ? ? List<Instance> noneGrayInstances = new ArrayList<>();
? ? ? ? ? ? Instance toBeChooseInstance;
? ? ? ? ? ? if (CollectionUtils.isEmpty(allInstances)) {
? ? ? ? ? ? ? ? log.warn("no instance in service {}", serviceName);
? ? ? ? ? ? ? ? return null;
? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? //List<Instance> instancesToChoose = allInstances;
? ? ? ? ? ? ? ? if (StringUtils.isNotBlank(clusterName)) {
? ? ? ? ? ? ? ? ? ? for (Instance instance : allInstances) {
? ? ? ? ? ? ? ? ? ? ? ? Map<String, String> metadata = instance.getMetadata();? ? ? ? ? ? ? ? ? ? ? ? if(ribbonAttributes.get(GrayConstant.GRAY_TAG).trim().toLowerCase().equals(metadata.get(GrayConstant.GRAY_TAG).trim().toLowerCase() )){
? ? ? ? ? ? ? ? ? ? ? ? ? ? log.info("当前服务:{},灰度tag为:{} ----- 被调服务:{} 进行灰度匹配,已匹配灰度服务:{},灰度tag为:{}",currServerName,ribbonAttributes.get(GrayConstant.GRAY_TAG), serviceName,instance, this.nacosDiscoveryProperties.getMetadata().get(GrayConstant.GRAY_TAG));
? ? ? ? ? ? ? ? ? ? ? ? ? ? grayInstances.add(instance);
? ? ? ? ? ? ? ? ? ? ? ? } else if (!StringUtils.isBlank(metadata.get(GrayConstant.GRAY_TAG))) {
? ? ? ? ? ? ? ? ? ? ? ? ? ? log.info("服务:{} 进行灰度匹配,未匹配灰度服务:{},metadata为:{}", serviceName,instance,metadata);
? ? ? ? ? ? ? ? ? ? ? ? ? ? log.info("当前服务:{},灰度tag为:{} ----- 被调服务:{} 进行灰度匹配,未匹配灰度服务:{},灰度tag为:{}",currServerName,ribbonAttributes.get(GrayConstant.GRAY_TAG), serviceName,instance, this.nacosDiscoveryProperties.getMetadata().get(GrayConstant.GRAY_TAG));
? ? ? ? ? ? ? ? ? ? ? ? ? ? // 非灰度服务
? ? ? ? ? ? ? ? ? ? ? ? ? ? noneGrayInstances.add(instance);
? ? ? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? log.info("[ribbon负载均衡策略] 灰度服务: {}, 非灰服务:{}", grayInstances, noneGrayInstances);
? ? ? ? ? ? ? ? // 如果灰度服务不为空, 则走灰度服务
? ? ? ? ? ? ? ? if (grayInstances != null && grayInstances.size() > 0) {
? ? ? ? ? ? ? ? ? ? // 走灰度服务 -- 从本集群中按照权重随机选择一个服务实例
? ? ? ? ? ? ? ? ? ? toBeChooseInstance = ExtendBalancer.getHostByRandomWeight2(grayInstances);
? ? ? ? ? ? ? ? ? ? log.info("[ribbon负载均衡策略] 灰度规则匹配成功, 匹配的灰度服务是: {}", toBeChooseInstance);
? ? ? ? ? ? ? ? ? ? return new NacosServer(toBeChooseInstance);
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? // 灰度服务为空, 走非断灰的服务
? ? ? ? ? ? ? ? if (noneGrayInstances != null && noneGrayInstances.size() > 0) {
? ? ? ? ? ? ? ? ? ? // 走非灰服务 -- 从本集群中按照权重随机选择一个服务实例
? ? ? ? ? ? ? ? ? ? toBeChooseInstance = ExtendBalancer.getHostByRandomWeight2(noneGrayInstances);
? ? ? ? ? ? ? ? ? ? // toBeChooseInstance = WeightedBalancer.chooseInstanceByRandomWeight(noneGrayInstances);
? ? ? ? ? ? ? ? ? ? log.info("[ribbon负载均衡策略] 不走灰度, 匹配的非灰度服务是: {}", toBeChooseInstance);
? ? ? ? ? ? ? ? ? ? return new NacosServer(toBeChooseInstance);
? ? ? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? ? ? log.info("未找到可匹配服务,实际服务:{}", allInstances);
? ? ? ? ? ? ? ? ? ? //按nacos权重获取。这个是NacosRule的代码copy 过来 没有自己实现权重随机。这个权重是nacos控制台服务的权重设置
? ? ? ? ? ? ? ? ? ? // 如果业务上有自己特殊的业务。可以自己定制规则,黑白名单,用户是否是灰度用户,测试账号。等等一些自定义设置
? ? ? ? ? ? ? ? ? ? toBeChooseInstance = ExtendBalancer.getHostByRandomWeight2(allInstances);
? ? ? ? ? ? ? ? ? ? // toBeChooseInstance = WeightedBalancer.chooseInstanceByRandomWeight(allInstances);
? ? ? ? ? ? ? ? ? ? log.info("[ribbon负载均衡策略] 未找到可匹配服务, 随机选择一个: {}", toBeChooseInstance);
? ? ? ? ? ? ? ? ? ? return new NacosServer(toBeChooseInstance);
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? } catch (Exception var10) {
? ? ? ? ? ? log.warn("NacosRule error", var10);
? ? ? ? ? ? return null;
? ? ? ? }
? ? }
}
3.修改默认负载均衡策略
//第一步:创建配置类,修改默认负载均衡策略
package com.charlie.cloudportal.config;
import com.charlie.cloudportal.config.rule.CanaryBalancerRule;
import com.netflix.loadbalancer.IRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Author: charlie
* @CreateTime: 2022/3/13
* Description: 全局配置类
*/
@Configuration
public class RibbonConfig {
? ? /**
? ? * 全局配置
? ? * 指定负载均衡策略
? ? * @return
? ? */
? ? @Bean
? ? public IRule canaryBalancerRule() {
? ? ? ? return new CanaryBalancerRule();
? ? }
}
//第二步:在bootstrap.yml文件中开启ribbon饥饿加载,解决第一次调用慢的问题
#ribbon默认是懒加载,为了解决第一次调用慢的问题(项目启动时不会创建,只有在发起调用的时候才会创建客户端),可以开启饥饿加载
ribbon:
#?开启ribbon饥饿加载: true开始;false关闭
? eager-load:
enabled:true
4. 配置版本号
在bootstrap.yml文件中配置版本号
spring:
? cloud:
? ? nacos:
? ? ? discovery:
? ? ? ? metadata:
? ? ? ? ? ? version: 1.0.0
? ? ? ? ? ? active: ${spring.profiles.active}
四、效果展示
1.nacos注册的服务信息如下:
portal在nacos注册的服务信息如下:
consumer在nacos注册的服务信息如下:
2.访问portal
127.0.0.1:18020/portal-provider/userLogin?username=1234,通过控制台信息证明,version=1.0.0的服务进行了通信
127.0.0.1:18021/portal-provider/userLogin?username=1234,通过控制台信息证明,version=1.0.1的服务进行了通信
以上的构思仅提供一个思路,期望对看到的您有所启发,同时上述代码是经过本人运行过的!