灰度发布
灰度发布(Canary Release / Gray Release)是将新版本只开放给一部分用户或流量,逐步扩大比例直到全量,以降低发布风险。
核心流程
全量流量
▼
Gateway(流量染色)
├── 规则匹配(Header / Cookie / 用户ID / 比例)
│ ├── 命中灰度规则 → 打上标记 X-Gray: true
│ └── 未命中 → X-Gray: false / 不打标
▼
LoadBalancer(根据标记选实例)
├── X-Gray: true → 路由到 v2 实例
└── X-Gray: false → 路由到 v1 实例
实例通过 Nacos 元数据(metadata) 声明自己的版本:
# v2 实例的 bootstrap.yml
spring:
cloud:
nacos:
discovery:
metadata:
version: v2 # v1 实例写 v1方案一:Nacos 元数据 + 自定义 LoadBalancer
自定义负载均衡策略
@Bean
@LoadBalanced
public ReactorLoadBalancer<ServiceInstance> grayLoadBalancer(
Environment env, LoadBalancerClientFactory factory) {
String name = env.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new GrayLoadBalancer(factory.getLazyProvider(name, ServiceInstanceListSupplier.class));
}
public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
HttpHeaders headers = ((RequestDataContext) request.getContext()).getClientRequest().getHeaders();
boolean isGray = "true".equals(headers.getFirst("X-Gray"));
return serviceInstanceListSupplierProvider.getIfAvailable()
.get(request)
.next()
.map(instances -> {
List<ServiceInstance> targets = instances.stream()
.filter(i -> {
String v = i.getMetadata().get("version");
return isGray ? "v2".equals(v) : !"v2".equals(v);
})
.toList();
// 如果没有目标实例,降级到全量列表
if (targets.isEmpty()) targets = instances;
return new DefaultResponse(targets.get(ThreadLocalRandom.current().nextInt(targets.size())));
});
}
}Gateway 流量染色过滤器
@Component
public class GrayMarkFilter implements GlobalFilter, Ordered {
// 灰度用户列表(实际项目从 Redis / Nacos Config 动态加载)
private final Set<String> grayUserIds = Set.of("user_001", "user_002");
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
boolean isGray = userId != null && grayUserIds.contains(userId);
ServerHttpRequest mutated = exchange.getRequest().mutate()
.header("X-Gray", String.valueOf(isGray))
.build();
return chain.filter(exchange.mutate().request(mutated).build());
}
@Override
public int getOrder() { return -90; }
}方案二:Gateway 按比例分流
Spring Cloud Gateway 内置权重路由,无需修改 LoadBalancer:
spring:
cloud:
gateway:
routes:
- id: order-v1
uri: lb://order-service-v1
predicates:
- Path=/api/order/**
- Weight=group1, 90 # 90% 流量
- id: order-v2
uri: lb://order-service-v2
predicates:
- Path=/api/order/**
- Weight=group1, 10 # 10% 流量(灰度)此方案需要 v1/v2 注册为不同的服务名,适合蓝绿部署场景。
方案三:基于请求头的精准灰度
适合 AB 测试或内测用户:
spring:
cloud:
gateway:
routes:
- id: gray
uri: lb://order-service
predicates:
- Path=/api/order/**
- Header=X-Beta-User, true # 只有携带此头的请求走灰度实例
filters:
- AddRequestHeader=X-Gray, true
- id: stable
uri: lb://order-service
predicates:
- Path=/api/order/**Feign 传递灰度标记
灰度标记需要跨服务传递,否则只有第一跳生效:
@Component
public class GrayFeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 从当前请求上下文取出灰度标记,注入到 Feign 请求
String gray = RequestContextHolder.currentAttributes() != null
? (String) RequestContextHolder.currentAttributes().getAttribute("X-Gray", 0)
: null;
if (gray != null) {
template.header("X-Gray", gray);
}
}
}发布流程
1. 部署 v2 实例(少量,metadata.version=v2)
2. 配置灰度规则(指定用户 or 1% 比例)
3. 观察监控(错误率、P99、业务指标)
4. 逐步扩大灰度比例(5% → 20% → 50% → 100%)
5. 全量后下线 v1 实例
一键回滚:修改 Nacos Config 中的灰度规则,将比例降为 0%,流量立即全部回到 v1。
注意事项
| 问题 | 处理方式 |
|---|---|
| 数据库兼容 | v1/v2 必须兼容同一份 Schema,否则需先做 DB 迁移 |
| 缓存污染 | 灰度请求产生的缓存可能被 v1 读取,注意 key 隔离 |
| 链路标记丢失 | Feign 拦截器需透传 X-Gray 头 |
| 会话粘性 | 同一用户在灰度期间的请求应路由到固定版本,避免体验割裂 |
相关链接
本目录
- API 网关(Gateway 权重路由、过滤器)
- 负载均衡(自定义 LoadBalancer 策略)
- 服务注册与发现(Nacos 元数据)
- Nacos 集成(metadata 配置)
- 服务调用(Feign 传递灰度标记)
- 微服务安全(用户身份染色)
- 多环境管理(环境隔离)
- Spring Cloud 目录