基于 GateWay 和 Nacos 实现微服务架构灰度发布方案
Posted 小毕超
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于 GateWay 和 Nacos 实现微服务架构灰度发布方案相关的知识,希望对你有一定的参考价值。
一、灰度发布
灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing
,即让一部分用户继续用产品特性A
,一部分用户开始用产品特性B
,如果用户对B
没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B
上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。
灰度发布开始到结束期间的这一段时间,称为灰度期。灰度发布能及早获得用户的意见反馈,完善产品功能,提升产品质量,让用户参与产品测试,加强与用户互动,降低产品升级所影响的用户范围。
下面基于 GateWay
和 Nacos
实现微服务架构灰度发布方案,首先对生产的服务和灰度环境的服务统一注册到 Nacos
中,但是版本不同,比如生产环境版本为 1.0
,灰度环境版本为 2.0
,请求经过网关后,判断携带的用户是否为灰度用户,如果是将请求转发至 2.0
的服务中,否则转发到 1.0
的服务中。
二、开始实施
首先搭建两个web服务模拟生产和灰度环境,分别注册到nacos 中,注意这里服务ID 要一致:
生产环境配置:
spring:
application:
name: web
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
metadata:
version: 1.0 # 指定版本号
灰度环境配置:
spring:
application:
name: web
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
metadata:
version: 2.0 # 指定版本号
启动两个服务后,可以在nacos 中查看详情:
下面为了模拟两个服务的差异性,创建相同的接口,不同的返回:
@RestController
public class TestController
@GetMapping("/getTest")
public String getTest()
return "当前处于-生产环境!";
@RestController
public class TestController
@GetMapping("/getTest")
public String getTest()
return "当前处于-灰度环境!";
下面开始搭建 GateWay 网关,同样需要注册到 nacos 中,但是和以前不同的是,这里我们要实现一个负载均衡器,在负载均衡器中判断是否使用哪个版本的服务,这里为了演示效果,在nacos 中新建一个配置文件,将灰度用户配置在这个配置文件中,在项目中应该从 db 或 noSQL 中进行获取。
Data ID: env-config.yaml
Group: DEFAULT_GROUP
env:
gray:
version: 2.0
users: abc,ii,ss,kk,bb,pp
pro:
version: 1.0
再增加一个 GateWay 路由的配置:
Data ID:gateway.yaml
Group: DEFAULT_GROUP
spring:
cloud:
gateway:
httpclient:
connect-timeout: 2000
response-timeout: 10s
routes:
- id: web
uri: lb://web/
order: 0
predicates:
- Path=/web/**
filters:
- StripPrefix=1 # 去除请求地址中的前缀
下面搭建 gateway 网关服务,注册到 nacos 中,并加载上面创建的配置文件:
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
config:
server-addr: 127.0.0.1:8848
file-extension: yaml
refresh-enabled: true
extension-configs[0]:
data-id: env-config.yaml
group: DEFAULT_GROUP
refresh: true
启动后,查看下是否已经注册到 nacos 中了:
测试下是否可以负载转发:
已经实现了负载效果,但是还没有达到我们想要的效果,下面开始对 gateway 网关进行修改。
首先我们新建一个 EnvProperties 来接收 env-config.yaml 中的配置,注意一定要加 @RefreshScope
注解,这样才能修改配置后通知到相应的服务:
@Data
@Configuration
@RefreshScope
public class EnvProperties
@Value("$env.pro.version")
private String proVersion;
@Value("$env.gray.users")
private List<String> grayUsers;
@Value("$env.gray.version")
private String grayVersion;
在创建一个 ThreadLocal ,存储当前的版本信息,这里先记下来,后面就知道什么作用了:
public class GrayscaleThreadLocalEnvironment
private static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
public static void setCurrentEnvironment(String currentEnvironmentVsersion)
threadLocal.set(currentEnvironmentVsersion);
public static String getCurrentEnvironment()
return threadLocal.get();
下面创建 过滤器 对请求进行拦截,然后获取到用户的信息,这里就默认用户ID 在 header 中,key 为 userId,取到之后判断是否在 灰度用户列表中,如果存在就把当前的 ThreadLocal(就是上面声明的ThreadLocal ) 中存储灰度的版本号,,否则就为生产的版本号:
@Component
@RefreshScope
public class GrayscaleGlobalFilter implements GlobalFilter, Ordered
@Autowired
EnvProperties envProperties;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
HttpHeaders header = response.getHeaders();
header.add("Content-Type", "application/json; charset=UTF-8");
List<String> list = request.getHeaders().get("userId");
if (Objects.isNull(list) || list.isEmpty())
return resultErrorMsg(response," 缺少userId!");
String userId = list.get(0);
if (StringUtils.isBlank(userId))
return resultErrorMsg(response," 缺少userId!");
if (envProperties.getGrayUsers().contains(userId))
//指定灰度版本
GrayscaleThreadLocalEnvironment.setCurrentEnvironment(envProperties.getGrayVersion());
else
//指定生产版本
GrayscaleThreadLocalEnvironment.setCurrentEnvironment(envProperties.getProVersion());
return chain.filter(exchange.mutate().request(request).build());
public int getOrder()
return -1;
private Mono<Void> resultErrorMsg(ServerHttpResponse response, String msg)
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", "403");
jsonObject.put("message", msg);
DataBuffer buffer = response.bufferFactory().wrap(jsonObject.toJSONString().getBytes());
return response.writeWith(Mono.just(buffer));
上面的过滤器已经标识出当前请求属于灰度还是生产,下面就需要我们重写Ribbon 负载均衡器,这里重写的 RoundRobinRule ,在 choose 方法中,根据当前 ThreadLocal 中的版本,便利服务中版本与之相等的服务,作为转发服务,为了防止服务获取失败,这里曾加了重试策略,重试 10 次还是失败,即放弃重试:
@Component
@Slf4j
public class EnvRoundRobinRule extends RoundRobinRule
private AtomicInteger nextServerCyclicCounter;
public EnvRoundRobinRule()
nextServerCyclicCounter = new AtomicInteger(0);
public Server choose(ILoadBalancer lb, Object key)
if (lb == null)
log.warn("no load balancer");
return null;
Server server = null;
int count = 0;
// 如果失败,重试 10 次
while (Objects.isNull(server) && count++ < 10)
List<Server> reachableServers = lb.getReachableServers();
List<Server> allServers = lb.getAllServers();
int upCount = reachableServers.size();
int serverCount = allServers.size();
if ((upCount == 0) || (serverCount == 0))
log.warn("No up servers available from load balancer: " + lb);
return null;
List<NacosServer> filterServers = new ArrayList<>();
String currentEnvironmentVersion = GrayscaleThreadLocalEnvironment.getCurrentEnvironment();
for (Server serverInfo : reachableServers)
NacosServer nacosServer = (NacosServer) serverInfo;
String version = nacosServer.getMetadata().get("version");
if (version.equals(currentEnvironmentVersion))
filterServers.add(nacosServer);
int filterServerCount = filterServers.size();
int nextServerIndex = incrementAndGetModulo(filterServerCount);
server = filterServers.get(nextServerIndex);
if (server == null)
Thread.yield();
continue;
if (server.isAlive() && (server.isReadyToServe()))
return (server);
server = null;
if (count >= 10)
log.warn("No available alive servers after 10 tries from load balancer: "
+ lb);
return server;
private int incrementAndGetModulo(int modulo)
for (; ; )
int current = nextServerCyclicCounter.get();
int next = (current + 1) % modulo;
if (nextServerCyclicCounter.compareAndSet(current, next))
return next;
到这 流程基本就已经结束了,下面在header 中增加 userId 为 abc,然后多访问几次,可以看到都被转发到了 灰度环境:
下面在header 中增加 userId 为 110,然后多访问几次,可以看到都被转发到了 生产环境:
以上是关于基于 GateWay 和 Nacos 实现微服务架构灰度发布方案的主要内容,如果未能解决你的问题,请参考以下文章
关于Spring cloud Gateway集成nacos 实现路由到指定微服务的方式总结
SpringCloud实战(十六)-基于Gateway + nacos网关灰度发布(只控制到网关层,局限性太大,微服务复杂链路调用规则控制建议重写Ribbon,而不是只重写Gateway路由规则)
SpringCloud实战(十六)-基于Gateway + nacos网关灰度发布(只控制到网关层,局限性太大,微服务复杂链路调用规则控制建议重写Ribbon,而不是只重写Gateway路由规则)(代