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.

Java Architect Essentials
Java Architect Essentials
Java Architect Essentials
Implementing Gray Release with Ribbon, Zuul, and Eureka in Spring Cloud

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=value

Implementation 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=YourCustomRule

or 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

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

gray releasefeigneurekaSpring CloudresttemplateRibbonZuul
Java Architect Essentials
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.