Custom springboot components--based on nacos and spring-cloud-loadbalancer to achieve grayscale release

A grayscale release

Grayscale release is also called canary release. The origin is that mine workers discovered that canaries are very sensitive to gas. Miners will put a canary into the well before going down the well. If the canary stops calling, It means that the gas concentration is high.
insert image description here
After the grayscale release starts, a new version of the application is launched first, but the traffic is not directly cut, but the tester conducts an online test on the new version, and the new version of the application launched is our canary. If there is no problem, you can import a small amount of user traffic to the new version, and then observe the running status of the new version and collect various runtime data. If you compare various data between the old and new versions at this time, it is the so-called A /B test.

After confirming that the new version is running well, gradually import more traffic to the new version. During this period, the number of running server copies of the old and new versions can be continuously adjusted so that the new version can withstand the increasing traffic. High flow pressure. Until 100% of the traffic is switched to the new version, and finally the remaining old version services are closed to complete the grayscale release.

If a problem with the new version is found during the grayscale release process (greyscale period), the traffic should be switched back to the old version immediately, so that the negative impact will be kept to a minimum.

Two implementation ideas

2.1 Basic ideas

Spring Cloud LoadBalancer is aClient Load Balancer, similar to Ribbon, but since Ribbon has entered maintenance mode, and Ribbon 2 is not compatible with Ribbon 1, the Spring Cloud family bucket adds Spring cloud Loadbalancer as a new load balancer in the Spring Cloud Commons project, and does Forward compatibility;

This article is mainly based on Spring Cloud LoadBalancer, rewriting the default client load balancer of Spring Cloud LoadBalancer to achieve gray balance
insert image description here

We can see that Spring Cloud LoadBalancer provides relatively few load balancing strategies, built-inpollingrandomThe load balancing strategy is the default polling strategy. This article mainly overrides the request header of the parsing request of the polling strategy to obtain the version of the current request, and matches it from nacos to the corresponding version instance to realize the load balancing of the gray version.

2.2 Code implementation

2.2.1 Custom Grayscale Load Balancing Strategy

Rewrite the polling strategy to implement a custom grayscale strategy

The custom grayscale strategy inherits the polling strategy, rewrites the choose method, parses the version number in the current request header and compares it with the version number in the nacos metadata, and returns the corresponding service instance after matching.

/**
 *  自定义灰度校验策略
 * @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();
        }
    }
}

Register a custom grayscale polling strategy

Inherit the LoadBalancerClientConfiguration configuration to overwrite the registration of the original polling strategy

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

Client load balancing policy configuration

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

}

2.2.2 Transfer of version number in feign interface

Through fegin's built-in interceptor, the delivery of the version number is guaranteed

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

Through Ali's ttl to ensure that the version in the child thread is not lost

/**
 * 解决线程之间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();
    }


}

The interceptor is registered in the spring container

@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 Define spring.factories

insert image description here

Three client calls

insert image description here
idea enables multi-instance
local virtual cluster
insert image description here
insert image description here
client calls

2.1 The client directly calls

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

call result
insert image description here
insert image description here

2.2 Internal feign call

insert image description here
insert image description here

Guess you like

Origin blog.csdn.net/Instanceztt/article/details/125440225