什么是灰度發布?
灰度發布(又名金絲雀發布)是指在黑與白之間,能夠平滑過渡的一種發布方式。在其上可以進行A/B testing,即讓一部分用戶繼續用產品特性A,一部分用戶開始用產品特性B,如果用戶對B沒有什么反對意見,那么逐步擴大范圍,把所有用戶都遷移到B上面來。灰度發布可以保證整體系統的穩定,在初始灰度的時候就可以發現、調整問題,以保證其影響度。
本文以springcloud gateway + nacos來演示如何實現灰度發布,如果對springcloud gateway和nacos還不熟悉的朋友,可以先閱讀如下文章,然后再閱讀本文。
實現的整體思路:
- 編寫帶權重的灰度路由
- 編寫自定義filter
- nacos服務配置需要灰度發布的服務的元數據信息以及權重
- 灰度路由從nacos服務拉取元數據信息以及權重,然后根據權重算法,返回符合要求的服務實例給自定義的filter
- 網關配置文件配置需要灰度路由的服務(因為本文代碼沒有網關實現動態路由,不然灰度路由可以配置在配置中心,從配置中心拉取)
- filter通過責任鏈模式,把服務實例透傳給其他filter比如NettyRoutingFilter
下邊進入實戰
正文
1、所使用的開發版本
<jdk.version>1.8</jdk.version>
<!-- spring cloud -->
<spring-cloud.version>Hoxton.SR3</spring-cloud.version>
<spring-boot.version>2.2.5.RELEASE</spring-boot.version>
<spring-cloud-alibaba.version>2.2.1.RELEASE</spring-cloud-alibaba.version>
2、pom.xml引入
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
ps:nacos的jar注意排除ribbon依賴,不然loadbalancer無法生效
3、編寫權重路由
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();
}
4、自定義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;
}
}
5、配置自定義filter給spring管理
@Configuration
public class GrayGatewayReactiveLoadBalancerClientAutoConfiguration {
public GrayGatewayReactiveLoadBalancerClientAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean({GrayReactiveLoadBalancerClientFilter.class})
public GrayReactiveLoadBalancerClientFilter grayReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory, LoadBalancerProperties properties) {
return new GrayReactiveLoadBalancerClientFilter(clientFactory, properties);
}
}
6、編寫網關application.yml配置
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/**
uri中的grayLb配置,代表該服務需要進行灰度發布
7、在注冊中心nacos配置灰度發布的服務版本以及權重值
weight代表權重,version代表版本
總結
上述就是實現灰度發布的過程,實現灰度發布的方法有很多種,文章中只是提供一種思路。雖然springcloud官方推薦使用loadbalancer來代替ribbon。因為ribbon是阻塞的,但從官方的loadbalancer的負載均衡算法來看,目前loadbalancer默認只支持輪詢算法,要其他算法得自己擴展實現,而ribbon默認支持7種算法,用默認的算法基本上就可以滿足我們的需求了。其次ribbon支持懶加載處理,超時以及重試與斷路器hystrix集成等配置,loadbalancer目前就支持重試。所以如果正式環境要自己實現灰度發布,可以考慮對ribbon進行擴展。本文的實現只是作為一種擴展補充,畢竟springcloud推薦loadbalancer,索性就寫個demo實現下。
最后灰度發布的實現,業內也有開源的實現--Discovery,感興趣的朋友可以通過如下鏈接進行查看