What is grayscale publishing?

Grayscale publishing (aka Canary publishing) is a publishing method that has a smooth transition between black and white. A/B testing can be carried out on it, that is, some users should continue to use product feature A while some users should start using product feature B. If users have no objection to B, the scope should be gradually expanded and all users should be migrated to B. Gray release can ensure the stability of the overall system, and problems can be found and adjusted at the initial gray level to ensure its impact.

This article uses SpringCloud Gateway + NACOS to demonstrate how to implement grayscale publishing. If you are not familiar with SpringCloud Gateway and NACOS, you can read the following article before reading this article.

Springcloud Gateway official introduction

Nacos official introduction

The overall idea of implementation:

  • Write grayscale routes with weights
  • Write a custom filter
  • Nacos service configuration requires grayscale publishing metadata information and weights for services
  • Grayscale routing pulls metadata information and weights from nacOS services, and then returns the required service instances to the customized filter according to the weight algorithm
  • The gateway profile configures services that require grayscale routing (because this code does not have a gateway for dynamic routing, otherwise the grayscale route can be configured in the configuration center and pulled from the configuration center).
  • The filter passes the service instance to other filters such as NettyRoutingFilter through the chain of responsibility pattern

Let’s go to the actual combat.

The body of the

1. The development version used

< JDK version > 1.8 < / JDK version > <! -- spring cloud --> <spring-cloud.version>Hoxton.SR3</spring-cloud.version> < spring - the boot version > 2.2.5. RELEASE < / spring - the boot. Version > < spring - cloud - alibaba. Version > 2.2.1. RELEASE < / spring - cloud - alibaba. Version >Copy the code

2. Import of POM. XML

   <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
                </exclusion>
            </exclusions>
        </dependency>


        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

    </dependencies>Copy the code

Ps: nacos jar to exclude the ribbon dependencies, otherwise loadbalancer will not take effect

3. Write weighted routes

public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer { private static final Log log = LogFactory.getLog(GrayLoadBalancer.class); private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider; private String serviceId; public GrayLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) { this.serviceId = serviceId; this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider; } @Override public Mono<Response<ServiceInstance>> choose(Request request) { HttpHeaders headers = (HttpHeaders) request.getContext(); if (this.serviceInstanceListSupplierProvider ! = null) { ServiceInstanceListSupplier supplier = (ServiceInstanceListSupplier)this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::ne w); return ((Flux)supplier.get()).next().map(list->getInstanceResponse((List<ServiceInstance>)list,headers)); } return null; } private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances,HttpHeaders headers) { if (instances.isEmpty()) { return getServiceInstanceEmptyResponse(); } else { return getServiceInstanceResponseWithWeight(instances); @param instances * @param headers * @return */ private Response<ServiceInstance> getServiceInstanceResponseByVersion(List<ServiceInstance> instances, HttpHeaders headers) { String versionNo = headers.getFirst("version"); System.out.println(versionNo); Map<String,String> versionMap = new HashMap<>(); versionMap.put("version",versionNo); final Set<Map.Entry<String,String>> attributes = Collections.unmodifiableSet(versionMap.entrySet()); ServiceInstance serviceInstance = null; for (ServiceInstance instance : instances) { Map<String,String> metadata = instance.getMetadata(); if(metadata.entrySet().containsAll(attributes)){ serviceInstance = instance; break; } } if(ObjectUtils.isEmpty(serviceInstance)){ return getServiceInstanceEmptyResponse(); } return new DefaultResponse(serviceInstance); } /** ** According to the weight value configured in nacos, @param instances * * @return */ private Response<ServiceInstance> getServiceInstanceResponseWithWeight(List<ServiceInstance> instances) { Map<ServiceInstance,Integer> weightMap = new HashMap<>(); for (ServiceInstance instance : instances) { Map<String,String> metadata = instance.getMetadata(); System.out.println(metadata.get("version")+"-->weight:"+metadata.get("weight")); if(metadata.containsKey("weight")){ weightMap.put(instance,Integer.valueOf(metadata.get("weight"))); } } WeightMeta<ServiceInstance> weightMeta = WeightRandomUtils.buildWeightMeta(weightMap); if(ObjectUtils.isEmpty(weightMeta)){ return getServiceInstanceEmptyResponse(); } ServiceInstance serviceInstance = weightMeta.random(); if(ObjectUtils.isEmpty(serviceInstance)){ return getServiceInstanceEmptyResponse(); } System.out.println(serviceInstance.getMetadata().get("version")); return new DefaultResponse(serviceInstance); } private Response<ServiceInstance> getServiceInstanceEmptyResponse() { log.warn("No servers available for service: " + this.serviceId); return new EmptyResponse(); }Copy the code

4. Customize the filter

public class GrayReactiveLoadBalancerClientFilter implements GlobalFilter, Ordered { private static final Log log = LogFactory.getLog(ReactiveLoadBalancerClientFilter.class); private static final int LOAD_BALANCER_CLIENT_FILTER_ORDER = 10150; private final LoadBalancerClientFactory clientFactory; private LoadBalancerProperties properties; public GrayReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory, LoadBalancerProperties properties) { this.clientFactory = clientFactory; this.properties = properties; } @Override public int getOrder() { return LOAD_BALANCER_CLIENT_FILTER_ORDER; } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { URI url = (URI)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR); String schemePrefix = (String)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR); if (url ! = null && ("grayLb".equals(url.getScheme()) || "grayLb".equals(schemePrefix))) { ServerWebExchangeUtils.addOriginalRequestUrl(exchange, url); if (log.isTraceEnabled()) { log.trace(ReactiveLoadBalancerClientFilter.class.getSimpleName() + " url before: " + url); } return this.choose(exchange).doOnNext((response) -> { if (! response.hasServer()) { throw NotFoundException.create(this.properties.isUse404(), "Unable to find instance for " + url.getHost()); } else { URI uri = exchange.getRequest().getURI(); String overrideScheme = null; if (schemePrefix ! = null) { overrideScheme = url.getScheme(); } DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance((ServiceInstance)response.getServer(), overrideScheme); URI requestUrl = this.reconstructURI(serviceInstance, uri); if (log.isTraceEnabled()) { log.trace("LoadBalancerClientFilter url chosen: " + requestUrl); } exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, requestUrl); } }).then(chain.filter(exchange)); } else { return chain.filter(exchange); } } protected URI reconstructURI(ServiceInstance serviceInstance, URI original) { return LoadBalancerUriTools.reconstructURI(serviceInstance, original); } private Mono<Response<ServiceInstance>> choose(ServerWebExchange exchange) { URI uri = (URI)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR); GrayLoadBalancer loadBalancer = new GrayLoadBalancer(clientFactory.getLazyProvider(uri.getHost(), ServiceInstanceListSupplier.class), uri.getHost()); if (loadBalancer == null) { throw new NotFoundException("No loadbalancer available for " + uri.getHost()); } else { return loadBalancer.choose(this.createRequest(exchange)); } } private Request createRequest(ServerWebExchange exchange) { HttpHeaders headers = exchange.getRequest().getHeaders(); Request<HttpHeaders> request = new DefaultRequest<>(headers); return request; }}Copy the code

5. Configure a custom filter for Spring management

@Configuration public class GrayGatewayReactiveLoadBalancerClientAutoConfiguration { public GrayGatewayReactiveLoadBalancerClientAutoConfiguration() { } @Bean @ConditionalOnMissingBean({GrayReactiveLoadBalancerClientFilter.class}) public GrayReactiveLoadBalancerClientFilter grayReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory, LoadBalancerProperties properties) { return new GrayReactiveLoadBalancerClientFilter(clientFactory, properties); }}Copy the code

6. Write gateway application.yml configuration

Server: port: 9082 # output log logging configuration: level: org. Springframework. Cloud. The gateway: TRACE org.springframework.http.server.reactive: DEBUG org.springframework.web.reactive: DEBUG reactor.ipc.netty: Management: endpoints: web: exposure: include: '*' Spring: Application: name: gateway-reactor-gray cloud: nacos: discovery: server-addr: localhost:8848 gateway: discovery: locator: enabled: true lower-case-service-id: true routes: - id: hello-consumer uri: grayLb://hello-consumer predicates: - Path=/hello/**Copy the code

The grayLb configuration in the URI indicates that grayscale publishing is required for the service

7, in the registry NACOS to configure grayscale publishing service version and weight value

Weight indicates the weight, and version indicates the version

conclusion

The above is the process of realizing gray scale publishing. There are many ways to realize gray scale publishing. This article only provides a way of thinking. Springcloud officially recommends using loadbalancer instead of the ribbon. The ribbon is blocked. However, the official loadbalancer algorithms only support polling algorithms by default. Other algorithms have to be extended. Second, the ribbon supports lazy load handling, timeouts, and retries integrated with the hystrix circuit breaker. Loadbalancer currently supports retries. So if the formal environment wants to implement grayscale publishing on its own, consider extending the Ribbon. After all, springcloud recommends loadbalancer, so I’ll just write a demo implementation.

Finally, there is an open source implementation of grayscale publishing in the industry –Discovery. Interested friends can check it through the following link

Github.com/Nepxion/Dis…

The demo link

Github.com/lyb-geek/ga…

This article is published by OpenWrite, a platform for operating tools such as mass blog posts