下载工作台
Spring Cloud 微服务

灰度发布与流量治理

试读上半部分 · 解锁后可读全文

第 15 章 · 灰度发布与流量治理

本章目标:掌握 金丝雀发布(Canary)蓝绿部署滚动更新 概念;在 gateway-svcNacos 元数据实现基于权重的灰度路由;使用 Spring Cloud LoadBalancer 自定义 灰度规则;结合 K8s / Istio(ch13)理解平台层灰度;建立 发布检查清单快速回滚流程。

学时建议:5~6 小时(含 2 小时双版本联调)

前置spring-cloud-web ch01~ch14;Gateway 路由(ch03);Nacos 元数据(ch02);选修 Istio ch13。


15.1 为什么需要灰度发布

直接将 product-svc v2 全量替换 v1 风险高:

风险灰度缓解
新 Bug 影响全部用户先 5% 流量验证
性能回归未发现对比 v1/v2 指标
数据库迁移不兼容与功能开关配合
发布前:  100% ──► product-svc v1

金丝雀:   90% ──► v1
          10% ──► v2  (观察错误率、P99)

全量:    100% ──► v2  (或回滚到 v1)

15.2 三种部署策略对比

策略原理资源回滚速度适用
滚动更新逐步替换 Pod/实例K8s 默认
蓝绿两套环境切换流量高(双倍)快(切回蓝)关键发布
金丝雀小比例引流新版本快(调权重)持续交付

svc-spring-demo 教学:金丝雀(Nacos 权重 + Gateway Header)。


15.3 Nacos 实例元数据

启动 product-svc 两版本,注册时带 metadata:

# product-svc v1
spring:
  cloud:
    nacos:
      discovery:
        metadata:
          version: v1
          env: prod

# product-svc v2(不同端口或 K8s Deployment)
spring:
  cloud:
    nacos:
      discovery:
        metadata:
          version: v2
          env: prod

Nacos 控制台可设置实例权重(0~1),LoadBalancer 默认考虑权重。


15.4 自定义灰度 LoadBalancer

实现 ReactorServiceInstanceLoadBalancer,按请求头 X-Gray-Version 匹配实例的 Nacos metadata.version

import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.DefaultResponse;
import org.springframework.cloud.client.EmptyResponse;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.RequestDataContext;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.beans.factory.ObjectProvider;
import reactor.core.publisher.Mono;

public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {

    private final ObjectProvider<ServiceInstanceListSupplier> supplier;
    private final String serviceId;

    public GrayLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> supplier, String serviceId) {
        this.supplier = supplier;
        this.serviceId = serviceId;
    }

    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        // Gateway 场景下 context 为 RequestDataContext,可取出原始请求头
        String version = null;
        if (request.getContext() instanceof RequestDataContext ctx) {
            version = ctx.getClientRequest().getHeaders().getFirst("X-Gray-Version");
        }
        ServiceInstanceListSupplier listSupplier = supplier.getIfAvailable();
        if (listSupplier == null) {
            return Mono.just(new EmptyResponse());
        }
        String grayVersion = version;
        return listSupplier.get(request).next()
            .map(instances -> select(instances, grayVersion));
    }

    private Response<ServiceInstance> select(List<ServiceInstance> instances, String version) {
        List<ServiceInstance> candidates = instances;
        if (version != null) {
            List<ServiceInstance> matched = instances.stream()
                .filter(i -> version.equals(i.getMetadata().get("version")))
                .toList();
            if (!matched.isEmpty()) {
                candidates = matched;   // 有匹配走灰度组;无匹配回退全部实例(兜底策略)
            }
        }
        if (candidates.isEmpty()) {
            return new EmptyResponse();
        }
        return new DefaultResponse(
            candidates.get(ThreadLocalRandom.current().nextInt(candidates.size())));
    }
}

注册配置@LoadBalancerClients类型级注解,不能标在 @Bean 方法上):

import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

/** 对所有 lb:// 服务启用灰度 LB;只对个别服务启用改用 @LoadBalancerClient(name = "product-svc", ...) */
@LoadBalancerClients(defaultConfiguration = GrayLoadBalancerClientConfig.class)
@Configuration
public class GrayLoadBalancerClientsConfig {
}

/** 每个服务一个 LB 实例,由 LoadBalancerClientFactory 按服务名创建 */
class GrayLoadBalancerClientConfig {

    @Bean
    ReactorServiceInstanceLoadBalancer grayLoadBalancer(
            Environment environment, LoadBalancerClientFactory factory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new GrayLoadBalancer(
            factory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
    }
}

Gateway 按用户灰度:YAML 过滤器参数里的 SpEL 拿不到 exchange 变量,应写一个 GlobalFilter(写法参考 ch14 的 TenantResolveFilter)按规则注入灰度头:

@Component
public class GrayVersionFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 规则示例:内部员工 Header X-Beta-User: true → v2;或 userId % 100 < 10 → v2
        String version = resolveVersion(exchange);
        if (version == null) {

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。