Backend Development 21 min read

Implementing Gray Release (Canary Deployment) in Spring Cloud with Nacos and Ribbon

This article explains how to implement gray (canary) release in a Spring Cloud micro‑service system using Nacos for service discovery, Spring Cloud Gateway filters, ThreadLocal gray flags, custom Ribbon load‑balancing rules, and configuration files to switch between production and gray versions.

Top Architect
Top Architect
Top Architect
Implementing Gray Release (Canary Deployment) in Spring Cloud with Nacos and Ribbon

Concept

Gray release (also called canary release) is a gradual deployment method that allows a portion of users to use a new version (B) while the rest continue with the old version (A). It helps discover and fix issues before full rollout.

Component Version

The demo uses Spring Boot 2.3.12.RELEASE, Spring Cloud Hoxton.SR12 and Spring Cloud Alibaba 2.2.9.RELEASE.

Core Components

Registry: Nacos

Gateway: Spring Cloud Gateway

Load balancer: Ribbon (or Spring Cloud LoadBalancer)

RPC: OpenFeign

Gray Release Code Implementation

Gray flag is stored in a ThreadLocal ( GrayFlagRequestHolder ) and set by a pre‑filter in the gateway. The filter checks request headers, IP, city or user list to decide whether to use the gray version.

public class GrayFlagRequestHolder {
    private static final ThreadLocal
grayFlag = new ThreadLocal<>();
    public static void setGrayTag(final GrayStatusEnum tag) { grayFlag.set(tag); }
    public static GrayStatusEnum getGrayTag() { return grayFlag.get(); }
    public static void remove() { grayFlag.remove(); }
}

Pre‑filter ( GrayGatewayBeginFilter ) sets the gray status and adds a custom header to the downstream request.

public class GrayGatewayBeginFilter implements GlobalFilter, Ordered {
    @Autowired
    private GrayGatewayProperties grayGatewayProperties;
    @Override
    public Mono
filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        GrayStatusEnum grayStatusEnum = GrayStatusEnum.ALL;
        if (grayGatewayProperties.getEnabled()) {
            grayStatusEnum = GrayStatusEnum.PROD;
            if (checkGray(exchange.getRequest())) {
                grayStatusEnum = GrayStatusEnum.GRAY;
            }
        }
        GrayFlagRequestHolder.setGrayTag(grayStatusEnum);
        ServerHttpRequest newRequest = exchange.getRequest().mutate()
                .header(GrayConstant.GRAY_HEADER, grayStatusEnum.getVal())
                .build();
        ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
        return chain.filter(newExchange);
    }
    // ... (methods checkGray, checkGrayHeadKey, checkGrayIPList, etc.)
    @Override
    public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; }
}

Post‑filter ( GrayGatewayAfterFilter ) clears the ThreadLocal to avoid memory leaks.

public class GrayGatewayAfterFilter implements GlobalFilter, Ordered {
    @Override
    public Mono
filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        GrayFlagRequestHolder.remove();
        return chain.filter(exchange);
    }
    @Override
    public int getOrder() { return Ordered.LOWEST_PRECEDENCE; }
}

Global exception handler also clears the holder.

public class GrayGatewayExceptionHandler implements WebExceptionHandler, Ordered {
    @Override
    public Mono
handle(ServerWebExchange exchange, Throwable ex) {
        GrayFlagRequestHolder.remove();
        ServerHttpResponse response = exchange.getResponse();
        if (ex instanceof ResponseStatusException) {
            response.setStatusCode(((ResponseStatusException) ex).getStatus());
            return response.setComplete();
        } else {
            response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
            return response.setComplete();
        }
    }
    @Override
    public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; }
}

Custom Ribbon Load Balancer

An abstract rule ( AbstractGrayLoadBalancerRule ) filters the server list according to the gray status stored in GrayFlagRequestHolder . The rule returns all servers for ALL, only production version for PROD, and only gray version for GRAY.

public abstract class AbstractGrayLoadBalancerRule extends AbstractLoadBalancerRule {
    @Autowired
    private GrayVersionProperties grayVersionProperties;
    @Value("${spring.cloud.nacos.discovery.metadata.version}")
    private String metaVersion;
    public List
getReachableServers() { /* ... */ }
    public List
getAllServers() { /* ... */ }
    protected List
getGrayServers(List
servers) { /* ... */ }
}

Custom round‑robin rule ( GrayRoundRobinRule ) extends the abstract rule.

Spring MVC and Feign Interceptors

GrayMvcHandlerInterceptor copies the gray header into the holder; GrayFeignRequestInterceptor adds the gray header to Feign calls.

public class GrayMvcHandlerInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String grayTag = request.getHeader(GrayConstant.GRAY_HEADER);
        if (grayTag != null) {
            GrayFlagRequestHolder.setGrayTag(GrayStatusEnum.getByVal(grayTag));
        }
        return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        GrayFlagRequestHolder.remove();
    }
}
public class GrayFeignRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        GrayStatusEnum grayStatusEnum = GrayFlagRequestHolder.getGrayTag();
        if (grayStatusEnum != null) {
            template.header(GrayConstant.GRAY_HEADER, Collections.singleton(grayStatusEnum.getVal()));
        }
    }
}

Configuration

YAML files define the gray switch, request‑header key/value, IP and city white‑lists, and the version numbers (prodVersion=V1, grayVersion=V2). The gateway enables gray mode and the services register their version in Nacos metadata.

Demo Scenario

Five services are started: a gateway, user‑app V1/V2 and order‑app V1/V2. Different scenarios (gray switch off, only production, header‑based gray) show how requests are routed to the appropriate version.

Source Code

Full source is available at https://gitee.com/kerwin_code/spring-cloud-gray-example .

Open Issues

How to handle gray release for distributed task schedulers (e.g., XXL‑Job).

How to control gray version for MQ messages.

The solution is relatively complex; alternative approaches such as Nginx+Lua routing with separate Nacos namespaces may be simpler.

JavaMicroservicesgray releaseNacosSpring CloudCanary Deploymentribbon
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.