Implementing Gray Release with Ribbon, Zuul, and Eureka in Spring Cloud
This article explains how to achieve gray release routing in a Spring Cloud microservice architecture by customizing Ribbon's load‑balancing rule, using Eureka metadata, adding version information via Zuul and Feign/RestTemplate interceptors, and configuring thread‑local propagation for consistent service selection.
Preface
In typical business development, backend services call each other through Feign or RestTemplate. When a service name is used for routing, Spring Cloud's Ribbon component automatically provides load‑balancing.
Call Chain Analysis
External Call
Request → zuul → Service
zuul forwards the request by selecting a service instance from the list via Ribbon .
Internal Call
Request → zuul → Service RestTemplate call → Service
Request → zuul → Service Feign call → Service
Both RestTemplate and Feign obtain a service instance from Ribbon before invoking the target service.
Prerequisite Knowledge
Eureka Metadata
Eureka stores two kinds of metadata: standard (host, IP, port, health‑check URL, etc.) and custom metadata, which can be set via eureka.instance.metadata-map.
Standard metadata: hostname, IP address, port, status page, health‑check information, all published to the service registry. Custom metadata: configured with eureka.instance.metadata-map ; readable by remote clients but does not affect client behavior unless the client interprets it.
Eureka RESTful API
Request Name
Method
HTTP Path
Description
Register New Service
POST
/eureka/apps/ {appID} Send JSON or XML; HTTP 204 indicates success.
Deregister Service
DELETE
/eureka/apps/ {appID} / {instanceID} HTTP 200 indicates success.
Send Heartbeat
PUT
/eureka/apps/ {appID} / {instanceID} HTTP 200 indicates success.
Query All Services
GET
/eureka/apps
HTTP 200 returns XML/JSON.
Query Services by AppID
GET
/eureka/apps/ {appID} HTTP 200 returns XML/JSON.
Query Service by AppID & InstanceID
GET
/eureka/apps/ {appID} / {instanceID} HTTP 200 returns XML/JSON.
Change Service Status
PUT
/eureka/apps/ {appID} / {instanceID} /status?value=DOWN
Service up/down; HTTP 200 indicates success.
Change Metadata
PUT
/eureka/apps/ {appID} / {instanceID} /metadata?key=value
HTTP 200 indicates success.
Changing Custom Metadata
Configuration file method: eureka.instance.metadata-map.version = v1 API method:
PUT /eureka/apps/{appID}/{instanceID}/metadata?key=valueImplementation Process
Client request reaches Nginx, then forwards to the gateway zuul. The zuul interceptor extracts a token, parses the userId, and stores it.
The gateway pulls the gray‑user list from Apollo, decides if the user is gray, and if so adds a request header and a thread‑local variable version=xxx. Non‑gray users are passed through unchanged.
After the zuul interceptor finishes, zuul forwards the request using Ribbon for load balancing.
The custom Ribbon rule reads the thread‑local version, obtains the cached service list (periodically refreshed from Eureka), compares each instance's metadata-map.version with the thread variable, and selects a matching instance; if none match, it returns null and an error occurs.
If the target service is non‑gray (no version), Ribbon falls back to its default rule to pick any instance.
When zuul forwards to a consumer service, the consumer’s Feign or RestTemplate interceptor adds the same version header and stores it in a thread variable for the next hop.
Finally, configure the custom rule globally by setting
yourServiceId.ribbon.NFLoadBalancerRuleClassName=YourCustomRuleor by extending RibbonClientConfiguration and PropertiesFactory so the rule applies to all services without per‑service configuration.
Design Idea
Mark a service as gray by adding a custom metadata field version in Eureka. The zuul interceptor adds this version to a thread variable (using HystrixRequestVariableDefault because ThreadLocal is lost across Hystrix thread pools). The custom Ribbon rule GrayMetadataRule reads the thread variable and selects instances whose metadata version matches.
public class GrayMetadataRule extends ZoneAvoidanceRule {
@Override
public Server choose(Object key) {
String version = HystrixRequestVariableDefault.get();
List<Server> serverList = this.getPredicate().getEligibleServers(this.getLoadBalancer().getAllServers(), key);
for (Server server : serverList) {
Map<String, String> metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata();
String metaVersion = metadata.get("version");
if (!StringUtils.isEmpty(metaVersion) && metaVersion.equals(version)) {
return server;
}
}
return null;
}
}Interceptor examples:
public class CoreHttpRequestInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
HttpRequestWrapper requestWrapper = new HttpRequestWrapper(request);
String hystrixVer = CoreHeaderInterceptor.version.get();
requestWrapper.getHeaders().add(CoreHeaderInterceptor.HEADER_VERSION, hystrixVer);
return execution.execute(requestWrapper, body);
}
} public class CoreFeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
String hystrixVer = CoreHeaderInterceptor.version.get();
template.header(CoreHeaderInterceptor.HEADER_VERSION, hystrixVer);
}
}Gray Release Usage
Configuration example (application.yml):
spring.application.name = provide-test
server.port = 7770
eureka.client.service-url.defaultZone = http://localhost:1111/eureka/
# eureka.instance.metadata-map.version = v1 (uncomment to register gray version)Test scenario: start four instances – two provider services (one with version=v1, one without) and two consumer services (similarly). Gray users (e.g., token "andy") should be routed to the versioned instance, while normal users go to the non‑gray instance.
Note: Ribbon caches the service list, so metadata changes are not reflected immediately; cache refresh interval can be configured.
Automation Configuration
Integrate with Apollo to listen for configuration changes, automatically update the gray‑user list and version metadata without manual API calls.
Original article link: https://blog.csdn.net/dupengcheng1/article/details/89187452
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 Architect Essentials
Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow 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.
