自定义springboot组件--基于nacos和spring-cloud-loadbalancer实现灰度发布

一 灰度发布

灰度发布也叫金丝雀发布,起源是,矿井工人发现,金丝雀对瓦斯气体很敏感,矿工会在下井之前,先放一只金丝雀到井中,如果金丝雀不叫了,就代表瓦斯浓度高.
自定义springboot组件--基于nacos和spring-cloud-loadbalancer实现灰度发布_第1张图片
在灰度发布开始后,先启动一个新版本应用,但是并不直接将流量切过来,而是测试人员对新版本进行线上测试,启动的这个新版本应用,就是我们的金丝雀。如果没有问题,那么可以将少量的用户流量导入到新版本上,然后再对新版本做运行状态观察,收集各种运行时数据,如果此时对新旧版本做各种数据对比,就是所谓的A/B测试。

当确认新版本运行良好后,再逐步将更多的流量导入到新版本上,在此期间,还可以不断地调整新旧两个版本的运行的服务器副本数量,以使得新版本能够承受越来越大的流量压力。直到将100%的流量都切换到新版本上,最后关闭剩下的老版本服务,完成灰度发布。

如果在灰度发布过程中(灰度期)发现了新版本有问题,就应该立即将流量切回老版本上,这样,就会将负面影响控制在最小范围内。

二 实现思路

2.1 基本思路

Spring Cloud LoadBalancer是一个客户端负载均衡器,类似于Ribbon,但是由于Ribbon已经进入维护模式,并且Ribbon 2并不与Ribbon 1相互兼容,所以Spring Cloud全家桶在Spring Cloud Commons项目中,添加了Spring cloud Loadbalancer作为新的负载均衡器,并且做了向前兼容;

本文主要是基于Spring Cloud LoadBalancer,重写Spring Cloud LoadBalancer默认的客户端负载均衡器,实现灰度均衡
自定义springboot组件--基于nacos和spring-cloud-loadbalancer实现灰度发布_第2张图片

我们可以看到Spring Cloud LoadBalancer提供的负载均衡策略比较少,内置轮询随机的负载均衡策略,默认轮询策略,本文主要覆写轮询策略的解析请求的请求头获得当前请求的版本后,从nacos匹配到相对应的版本实例实现灰度版本的负载均衡.

2.2 代码实现

2.2.1 自定义灰度负载均衡策略

重写轮询策略,实现自定义的灰度策略

自定义灰度策略继承轮询策略,重写choose方法,解析当前请求头中版本号与nacos元数据中的版本号比对,匹配后返回相应的服务实例.

/**
 *  自定义灰度校验策略
 * @author likun
 * @date 2022年06月23日 16:08
 */
@Slf4j
public class GrayRoundRobinLoadBalancer extends RoundRobinLoadBalancer {
    final String serviceId;
    ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
    public GrayRoundRobinLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) {
        super(serviceInstanceListSupplierProvider, serviceId);
        this.serviceId=serviceId;
        this.serviceInstanceListSupplierProvider=serviceInstanceListSupplierProvider;
    }


    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        //从当前请求头中获得请求的版本号
        ServiceInstanceListSupplier supplier = (ServiceInstanceListSupplier)this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
        return supplier.get(request).next().map((serviceInstances) -> this.getInstanceResponse(request, serviceInstances));
    }

    public Response<ServiceInstance> getInstanceResponse(Request request, List<ServiceInstance> serviceInstances){
        // 注册中心没有可用的实例
        if (CollUtil.isEmpty(serviceInstances)){
            log.warn("No servers available for service{}",serviceId);
            return new EmptyResponse();
        }
        if (request==null||request.getContext()==null){
            return super.choose(request).block();
        }
        DefaultRequestContext requestContext = (DefaultRequestContext) request.getContext();

        if (!(requestContext.getClientRequest() instanceof RequestData)){
            return super.choose(request).block();
        }

        RequestData requestData = (RequestData) requestContext.getClientRequest();

        String version = requestData.getHeaders().getFirst(CommonConstants.VERSION);

        if (StrUtil.isBlank(version)){
            return super.choose(request).block();
        }
        // 判断nacos中有没有相对应的版本号
        List<ServiceInstance> serviceInstanceList = serviceInstances.stream().filter(serviceInstance -> {
            NacosServiceInstance nacosServiceInstance = (NacosServiceInstance) serviceInstance;
            // 获得当前配置中的元数据信息
            Map<String, String> metadata = nacosServiceInstance.getMetadata();

            String targetVersion = MapUtil.getStr(metadata, CommonConstants.VERSION);
            return version.equalsIgnoreCase(targetVersion);
        }).collect(Collectors.toList());

        if (CollUtil.isNotEmpty(serviceInstanceList)){
            // 从匹配到的结果中随机的返回一个
            ServiceInstance serviceInstance = RandomUtil.randomEle(serviceInstanceList);
            return new DefaultResponse(serviceInstance);
        }else {
            return super.choose(request).block();
        }
    }
}

注册自定义灰度轮询策略

继承LoadBalancerClientConfiguration 配置覆盖掉原有的轮询策略的注册

@Configuration(
        proxyBeanMethods = false
)
@ConditionalOnDiscoveryEnabled
public class GrayLoadBalancerClientConfiguration extends LoadBalancerClientConfiguration {
    /**
     * 注入自定义的灰度策略
     * @param environment
     * @param loadBalancerClientFactory
     * @return
     */
    @Override
    @Bean
    @ConditionalOnMissingBean
    public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new GrayRoundRobinLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
    }
}

客户端负载均衡策略配置

@Configuration
// 配置自定义的负载均衡策略
@LoadBalancerClients(defaultConfiguration = GrayLoadBalancerClientConfiguration.class)
@ConditionalOnProperty(name = "gray.rule.enabled",matchIfMissing = true,havingValue = "true")
public class XlcpGrayAutoConfiguration {

}

2.2.2 feign接口中版本号的传递

通过fegin的内置拦截器,保证版本号的传递

public class GrayFeigeInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
        String version = WebUtils.getRequest().getHeader(CommonConstants.VERSION);
        if (StrUtil.isBlank(version)){
            version= HeaderVersionHolder.getVersion();
        }
        requestTemplate.header(CommonConstants.VERSION,version);
    }
}

通过阿里的ttl保证子线程中version不丢失

/**
 * 解决线程之间version的传递
 * @author likun
 * @date 2022年06月23日 17:06
 */
public class HeaderVersionHolder {

    public static TransmittableThreadLocal<String> VERSION = new TransmittableThreadLocal<String>();

    public static void setVersion(String version){
        VERSION.set(version);
    }

    public static String getVersion(){
        return VERSION.get();
    }

    public static void remove(){
        VERSION.remove();
    }


}

拦截器注册到spring容器中

@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnProperty(name = "gray.rule.enabled",matchIfMissing = true,havingValue = "true")
@AutoConfigureAfter(XlcpGrayAutoConfiguration.class)
public class GrayFeignAutoConfiguration {
    @Bean
    public GrayFeigeInterceptor grayFeigeInterceptor(){
        return new GrayFeigeInterceptor();
    }
}

2.2.3 定义spring.factories

自定义springboot组件--基于nacos和spring-cloud-loadbalancer实现灰度发布_第3张图片

三 客户端调用

自定义springboot组件--基于nacos和spring-cloud-loadbalancer实现灰度发布_第4张图片
idea开启多实例
本地虚拟集群
自定义springboot组件--基于nacos和spring-cloud-loadbalancer实现灰度发布_第5张图片
自定义springboot组件--基于nacos和spring-cloud-loadbalancer实现灰度发布_第6张图片
客户端调用

2.1 客户端直接调用

 @GetMapping("/testGray")
    @Inner(value = false)
    public R testGray(){
        Environment environment = SpringContextHolder.getBean(Environment.class);
        String port = environment.getProperty("server.port");
        log.info("我被调用了,我的端口是:{}",port);
        
        return R.ok("我被调用了,我的端口是:"+port);
    }

调用结果
自定义springboot组件--基于nacos和spring-cloud-loadbalancer实现灰度发布_第7张图片
自定义springboot组件--基于nacos和spring-cloud-loadbalancer实现灰度发布_第8张图片

2.2 内部feign调用

自定义springboot组件--基于nacos和spring-cloud-loadbalancer实现灰度发布_第9张图片
自定义springboot组件--基于nacos和spring-cloud-loadbalancer实现灰度发布_第10张图片

你可能感兴趣的:(自定义springboot组件,spring,spring,boot,java,架构)