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.
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.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.