在微服务架构中,系统升级已经成为日常工作。
然而对于线上系统来说,最担心的问题往往不是发布本身,而是:
新版本存在未知 Bug
数据兼容性问题
服务接口变更
第三方依赖异常
高并发场景下的隐藏问题
如果采用传统全量发布方式,一旦出现问题,影响范围往往是全部用户。
因此,大部分互联网公司都会采用一种更加稳妥的上线方式:
灰度发布(Canary Release)。
即:
大部分用户继续访问旧版本
小部分用户优先访问新版本
验证稳定后逐步扩大流量
本文结合 Spring Cloud + Nacos 项目实践,介绍一种基于:
Spring Cloud Gateway
Nacos
OpenFeign
Spring Cloud LoadBalancer
实现的全链路灰度发布方案。
什么是全链路灰度发布
正常调用链:
Gateway
│
▼
Order Service
│
▼
User Service
灰度用户访问:
Gateway
│
▼
Order Service V2
│
▼
User Service V2
错误场景:
Gateway
│
▼
Order Service V2
│
▼
User Service V1
如果出现版本混用,容易导致:
DTO 不兼容
RPC 调用失败
数据异常
缓存污染
因此灰度发布真正的核心并不是流量入口,而是:
保证一次请求在整个微服务调用链中始终访问同一个版本服务。
整体架构设计
本文采用:
Spring Cloud Gateway
Nacos
OpenFeign
Spring Cloud LoadBalancer
实现全链路灰度。
系统整体架构
用户请求
│
▼
Spring Cloud Gateway
│
┌─────────────┴─────────────┐
│ │
▼ ▼
普通流量(V1) 灰度流量(V2)
│ │
▼ ▼
Order Service V1 Order Service V2
│ │
▼ ▼
User Service V1 User Service V2
│ │
└─────────┬─────────────────┘
│
▼
Nacos Registry
version=V1
version=V2
整体流程:
普通用户
Gateway
↓
Order V1
↓
User V1
灰度用户
Gateway
↓
Order V2
↓
User V2服务版本标识设计
灰度路由的基础是服务实例版本标识。
服务启动时向 Nacos 注册自己的版本号。
V1:
spring:
cloud:
nacos:
discovery:
metadata:
version: V1V2:
spring:
cloud:
nacos:
discovery:
metadata:
version: V2Nacos 实例版本示意图
user-service
├── 192.168.1.10:7201
│ version=V1
│
├── 192.168.1.11:7202
│ version=V2
│
├── 192.168.1.12:7201
│ version=V1
│
└── 192.168.1.13:7202
version=V2负载均衡器根据:
instance.getMetadata().get("version")完成实例筛选。
Gateway 灰度流量识别
Gateway 是整个系统流量入口。
所有请求进入系统时首先判断:
用户ID
租户ID
IP白名单
城市
百分比流量
自定义请求头
例如测试人员请求:
gray: gray-996命中灰度规则后:
exchange.getAttributes().put(
"gray",
GrayStatusEnum.GRAY
);同时写入请求头:
request.mutate()
.header("gray","GRAY")
.build();继续向下游传递。
需要注意的是:
Spring Cloud Gateway 基于 WebFlux 和 Reactor 实现,属于异步响应式框架,请求处理过程中可能发生线程切换,因此不建议在 Gateway 中使用 ThreadLocal 保存灰度状态。
Gateway 层推荐使用:
ServerWebExchange Attribute或者:
Reactor Context保存灰度信息。
生产环境中,请求头灰度主要用于测试验证。
真正的灰度规则通常来自:
用户标签系统
租户维度
区域维度
百分比流量控制
AB实验平台
灰度状态传递
识别灰度用户只是第一步。
更重要的是:
保证整个调用链都能感知当前请求所属版本。
灰度标记传递流程
用户请求
│
│ Header(gray=gray-996)
▼
Gateway
│
│ 灰度规则判断
▼
Order Service
│
│ MVC Interceptor
│
│ ThreadLocal
▼
Feign 调用
│
│ Header(gray=GRAY)
▼
User Service
│
│ MVC Interceptor
│
│ ThreadLocal
▼
业务处理推荐方案:
Gateway:
Exchange Attribute
↓
Request Header业务服务:
Spring MVC Interceptor
↓
ThreadLocal
↓
Feign Interceptor这样既保证灰度信息传递,又避免了 Gateway 中 ThreadLocal 的问题。
ThreadLocal 保存灰度状态
业务服务内部仍然可以使用 ThreadLocal 保存当前请求灰度状态。
public class GrayContextHolder {
private static final ThreadLocal<String>
VERSION_HOLDER = new ThreadLocal<>();
public static void set(String version){
VERSION_HOLDER.set(version);
}
public static String get(){
return VERSION_HOLDER.get();
}
public static void remove(){
VERSION_HOLDER.remove();
}
}在 Spring MVC Interceptor 中写入:
GrayContextHolder.set(
request.getHeader("gray")
);请求结束后必须清理:
GrayContextHolder.remove();OpenFeign 透传灰度标识
订单服务调用用户服务时:
Order Service
↓
User Service需要继续传递灰度信息。
实现 Feign 拦截器:
public class GrayFeignRequestInterceptor
implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
String version = GrayContextHolder.get();
if(version != null){
template.header(
"gray",
version
);
}
}
}
这样整个调用链都会保持统一版本。
基于 LoadBalancer 实现灰度路由
Ribbon 已进入维护状态。
Spring Cloud 官方推荐:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>
spring-cloud-starter-loadbalancer
</artifactId>
</dependency>
LoadBalancer 路由流程
请求进入服务
│
▼
获取灰度状态
│
┌────────────┴────────────┐
▼ ▼
PROD GRAY
│ │
▼ ▼
获取V1实例列表 获取V2实例列表
│ │
▼ ▼
LoadBalancer LoadBalancer
选择实例 选择实例
│ │
▼ ▼
V1实例 V2实例核心逻辑:
instance.getMetadata().get("version")根据当前灰度状态筛选对应版本实例。
示例:
String version = GrayContextHolder.get();
if ("GRAY".equals(version)) {
return instances.stream()
.filter(instance ->
"V2".equals(
instance.getMetadata()
.get("version")
))
.toList();
}最终实现:
正式用户
→ V1集群
灰度用户
→ V2集群生产环境踩坑总结
1.ThreadLocal 内存泄漏
很多项目都会忽略这一点。
必须在请求结束后清理:
GrayContextHolder.remove();推荐在:
MVC Interceptor
Filter
全局异常处理器
统一处理。
否则线程池复用后可能导致灰度状态污染。
2.MQ 消息兼容问题
例如:
Order V2
发送消息
User V1
消费消息当消息结构发生变化:
{
"newField":"xxx"
}旧版本消费者可能直接失败。
实际生产环境更推荐:
order-created-prod
order-created-gray通过不同 Topic 实现流量隔离。
3.XXL-Job 调度隔离
灰度环境中的定时任务需要避免被生产节点执行。
常见方案包括:
执行器隔离
Job分组隔离
路由策略控制
命名空间隔离
例如:
executor-prod
executor-gray4.Redis 缓存兼容问题
如果对象结构发生不兼容变更:
Redis
├── user:v1:1001
└── user:v2:1001建议采用缓存版本化策略,避免数据污染。
性能影响分析
增加灰度能力后主要增加:
Header透传
Metadata过滤
ThreadLocal读写
LoadBalancer实例筛选
这些操作本身开销较小。
实际性能损耗与:
灰度规则复杂度
用户标签系统
配置中心访问频率
服务实例规模
关系更大。
因此建议在上线前结合实际业务场景进行压测验证。
我的实践建议
对于中小型项目:
推荐本文方案。
优点:
实现简单
改造成本低
与 Spring Cloud 无缝集成
对于大型系统:
Gateway
+ 独立Nacos命名空间
+ Topic隔离
+ 独立Job集群
+ Redis隔离策略形成完整灰度环境。
实现:
服务隔离
配置隔离
消息隔离
调度隔离
缓存隔离
整体稳定性更高。
总结
灰度发布的本质并不是流量切换。
真正困难的是:
保证一次请求在整个微服务调用链中始终访问同一个版本服务。
本文利用:
Nacos Metadata
Gateway Filter
OpenFeign
ThreadLocal
Spring Cloud LoadBalancer
实现了一套完整的全链路灰度方案。
最终达到:
灰度用户
→ 全链路访问 V2
正式用户
→ 全链路访问 V1在保证系统稳定性的同时,大幅降低线上发布风险。
如果你的项目已经使用 Spring Cloud + Nacos,那么这套方案完全可以在现有架构上快速落地。