Implementing Gray Release with Spring Cloud Gateway, Zuul and Nacos – A Complete Guide
This article explains the concept of gray (canary) release, compares implementation options such as Nginx + Lua, Netflix Zuul and Spring Cloud Gateway, and provides detailed Java code for custom routing predicates, version handling, and dynamic configuration integration with Nacos.
Gray Release
Gray release (canary deployment) ensures a smooth upgrade by gradually routing a subset of users to new service versions, allowing multiple client and server versions to coexist while maintaining compatibility.
Implementation Options
nginx + lua (openresty)
Netflix Zuul
Customize Ribbon's predicate to obtain the version number from request headers via TTL.
@Slf4j
public class MetadataCanaryRuleHandler extends ZoneAvoidanceRule {
@Override
public AbstractServerPredicate getPredicate() {
return new AbstractServerPredicate() {
@Override
public boolean apply(PredicateKey predicateKey) {
String targetVersion = RibbonVersionHolder.getContext();
RibbonVersionHolder.clearContext();
if (StrUtil.isBlank(targetVersion)) {
log.debug("客户端未配置目标版本直接路由");
return true;
}
DiscoveryEnabledServer server = (DiscoveryEnabledServer) predicateKey.getServer();
Map<String, String> metadata = server.getInstanceInfo().getMetadata();
if (StrUtil.isBlank(metadata.get(SecurityConstants.VERSION))) {
log.debug("当前微服务{} 未配置版本直接路由", server.getInstanceInfo().getAppName());
return true;
}
if (metadata.get(SecurityConstants.VERSION).equals(targetVersion)) {
return true;
} else {
log.debug("当前微服务{} 版本为{},目标版本{} 匹配失败", server.getInstanceInfo().getAppName(), metadata.get(SecurityConstants.VERSION), targetVersion);
return false;
}
}
};
}
}
public class RibbonVersionHolder {
private static final ThreadLocal<String> context = new TransmittableThreadLocal<>();
public static String getContext() { return context.get(); }
public static void setContext(String value) { context.set(value); }
public static void clearContext() { context.remove(); }
}Spring Cloud Gateway Implementation
Because Spring Cloud Gateway is built on WebFlux, traditional ThreadLocal or RequestContextHolder cannot propagate the version information. The solution overrides the choose method to pass request headers to the routing predicate.
public class LoadBalancerClientFilter implements GlobalFilter, Ordered {
@Override
public int getOrder() { return LOAD_BALANCER_CLIENT_FILTER_ORDER; }
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange);
}
protected ServiceInstance choose(ServerWebExchange exchange) {
return loadBalancer.choose(((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost());
}
}Custom predicate that receives the version from the request headers:
protected ServiceInstance choose(ServerWebExchange exchange) {
HttpHeaders headers = exchange.getRequest().getHeaders();
return loadBalancer.choose(((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost(), headers);
}Predicate implementation that matches service metadata with the requested version:
public abstract class AbstractDiscoveryEnabledPredicate extends AbstractServerPredicate {
@Override
public boolean apply(@Nullable PredicateKey input) {
return input != null && input.getServer() instanceof NacosServer &&
apply((NacosServer) input.getServer(), (HttpHeaders) input.getLoadBalancerKey());
}
}
public class GrayMetadataAwarePredicate extends AbstractDiscoveryEnabledPredicate {
@Override
protected boolean apply(NacosServer server, HttpHeaders headers) {
PigxRibbonRuleProperties ribbonProperties = SpringContextHolder.getBean(PigxRibbonRuleProperties.class);
if (!ribbonProperties.isGrayEnabled()) {
log.debug("gray closed,GrayMetadataAwarePredicate return true");
return true;
}
Map<String, String> metadata = server.getMetadata();
String version = metadata.get(CommonConstants.VERSION);
if (StrUtil.isBlank(version)) {
log.debug("nacos server tag is blank ,GrayMetadataAwarePredicate return true");
return true;
}
String target = headers.getFirst(CommonConstants.VERSION);
if (StrUtil.isBlank(target)) {
log.debug("request headers version is blank,GrayMetadataAwarePredicate return true");
return true;
}
log.debug("请求版本:{} ,当前服务版本:{}", target, version);
return target.equals(version);
}
}Integration with Nacos
Dynamic configuration from Nacos makes gray release management straightforward.
Summary
The source code is based on a personal project that uses Spring Cloud, OAuth2.0, and a Vue front‑end for a full‑stack development platform.
Feel free to discuss Spring Cloud usage via QQ: 2270033969.
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.
Java Architecture Diary
Committed to sharing original, high‑quality technical articles; no fluff or promotional content.
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.
