公司需要快速迭代开发上线,又要保证质量,保证刚上线的系统,一旦出现问题可以很快控制影响面,就需要设计一套灰度发布系统。
灰度发布系统的作用,可以根据配置,将用户的流量导到新上线的系统上,来快速验证新的功能,而一旦出现问题,也可以马上的修复,简单的说,就是一套A/B Test系统。
灰度发布允许带着bug上线,只要bug不是致命的,当然这个bug是不知道的情况下,如果知道就要很快的改掉
灰度发布实质是让指定用户访问指定版本的服务。
所以首先需要指定用户匹配到指定的路由规则。
其次,服务的版本号信息可以通过HTTP请求头字段来指定。
最后,负载均衡算法需要能够根据版本号信息来做服务实例的选择。
灰度的策略
灰度必须要有灰度策略,灰度策略常见的方式有以下几种
1、基于Request Header进行流量切分
2、基于Cookie进行流量切分
3、基于请求参数进行流量切分
方案:couldgateway+nacos
实施:
1.编写带权重的灰度路由
2.编写自定义filter
3.nacos服务配置需要灰度发布的服务的元数据信息以及权重灰度路由从nacos服务拉取元数据信息以及权重,然后根据权重算法,返回符合要求的服务实例给自定义的filter
4.网关配置文件配置需要灰度路由的服务(因为本文代码没有网关实现动态路由,不然灰度路由可以配置在配置中心,从配置中心拉取)
5.filter通过责任链模式,把服务实例透传给其他filter比如NettyRoutingFilter
权重路由
public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private static final Log log = LogFactory.getLog(GrayLoadBalancer.class);
private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
private String serviceId;
public GrayLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) {
this.serviceId = serviceId;
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
}
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
HttpHeaders headers = (HttpHeaders) request.getContext();
if (this.serviceInstanceListSupplierProvider != null) {
ServiceInstanceListSupplier supplier = (ServiceInstanceListSupplier)this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
return ((Flux)supplier.get()).next().map(list->getInstanceResponse((List<ServiceInstance>)list,headers));
}
return null;
}
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances,HttpHeaders headers) {
if (instances.isEmpty()) {
return getServiceInstanceEmptyResponse();
} else {
return getServiceInstanceResponseWithWeight(instances);
}
}
/**
* 根据版本进行分发
* @param instances
* @param headers
* @return
*/
private Response<ServiceInstance> getServiceInstanceResponseByVersion(List<ServiceInstance> instances, HttpHeaders headers) {
String versionNo = headers.getFirst("version");
System.out.println(versionNo);
Map<String,String> versionMap = new HashMap<>();
versionMap.put("version",versionNo);
final Set<Map.Entry<String,String>> attributes =
Collections.unmodifiableSet(versionMap.entrySet());
ServiceInstance serviceInstance = null;
for (ServiceInstance instance : instances) {
Map<String,String> metadata = instance.getMetadata();
if(metadata.entrySet().containsAll(attributes)){
serviceInstance = instance;
break;
}
}
if(ObjectUtils.isEmpty(serviceInstance)){
return getServiceInstanceEmptyResponse();
}
return new DefaultResponse(serviceInstance);
}
/**
*
* 根据在nacos中配置的权重值,进行分发
* @param instances
*
* @return
*/
private Response<ServiceInstance> getServiceInstanceResponseWithWeight(List<ServiceInstance> instances) {
Map<ServiceInstance,Integer> weightMap = new HashMap<>();
for (ServiceInstance instance : instances) {
Map<String,String> metadata = instance.getMetadata();
System.out.println(metadata.get("version")+"-->weight:"+metadata.get("weight"));
if(metadata.containsKey("weight")){
weightMap.put(instance,Integer.valueOf(metadata.get("weight")));
}
}
WeightMeta<ServiceInstance> weightMeta = WeightRandomUtils.buildWeightMeta(weightMap);
if(ObjectUtils.isEmpty(weightMeta)){
return getServiceInstanceEmptyResponse();
}
ServiceInstance serviceInstance = weightMeta.random();
if(ObjectUtils.isEmpty(serviceInstance)){
return getServiceInstanceEmptyResponse();
}
System.out.println(serviceInstance.getMetadata().get("version"));
return new DefaultResponse(serviceInstance);
}
private Response<ServiceInstance> getServiceInstanceEmptyResponse() {
log.warn("No servers available for service: " + this.serviceId);
return new EmptyResponse();
}
filter
public class GrayReactiveLoadBalancerClientFilter implements GlobalFilter, Ordered {
private static final Log log = LogFactory.getLog(ReactiveLoadBalancerClientFilter.class);
private static final int LOAD_BALANCER_CLIENT_FILTER_ORDER = 10150;
private final LoadBalancerClientFactory clientFactory;
private LoadBalancerProperties properties;
public GrayReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory, LoadBalancerProperties properties) {
this.clientFactory = clientFactory;
this.properties = properties;
}
@Override
public int getOrder() {
return LOAD_BALANCER_CLIENT_FILTER_ORDER;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
URI url = (URI)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
String schemePrefix = (String)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR);
if (url != null && ("grayLb".equals(url.getScheme()) || "grayLb".equals(schemePrefix))) {
ServerWebExchangeUtils.addOriginalRequestUrl(exchange, url);
if (log.isTraceEnabled()) {
log.trace(ReactiveLoadBalancerClientFilter.class.getSimpleName() + " url before: " + url);
}
return this.choose(exchange).doOnNext((response) -> {
if (!response.hasServer()) {
throw NotFoundException.create(this.properties.isUse404(), "Unable to find instance for " + url.getHost());
} else {
URI uri = exchange.getRequest().getURI();
String overrideScheme = null;
if (schemePrefix != null) {
overrideScheme = url.getScheme();
}
DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance((ServiceInstance)response.getServer(), overrideScheme);
URI requestUrl = this.reconstructURI(serviceInstance, uri);
if (log.isTraceEnabled()) {
log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
}
exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, requestUrl);
}
}).then(chain.filter(exchange));
} else {
return chain.filter(exchange);
}
}
protected URI reconstructURI(ServiceInstance serviceInstance, URI original) {
return LoadBalancerUriTools.reconstructURI(serviceInstance, original);
}
private Mono<Response<ServiceInstance>> choose(ServerWebExchange exchange) {
URI uri = (URI)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
GrayLoadBalancer loadBalancer = new GrayLoadBalancer(clientFactory.getLazyProvider(uri.getHost(), ServiceInstanceListSupplier.class), uri.getHost());
if (loadBalancer == null) {
throw new NotFoundException("No loadbalancer available for " + uri.getHost());
} else {
return loadBalancer.choose(this.createRequest(exchange));
}
}
private Request createRequest(ServerWebExchange exchange) {
HttpHeaders headers = exchange.getRequest().getHeaders();
Request<HttpHeaders> request = new DefaultRequest<>(headers);
return request;
}
}
交给spring管理:
@Configuration
public class GrayGatewayReactiveLoadBalancerClientAutoConfiguration {
public GrayGatewayReactiveLoadBalancerClientAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean({GrayReactiveLoadBalancerClientFilter.class})
public GrayReactiveLoadBalancerClientFilter grayReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory, LoadBalancerProperties properties) {
return new GrayReactiveLoadBalancerClientFilter(clientFactory, properties);
}
}
```java
server:
port: 9082
# 配置输出日志
logging:
level:
org.springframework.cloud.gateway: TRACE
org.springframework.http.server.reactive: DEBUG
org.springframework.web.reactive: DEBUG
reactor.ipc.netty: DEBUG
#开启端点
management:
endpoints:
web:
exposure:
include: '*'
spring:
application:
name: gateway-reactor-gray
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
routes:
- id: hello-consumer
uri: grayLb://hello-consumer
predicates:
- Path=/hello/**
其中grayLb与filter中代码(“grayLb”.equals(url.getScheme()) || “grayLb”.equals(schemePrefix))搭配
在nacos配置原数据中定义 weight代表权重,version代表版本
请求进来以后即可通过拦截加配置打到灰度环境