Cloud Native 21 min read

End-to-End Gray Release with Spring Cloud, Nacos, and Load Balancer – A Practical Guide

This article walks through a complete gray‑release solution built on Spring Cloud, Nacos as the service registry, and a custom load‑balancer, detailing the architecture, code implementation, configuration, and step‑by‑step deployment to demonstrate traffic splitting and version routing.

Programmer XiaoFu
Programmer XiaoFu
Programmer XiaoFu
End-to-End Gray Release with Spring Cloud, Nacos, and Load Balancer – A Practical Guide

Concept

Gray release (also called canary release) enables a smooth transition between versions by routing a portion of traffic to a new version (B) while the rest continues using the old version (A). The approach can be refined by assigning lower weights to a small set of servers and gradually increasing them, a technique known as traffic splitting.

Component Versions

spring-boot: 2.3.12.RELEASE

spring-cloud-dependencies: Hoxton.SR12

spring-cloud-alibaba-dependencies: 2.2.9.RELEASE

Core Components

Registry: Nacos

Gateway: Spring Cloud Gateway

Load Balancer: Ribbon (or Spring Cloud LoadBalancer)

RPC: OpenFeign

Gray Release Code Implementation

The solution stores a version value in Nacos metadata. When a downstream service is called, the version determines which service instance to invoke.

spring-cloud-gray-example // parent project
  kerwin-common // common module
  kerwin-gateway // gateway module
  kerwin-order // order module
    order-app // order business service
  kerwin-starter // custom Spring Boot starter
    spring-cloud-starter-kerwin-gray // gray release starter (core code)
  kerwin-user // user module
    user-app // user business service
    user-client // Feign client and DTO

Gateway Pre‑Filter

The pre‑filter decides whether the request should be routed to a gray version and stores the gray status in a ThreadLocal holder.

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

Post‑Filter and Exception Handler

Both clear the ThreadLocal to avoid 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; }
}

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‑Balancer Rule

The rule selects servers based on the gray status stored in GrayFlagRequestHolder. It reads the target version from GrayVersionProperties and matches it against the version metadata of each Nacos server.

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

A concrete GrayRoundRobinRule can extend this abstract class to reuse the gray‑aware server selection logic.

Spring MVC and Feign Interceptors

The MVC interceptor extracts the gray header from incoming requests and stores it in the holder; the Feign interceptor propagates the stored gray status to downstream calls.

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

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", "call all versions"),
    PROD("PROD", "call production version"),
    GRAY("GRAY", "call 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 load the gateway filters, MVC interceptor, and Feign interceptor when the gray feature is enabled.

@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 Run Configuration

Five services are started: one gateway, user‑app V1, order‑app V1, user‑app V2, and order‑app V2. Nacos namespace is spring-cloud-gray-example.

# common-config.yaml (global)
kerwin:
  tool:
    gray:
      load: true
      version:
        prodVersion: V1
        grayVersion: V2
      user-app:
        ribbon:
          NFLoadBalancerRuleClassName: com.kerwin.gray.loadbalancer.GrayRoundRobinRule
      order-app:
        ribbon:
          NFLoadBalancerRuleClassName: com.kerwin.gray.loadbalancer.GrayRoundRobinRule
# gateway-app.yaml
kerwin:
  tool:
    gray:
      gateway:
        enabled: true
        grayHeadKey: gray
        grayHeadValue: gray-996
        grayIPList:
          - '127.0.0.1'
        grayCityList:
          - 本地

Each service is launched with VM options that set the server port and Nacos metadata version, e.g.

-Dserver.port=7201 -Dspring.cloud.nacos.discovery.metadata.version=V1

for the V1 instance.

Gray Release Demonstration

Scenario 1 – Gray Switch Off (no version distinction)

Set kerwin.tool.gray.load to false or set kerwin.tool.gray.gateway.enabled to false. Calls may hit any combination of V1 and V2 services.

Scenario 2 – Gray Switch On (only production version)

Enable the gateway switch; because the IP and city lists do not match, all traffic is routed to the production version (V1).

Scenario 3 – Gray Switch On with Header/IP/City Matching

Sending the request header gray=gray-996 forces traffic to the gray version (V2).

Source Code

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

Open Issues

If distributed task scheduling (e.g., XXL‑Job) is used, register separate executors for gray versions.

If MQ is used, deploy parallel MQ clusters and let gray services publish to the gray MQ.

The presented approach is one possible implementation; alternatives such as Nginx + Lua routing with separate Nacos namespaces can also achieve isolation.

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.

microservicesGray ReleaseNacosSpring CloudLoad Balancer
Programmer XiaoFu
Written by

Programmer XiaoFu

xiaofucode.com – a programmer learning guide driven by the pursuit of profit

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.