Implement Full-Chain Gray Release Using Spring Cloud Gateway, Ribbon & OpenFeign
This article explains how to achieve full‑link gray release in a microservice architecture by marking traffic at the gateway, propagating the gray tag via request headers, customizing Ribbon load‑balancing rules, and using OpenFeign interceptors to ensure gray‑tag transmission to downstream services.
In production, when a requirement changes, the common practice is to cut a small portion of traffic for testing before a full rollout, ensuring most clients remain unaffected if bugs appear.
This approach requires full‑link gray release.
What is Gray Release?
Gray release (also known as canary release) is a deployment method that allows a smooth transition between versions, enabling A/B testing where a subset of users experiences feature B while others keep feature A. If B is accepted, the rollout expands to all users, helping detect and adjust issues early.
Why Full‑Link Gray Release?
Previous articles covered gateway‑level gray release, which only routes traffic at the gateway based on a gray tag, but downstream services (e.g., comment service) may not receive the gray tag, causing calls to the original service.
Full‑link gray release must achieve two points:
Gateway routes traffic with a gray tag to the gray version of the article service.
The article service propagates the gray tag (grayTag) to the comment service, ensuring downstream calls also use the gray version.
Gateway Layer Gray Routing
We use Ribbon + Spring Cloud Gateway to customize the load‑balancing strategy for gray release.
Implementation steps:
In a global filter, add a gray tag to requests based on business rules.
Put the gray tag into the request header for downstream services.
Customize Ribbon’s load‑balancing strategy to select gray services from the registry.
Perform the request routing.
Question 1: What condition adds the gray tag? It depends on business needs (region, client type, random sampling, etc.). Here we simply use a header
grayTag=true.
Request header: grayTag=true
Question 2: Why put the gray tag in the request header? Placing the tag in the header allows downstream services to read it and decide whether to follow the gray logic, similar to token relay.
Question 3: How to isolate the gray tag per request? Store the tag in a
ThreadLocalso each thread has its own copy.
Question 4: How to know which services are gray in the registry? Nacos can store metadata indicating gray services.
Question 5: How to apply gray release only to specific services? Use a custom Ribbon rule that targets only the article service.
Global Filter Implementation
<code>public class GlobalGrayFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//① Parse request header for gray tag and store in ThreadLocal
HttpHeaders headers = exchange.getRequest().getHeaders();
if (headers.containsKey(GrayConstant.GRAY_HEADER)) {
String gray = headers.getFirst(GrayConstant.GRAY_HEADER);
if (StrUtil.equals(gray, GrayConstant.GRAY_VALUE)) {
//② Set gray tag
GrayRequestContextHolder.setGrayTag(true);
}
}
//③ Put gray tag into request header for downstream services
ServerHttpRequest tokenRequest = exchange.getRequest().mutate()
.header(GrayConstant.GRAY_HEADER, GrayRequestContextHolder.getGrayTag().toString())
.build();
ServerWebExchange build = exchange.mutate().request(tokenRequest).build();
return chain.filter(build);
}
}
</code>Note: This global filter must be placed before the OAuth2.0 authentication filter.
Custom Ribbon Rule (GrayRule)
<code>public class GrayRule extends ZoneAvoidanceRule {
@Override
public Server choose(Object key) {
try {
// Get gray tag from ThreadLocal
boolean grayTag = GrayRequestContextHolder.getGrayTag().get();
List<Server> serverList = this.getLoadBalancer().getReachableServers();
List<Server> grayServerList = new ArrayList<>();
List<Server> normalServerList = new ArrayList<>();
for (Server server : serverList) {
NacosServer nacosServer = (NacosServer) server;
if (nacosServer.getMetadata().containsKey(GrayConstant.GRAY_HEADER)
&& nacosServer.getMetadata().get(GrayConstant.GRAY_HEADER).equals(GrayConstant.GRAY_VALUE)) {
grayServerList.add(server);
} else {
normalServerList.add(server);
}
}
// Choose service based on gray tag
if (grayTag) {
return originChoose(grayServerList, key);
} else {
return originChoose(normalServerList, key);
}
} finally {
// Clear gray tag
GrayRequestContextHolder.remove();
}
}
private Server originChoose(List<Server> serverList, Object key) {
Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(serverList, key);
return server.orElse(null);
}
}
</code>GrayRule Configuration
<code>public class GrayRuleConfig {
@Bean
public GrayRule grayRule() {
return new GrayRule();
}
}
</code>Important: Do not let Spring Boot scan this class into the IOC container, otherwise the rule applies globally.
OpenFeign Gray Tag Propagation
To forward the gray tag through Feign calls, implement a
RequestInterceptorthat copies request headers.
<code>@Component
@Slf4j
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
HttpServletRequest httpServletRequest = RequestContextUtils.getRequest();
Map<String, String> headers = getHeaders(httpServletRequest);
for (Map.Entry<String, String> entry : headers.entrySet()) {
//② Set header into new Request
template.header(entry.getKey(), entry.getValue());
}
}
/** Get original request headers */
private Map<String, String> getHeaders(HttpServletRequest request) {
Map<String, String> map = new LinkedHashMap<>();
Enumeration<String> enumeration = request.getHeaderNames();
if (enumeration != null) {
while (enumeration.hasMoreElements()) {
String key = enumeration.nextElement();
String value = request.getHeader(key);
// Save gray tag
if (StrUtil.equals(GrayConstant.GRAY_HEADER, key) && Boolean.TRUE.toString().equals(value)) {
//① Save gray tag
GrayRequestContextHolder.setGrayTag(true);
map.put(key, value);
}
}
}
return map;
}
}
</code>Annotate the target service with
@RibbonClientsto enable gray release for that service.
<code>@RibbonClients(value = {
@RibbonClient(value = "comments", configuration = GrayRuleConfig.class)
})
public class ArticleApplication {}
</code>Marking Services in Nacos
Two ways to add the gray tag:
Specify metadata in the service’s configuration file:
<code>spring:
cloud:
nacos:
discovery:
metadata:
## gray tag
grayTag: true
</code>Dynamically set the metadata in the Nacos console.
After configuration, clients only need to send the header
grayTag=trueto invoke the gray service.
Summary
Gateway adds a gray tag via a global filter and passes it in the request header.
Gateway uses a custom Ribbon rule to select gray services from the registry.
OpenFeign interceptors extract the gray tag from the header and store it in the context.
OpenFeign also uses the custom Ribbon rule to call the appropriate gray service.
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
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.