Cloud Native 22 min read

Implementing Gray Release in Spring Cloud Using Nacos and Load Balancer

This article explains how to build a full‑link gray (canary) release for a Spring Cloud microservice system by leveraging Nacos as the service registry, Spring Cloud Gateway filters, a custom Ribbon load‑balancer, and OpenFeign interceptors, complete with demo project structure, configuration files, and three test scenarios.

IoT Full-Stack Technology
IoT Full-Stack Technology
IoT Full-Stack Technology
Implementing Gray Release in Spring Cloud Using Nacos and Load Balancer

Concept

Gray (canary) release routes a subset of traffic to a new version and expands it after verification, ensuring system stability while allowing incremental migration. AB testing is a typical gray‑release pattern.

Component Versions

spring-boot: 2.3.12.RELEASE

spring-cloud-dependencies: Hoxton.SR12

spring-cloud-alibaba-dependencies: 2.2.9.RELEASE

Core Components

Registration Center: Nacos

Gateway: Spring Cloud Gateway

Load Balancer: Ribbon (or Spring Cloud LoadBalancer)

RPC Call: OpenFeign

Gray Flag Holder

A ThreadLocal stores the gray status for each request. Downstream components read and clear this value to avoid memory leaks.

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(); }
}

Gateway Pre‑Filter

The pre‑filter decides whether a request should be treated as gray based on a switch, request header, IP list, city list, or user list. It writes the selected GrayStatusEnum into the request header and stores it in GrayFlagRequestHolder.

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);
    }
    // checkGray, checkGrayHeadKey, checkGrayIPList, checkGrayCiryList, checkGrayUserNoList ...
    @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; }
}

Gateway Post‑Filter

After the downstream call finishes, the post‑filter clears the ThreadLocal to prevent 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; }
}

Global Exception Handler

If an exception occurs before the post‑filter runs, the handler also removes the holder and sets an appropriate HTTP status.

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‑Balancing

An abstract rule filters the service list according to the gray status stored in GrayFlagRequestHolder. It returns all servers for ALL, only production‑version servers for PROD, and only gray‑version servers for GRAY.

public abstract class AbstractGrayLoadBalancerRule extends AbstractLoadBalancerRule {
    @Autowired private GrayVersionProperties grayVersionProperties;
    @Value("${spring.cloud.nacos.discovery.metadata.version}") private String metaVersion;
    @Override
    public List<Server> getReachableServers() {
        ILoadBalancer lb = getLoadBalancer();
        if (lb == null) return new ArrayList<>();
        List<Server> reachable = lb.getReachableServers();
        return getGrayServers(reachable);
    }
    protected List<Server> getGrayServers(List<Server> servers) {
        List<Server> result = new ArrayList<>();
        if (servers == null) return result;
        String currentVersion = metaVersion;
        GrayStatusEnum status = GrayFlagRequestHolder.getGrayTag();
        if (status != null) {
            switch (status) {
                case ALL: return servers;
                case PROD: currentVersion = grayVersionProperties.getProdVersion(); break;
                case GRAY: currentVersion = grayVersionProperties.getGrayVersion(); break;
            }
        }
        for (Server server : servers) {
            NacosServer ns = (NacosServer) server;
            String version = ns.getMetadata().get("version");
            if (version != null && version.equals(currentVersion)) {
                result.add(server);
            }
        }
        return result;
    }
}

A concrete GrayRoundRobinRule extends this abstract class and reuses Ribbon’s round‑robin algorithm.

Business Service Gray Design

Spring MVC Interceptor

The interceptor copies the gray header from the upstream request into GrayFlagRequestHolder so that downstream Feign calls inherit the flag.

@SuppressWarnings("all")
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();
    }
}

OpenFeign Interceptor

The Feign interceptor reads the flag from the holder and adds the gray header to outbound Feign requests.

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","can call all versions"),
    PROD("PROD","only production version"),
    GRAY("GRAY","only 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 create the gateway filters, MVC interceptor, and Feign interceptor only when the corresponding classes are present and the property kerwin.tool.gray.load=true is set.

@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 Structure

spring-cloud-gray-example // parent project
  ├─ kerwin-common // common module
  ├─ kerwin-gateway // gateway service
  ├─ kerwin-order
  │   └─ order-app // order business service
  ├─ kerwin-starter
  │   └─ spring-cloud-starter-kerwin-gray // core gray code
  └─ kerwin-user
      └─ user-app // user business service

Running the Demo

Five services are started: one gateway, user‑v1, order‑v1, user‑v2, order‑v2. Nacos global configuration defines the production version V1 and gray version V2. Each service registers its version in metadata (e.g., -Dspring.cloud.nacos.discovery.metadata.version=V1 for production).

Gateway configuration ( gateway‑app.yaml) enables gray, sets the header key/value, and optionally lists IPs or cities for gray routing.

Gray Effect Demonstration

Scenario 1 – Gray switch off: All requests may hit any version because the gateway does not evaluate gray rules.

Scenario 2 – Gray switch on, only production: With no matching IP/header, the gateway forces PROD and routes to V1 services.

Scenario 3 – Gray switch on, header match: Sending gray=gray-996 forces GRAY and routes to V2 services.

Source Code

https://gitee.com/kerwin_code/spring-cloud-gray-example

Open Issues & Suggested Solutions

Distributed task scheduling (e.g., XXL‑Job): Register separate executors for gray and production versions; gray deployments use the gray executor.

Message queues: Deploy separate MQ clusters for gray and production, routing messages from gray services to the gray MQ cluster.

Complexity reduction: Isolate gray services in a dedicated Nacos namespace and let Nginx + Lua perform routing, keeping configuration independent from the Spring Cloud stack.

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 ReleaseNacosSpring CloudOpenFeignLoad BalancerCanary DeploymentRibbon
IoT Full-Stack Technology
Written by

IoT Full-Stack Technology

Dedicated to sharing IoT cloud services, embedded systems, and mobile client technology, with no spam ads.

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.