- 灰度发布:在进行内部版本发布时希望有一部分用户能体验新的功能进行打标签的方式进行分区访问;
- 基于SpringCloudGateWay的灰度发布实践
- 在gateway中进行服务路由是通过Ribbon进行负载的,SpringCloudGateWay使用到的路由策略有两种分别是RandomLoadBalancer和RoundRobinLoadBalancer两个类进行实现的,我们可以参考上述两个负载均衡实现类,在进行路由选择策略中,将灰度服务和非灰度服务进行区分,进行路由;
- 整理流程如下
- 首先,我们需要给服务器打标签,来区分灰度服务与非灰度服务;
- 给用户打标签来标注当前用户是内测灰度用户;
- 网关进行负载均衡时判断用户请求信息是否携带灰度参数,若携带参数则去匹配对应的灰度服务,不存在则走正常的服务;
- 首先是第一项,给应用服务打标签,我们使用的服务注册中心时eureka,eureka有一个metadata-map元数据域,我们可以在服务启动时将当前服务存储一个Key,Value的键值对,如下所示,我们配置了一个GRAY_META,版本是1.0.0;

- 第二项给用户打标签,这个便签比较动态,可以在内部服务系统给用户直接加一个标签,通过网关去查询,也可以在返回用户信息时,增加一个Header进行区分,如下所示

- 第三项,网关判断用户信息与服务器信息进行比对,那么我们就无法使用默认的负载均衡操作了,需要去自定义一个负载均衡,首先已知负载均衡都实现的是ReactorServiceInstanceLoadBalancer类进行负载均衡,那么我们也可以去实现这个类去自定义一个我们自己的负载均衡,代码如下
@Slf4j
@Component
public class GrayFilter implements GlobalFilter, Ordered {
private static final int LOAD_BALANCER_CLIENT_FILTER_ORDER = 10150;
private final LoadBalancerClientFactory clientFactory;
public GrayFilter(LoadBalancerClientFactory clientFactory) {
this.clientFactory = clientFactory;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("<-----------------------------[网关-路由选择]:路由选择开始----------------------------->");
URI url = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
String schemePrefix = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR);
ServerWebExchangeUtils.addOriginalRequestUrl(exchange, url);
if (log.isTraceEnabled()) {
log.trace(ReactiveLoadBalancerClientFilter.class.getSimpleName() + " url before: " + url);
}
return this.choose(exchange).doOnNext((response) -> {
if (!response.hasServer()) {
assert url != null;
throw NotFoundException.create(true, "Unable to find instance for " + url.getHost());
} else {
URI uri = exchange.getRequest().getURI();
String overrideScheme = null;
if (schemePrefix != null) {
assert url != null;
overrideScheme = url.getScheme();
}
DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance(response.getServer(), overrideScheme);
URI requestUrl = this.reconstructURI(serviceInstance, uri);
exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, requestUrl);
}
}).then(chain.filter(exchange));
}
private Mono<Response<ServiceInstance>> choose(ServerWebExchange exchange) {
URI uri = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
//获取可用路由信息
assert uri != null;
GrayRoundRobinLoadBalancer loadBalancer = new GrayRoundRobinLoadBalancer(clientFactory.getLazyProvider(uri.getHost(), ServiceInstanceListSupplier.class), uri.getHost());
return loadBalancer.choose(this.createRequest(exchange));
}
private Request createRequest(ServerWebExchange exchange) {
HttpHeaders headers = exchange.getRequest().getHeaders();
return new DefaultRequest<>(headers);
}
private URI reconstructURI(ServiceInstance serviceInstance, URI original) {
return LoadBalancerUriTools.reconstructURI(serviceInstance, original);
}
@Override
public int getOrder() {
return LOAD_BALANCER_CLIENT_FILTER_ORDER;
}
}
public class GrayRoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalancer {
//对应用户请求的Key
private static final String GaryKey = "GATE-GRAY-VERSION";
//对应灰度应用metadata中的Key
private static final String GRAY_META = "GRAY_META";
private Logger log = LoggerFactory.getLogger(GrayRoundRobinLoadBalancer.class);
private final AtomicInteger position;
private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
private final String serviceId;
public GrayRoundRobinLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) {
this.serviceId = serviceId;
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
this.position= new AtomicInteger(new Random().nextInt(1000));
}
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
HttpHeaders headers = (HttpHeaders) request.getContext();
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
return supplier.get(request).next().map(list -> processInstanceResponse(list,headers));
}
private Response<ServiceInstance> processInstanceResponse(List<ServiceInstance> serviceInstances,HttpHeaders headers) {
Response<ServiceInstance> serviceInstanceResponse = this.getInstanceResponse(serviceInstances,headers);
if (serviceInstanceResponse instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
((SelectedInstanceCallback)serviceInstanceResponse).selectedServiceInstance(serviceInstanceResponse.getServer());
}
return serviceInstanceResponse;
}
/**
* 获取可用实例
* @param instances 匹配serverId的应用列表
* @param headers 请求头
* @return 可用server
*/
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, HttpHeaders headers) {
String version = headers.getFirst(GaryKey);
//存放灰度server
List<ServiceInstance> grayInstanceServer =new ArrayList<>();
//非灰度server
List<ServiceInstance> defaultInstanceServer =new ArrayList<>();
for (ServiceInstance serviceInstance : instances) {
if (StringUtils.isNotBlank(version) && version.equals(serviceInstance.getMetadata().get(GRAY_META))) {
grayInstanceServer.add(serviceInstance);
} else {
Map<String, String> metadata = serviceInstance.getMetadata();
//meta是空的也加入
if (CollectionUtils.isEmpty(serviceInstance.getMetadata())){
defaultInstanceServer.add(serviceInstance);
}else{
//判断是否有版本号,有的话剔除,没有的话就加入到默认server中
if (StringUtils.isBlank(metadata.get(GateWayConstant.GATEWAY_GRAY_META_KEY))){
defaultInstanceServer.add(serviceInstance);
}
}
}
}
int pos = Math.abs(this.position.incrementAndGet());
ServiceInstance instance;
if (StringUtils.isNotBlank(version)){
if (grayInstanceServer.isEmpty()) {
if (log.isWarnEnabled()) {
log.warn("请求灰度应用[{}]-对应版本[{}] 无可用路由",serviceId,version);
}
return new EmptyResponse();
}
instance=grayInstanceServer.get(pos % grayInstanceServer.size());
log.info("<-----------------------------[网关-路由选择]:携带版本号为:[{}],请求地址[{}]----------------------------->", version,instance.getUri());
}else {
if (defaultInstanceServer.isEmpty()) {
if (log.isWarnEnabled()) {
log.warn("请求应用[{}]无可用路由",serviceId);
}
return new EmptyResponse();
}
instance=defaultInstanceServer.get(pos % defaultInstanceServer.size());
log.info("<-----------------------------[网关-路由选择]:[{}]默认路由,请求地址:[{}]----------------------------->",serviceId,instance.getUri());
}
return new DefaultResponse(instance);
}
- 以上三步我们就可以来对灰度应用与灰度用户匹配来达到我们灰度测试的目的
- 文章中写的比较粗暴简单,可以考虑使用配置中心像Apollo或者nacos来进行动态化的配置和转换,包括服务器的灰度版本也可以考虑在网关配置router时配置在metadata中