End-to-End Gray Release with Spring Cloud, Nacos, and Load Balancer – A Practical Guide
This article walks through a complete gray‑release solution built on Spring Cloud, Nacos as the service registry, and a custom load‑balancer, detailing the architecture, code implementation, configuration, and step‑by‑step deployment to demonstrate traffic splitting and version routing.
Concept
Gray release (also called canary release) enables a smooth transition between versions by routing a portion of traffic to a new version (B) while the rest continues using the old version (A). The approach can be refined by assigning lower weights to a small set of servers and gradually increasing them, a technique known as traffic splitting.
Component Versions
spring-boot: 2.3.12.RELEASE
spring-cloud-dependencies: Hoxton.SR12
spring-cloud-alibaba-dependencies: 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
The solution stores a version value in Nacos metadata. When a downstream service is called, the version determines which service instance to invoke.
spring-cloud-gray-example // parent project
kerwin-common // common module
kerwin-gateway // gateway module
kerwin-order // order module
order-app // order business service
kerwin-starter // custom Spring Boot starter
spring-cloud-starter-kerwin-gray // gray release starter (core code)
kerwin-user // user module
user-app // user business service
user-client // Feign client and DTOGateway Pre‑Filter
The pre‑filter decides whether the request should be routed to a gray version and stores the gray status in a ThreadLocal holder.
public class GrayFlagRequestHolder {
private static final ThreadLocal<GrayStatusEnum> 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(); }
} public class GrayGatewayBeginFilter implements GlobalFilter, Ordered {
@Autowired private GrayGatewayProperties grayGatewayProperties;
@Override public Mono<Void> 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);
}
private boolean checkGray(ServerHttpRequest request) {
return checkGrayHeadKey(request) || checkGrayIPList(request) ||
checkGrayCiryList(request) || checkGrayUserNoList(request);
}
// ... implementations of checkGrayHeadKey, checkGrayIPList, checkGrayCiryList, checkGrayUserNoList ...
@Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; }
}Post‑Filter and Exception Handler
Both clear the ThreadLocal to avoid memory leaks.
public class GrayGatewayAfterFilter implements GlobalFilter, Ordered {
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
GrayFlagRequestHolder.remove();
return chain.filter(exchange);
}
@Override public int getOrder() { return Ordered.LOWEST_PRECEDENCE; }
}
public class GrayGatewayExceptionHandler implements WebExceptionHandler, Ordered {
@Override public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
GrayFlagRequestHolder.remove();
ServerHttpResponse response = exchange.getResponse();
if (ex instanceof ResponseStatusException) {
response.setStatusCode(((ResponseStatusException) ex).getStatus());
} else {
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
}
return response.setComplete();
}
@Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; }
}Custom Ribbon Load‑Balancer Rule
The rule selects servers based on the gray status stored in GrayFlagRequestHolder. It reads the target version from GrayVersionProperties and matches it against the version metadata of each Nacos server.
public abstract class AbstractGrayLoadBalancerRule extends AbstractLoadBalancerRule {
@Autowired private GrayVersionProperties grayVersionProperties;
@Value("${spring.cloud.nacos.discovery.metadata.version}") private String metaVersion;
public List<Server> getReachableServers() {
ILoadBalancer lb = getLoadBalancer();
if (lb == null) return new ArrayList<>();
return getGrayServers(lb.getReachableServers());
}
public List<Server> getAllServers() {
ILoadBalancer lb = getLoadBalancer();
if (lb == null) return new ArrayList<>();
return getGrayServers(lb.getAllServers());
}
protected List<Server> getGrayServers(List<Server> servers) {
List<Server> result = new ArrayList<>();
if (servers == null) return result;
String currentVersion = metaVersion;
GrayStatusEnum grayStatusEnum = GrayFlagRequestHolder.getGrayTag();
if (grayStatusEnum != null) {
switch (grayStatusEnum) {
case PROD: currentVersion = grayVersionProperties.getProdVersion(); break;
case GRAY: currentVersion = grayVersionProperties.getGrayVersion(); break;
default: break;
}
}
for (Server server : servers) {
NacosServer nacosServer = (NacosServer) server;
String version = nacosServer.getMetadata().get("version");
if (version != null && version.equals(currentVersion)) {
result.add(server);
}
}
return result;
}
}A concrete GrayRoundRobinRule can extend this abstract class to reuse the gray‑aware server selection logic.
Spring MVC and Feign Interceptors
The MVC interceptor extracts the gray header from incoming requests and stores it in the holder; the Feign interceptor propagates the stored gray status to downstream 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()));
}
}
}Basic Configuration Classes
public interface GrayConstant { String GRAY_HEADER = "gray"; }
public enum GrayStatusEnum {
ALL("ALL", "call all versions"),
PROD("PROD", "call production version"),
GRAY("GRAY", "call gray version");
private final String val; private final String desc;
GrayStatusEnum(String val, String desc) { this.val = val; this.desc = desc; }
public String getVal() { return val; }
public static GrayStatusEnum getByVal(String val) {
if (val == null) return null;
for (GrayStatusEnum e : values()) { if (e.val.equals(val)) return e; }
return null;
}
}
@Data @Configuration @RefreshScope @ConfigurationProperties("kerwin.tool.gray.gateway")
public class GrayGatewayProperties {
private Boolean enabled = false;
private String grayHeadKey = "gray";
private String grayHeadValue = "gray-996";
private List<String> grayIPList = new ArrayList<>();
private List<String> grayCityList = new ArrayList<>();
private List<String> grayUserNoList = new ArrayList<>();
}
@Data @Configuration @RefreshScope @ConfigurationProperties("kerwin.tool.gray.version")
public class GrayVersionProperties {
private String prodVersion;
private String grayVersion;
}Auto‑Configuration
Conditional beans load the gateway filters, MVC interceptor, and Feign interceptor when the gray feature is enabled.
@Configuration
@ConditionalOnProperty(value = "kerwin.tool.gray.load", havingValue = "true")
@EnableConfigurationProperties(GrayVersionProperties.class)
public class GrayAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(GlobalFilter.class)
@EnableConfigurationProperties(GrayGatewayProperties.class)
static class GrayGatewayFilterAutoConfiguration {
@Bean public GrayGatewayBeginFilter grayGatewayBeginFilter() { return new GrayGatewayBeginFilter(); }
@Bean public GrayGatewayAfterFilter grayGatewayAfterFilter() { return new GrayGatewayAfterFilter(); }
@Bean public GrayGatewayExceptionHandler grayGatewayExceptionHandler() { return new GrayGatewayExceptionHandler(); }
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(WebMvcConfigurer.class)
static class GrayWebMvcAutoConfiguration {
@Bean public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurer() {
@Override public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new GrayMvcHandlerInterceptor());
}
};
}
}
@Configuration
@ConditionalOnClass(RequestInterceptor.class)
static class GrayFeignInterceptorAutoConfiguration {
@Bean public GrayFeignRequestInterceptor grayFeignRequestInterceptor() { return new GrayFeignRequestInterceptor(); }
}
}Project Run Configuration
Five services are started: one gateway, user‑app V1, order‑app V1, user‑app V2, and order‑app V2. Nacos namespace is spring-cloud-gray-example.
# common-config.yaml (global)
kerwin:
tool:
gray:
load: true
version:
prodVersion: V1
grayVersion: V2
user-app:
ribbon:
NFLoadBalancerRuleClassName: com.kerwin.gray.loadbalancer.GrayRoundRobinRule
order-app:
ribbon:
NFLoadBalancerRuleClassName: com.kerwin.gray.loadbalancer.GrayRoundRobinRule # gateway-app.yaml
kerwin:
tool:
gray:
gateway:
enabled: true
grayHeadKey: gray
grayHeadValue: gray-996
grayIPList:
- '127.0.0.1'
grayCityList:
- 本地Each service is launched with VM options that set the server port and Nacos metadata version, e.g.
-Dserver.port=7201 -Dspring.cloud.nacos.discovery.metadata.version=V1for the V1 instance.
Gray Release Demonstration
Scenario 1 – Gray Switch Off (no version distinction)
Set kerwin.tool.gray.load to false or set kerwin.tool.gray.gateway.enabled to false. Calls may hit any combination of V1 and V2 services.
Scenario 2 – Gray Switch On (only production version)
Enable the gateway switch; because the IP and city lists do not match, all traffic is routed to the production version (V1).
Scenario 3 – Gray Switch On with Header/IP/City Matching
Sending the request header gray=gray-996 forces traffic to the gray version (V2).
Source Code
https://gitee.com/kerwin_code/spring-cloud-gray-example
Open Issues
If distributed task scheduling (e.g., XXL‑Job) is used, register separate executors for gray versions.
If MQ is used, deploy parallel MQ clusters and let gray services publish to the gray MQ.
The presented approach is one possible implementation; alternatives such as Nginx + Lua routing with separate Nacos namespaces can also achieve isolation.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer XiaoFu
xiaofucode.com – a programmer learning guide driven by the pursuit of profit
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.
