实际生产中如有需求变更,并不会直接更新线上服务,最通常的做法便是,有一套uat环境在线上验证,验证通过后在发布生产,这里存在的问题便是,uat环境不是真实的流量可能会有很多场景验证不到,导致发到生产后会出现很多预料不到的问题。
还有一种做法便是:切出线上的小部分流量进行体验测试,经过测试后无问题则全面的上线。
这样的好处也是非常明显的,一旦出现了bug,能保证大部分的客户端正常使用。
要实现这种平滑过度的方式就需要用到本篇文章介绍到的全链路灰度发布。
一,什么是灰度发布
灰度发布(金丝雀发布)是指在黑与白之间,能狗平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品A的特效,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定性,在初始灰度的时候就可以发现、调整问题,以保证其影响度。
为什么是全链路灰度发布
网关灰度发布实现的是网关通过灰度标记路由到文章服务B(灰度服务),至于从文章服务B到评论服务是通过openfeign内部调用的,默认无法实现灰度标记grayTag的透传,因此文章服务B最终调用的是评论服务A,并不是评论服务B
全链路灰度发布需要实现的是:
-
网关通过灰度标记将部分流量转发给文章服务B
-
文章服务B能够实现灰度标记grayTag的透传,最终调用评论服务B
经过以上分析,全链路灰度发布需要实现两个点 -
网关路由转发实现灰度发布
-
服务内部通过openFeign调用实现灰度发布(透传灰度标记grayTag)
网关层的灰度路由转发
文章将使用Ribbo+spring cloud gateway进行改造负载均衡策略实现灰度发布。
实现思路如下:
- 在网关的全局过滤器中根据业务规则给流量打上灰度标记
- 将灰度标记放入到请求头中,传递给下游服务。
- 改造Ribbon负载均衡策略,根据流量标记从注册中心获取灰度服务
- 请求路由转发
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.xxxxxx.starter</groupId>
<artifactId>xxxxxx-spring-boot-gray-starter</artifactId>
<version>3.00.0.M3-SNAPSHOT</version>
</parent>
<artifactId>xxxxxx-spring-boot-gray-starter-gateway</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.xxxxxx.starter</groupId>
<artifactId>xxxxxx-spring-boot-gray-core</artifactId>
<version>3.00.0.M3-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
spring.factory
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.xxxxxx.starter.gray.gateway.config.GrayGatewayAutoConfiguration
configuration
package com.xxxxxx.starter.gray.gateway.config;
import com.xxxxxx.starter.gray.gateway.filter.GrayRequestContextFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GlobalConfiguration {
@Bean
public GrayRequestContextFilter grayRequestContextFilter() {
return new GrayRequestContextFilter();
}
}
package com.xxxxxx.starter.gray.gateway.config;
import com.xxxxxx.starter.gray.common.config.GrayRuleProperties;
import com.xxxxxx.starter.gray.config.LoadBalancerGrayRuleConfig;
import com.xxxxxx.starter.gray.core.config.NacosAutoGrayRuleConfigUpdater;
import com.xxxxxx.starter.gray.gateway.filter.GrayReactiveLoadBalancerClientFilter;
import com.xxxxxx.starter.gray.properties.GrayStarterConfig;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.config.LoadBalancerProperties;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import javax.annotation.PostConstruct;
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(GrayStarterConfig.class)
@ConditionalOnProperty(name = "gray.starter.enabled", havingValue = "true", matchIfMissing = true)
@AllArgsConstructor
// @formatter:off
@Import({
LoadBalancerGrayRuleConfig.class,
GrayRuleProperties.class,
GlobalConfiguration.class
})
// @formatter:on
@Slf4j
public class GrayGatewayAutoConfiguration {
@PostConstruct
private void postConstruct() {
if (log.isInfoEnabled()) {
log.info("GrayGatewayAutoConfiguration all init completed");
}
}
@Bean
@ConditionalOnMissingBean({
GrayRuleProperties.class})
public GrayRuleProperties grayRuleProperties() {
log.info("初始化灰度规则配置Properties");
return new GrayRuleProperties();
}
@Bean
@ConditionalOnMissingBean({
NacosAutoGrayRuleConfigUpdater.class})
public NacosAutoGrayRuleConfigUpdater nacosAutoGrayRuleConfigUpdater(ApplicationContext applicationContext) {
return new NacosAutoGrayRuleConfigUpdater(applicationContext);
}
@Bean
@ConditionalOnMissingBean({
GrayReactiveLoadBalancerClientFilter.class})
public GrayReactiveLoadBalancerClientFilter grayReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory, LoadBalancerProperties properties, GrayRuleProperties grayRuleProperties) {
return new GrayReactiveLoadBalancerClientFilter(clientFactory, properties, grayRuleProperties);
}
}
RequestContextFilter
package com.xxxxxx.starter.gray.gateway.filter;
import com.xxxxxx.starter.gray.common.context.GrayRequest;
import com.xxxxxx.starter.gray.common.context.GrayRequestContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.LoadBalancerClientFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
/**
*
* reactor does not contain a {@link RequestContextHolder} object that has the same effect as mvc.
* If you need to get the current request from the context, you need to manually configure it.
*
* Bind ServerWebExchange to {@link Context} via {@link GrayRequestContext} after intercepting with filter.
*
* 灰度上下文Context Filter
* <pre>
* 这个灰度上下文过滤器的优先级比较高,会高于网关的所有自定义GlobalFilter
* GrayRequestContextFilter
* WebRequestFilter
* GlobalLogFilter
* </pre>
* @author Ceven
*
*/
@Slf4j
public class GrayRequestContextFilter implements WebFilter, Ordered {
public GrayRequestContextFilter() {
log.info("***************************【GrayRequestContextFilter init completed】***************************");
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
if(log.isDebugEnabled()) {
log.debug("GrayRequestContextFilter");
}
ServerHttpRequest request = exchange.getRequest();
GrayRequest grayRequest = new GrayRequest();
grayRequest.setPath(request.getURI().getPath());
grayRequest.setServiceName(request.getURI().getHost());
Set<String> headerNames = request.getHeaders().keySet();
Iterator<String> headerIterator = headerNames.iterator();
while(headerIterator.hasNext()) {
String name = headerIterator.next();
String value = request.getHeaders().getFirst(name);
grayRequest.getHeaders().put(name.toLowerCase(), value); //Header请求头统一转成小写的方式,防止有些地方会对请求头处理做额外的处理
}
log.debug("grayRequestHeaders:{}", grayRequest.getHeaders());
GrayRequestContext.put(grayRequest);
return chain.filter(exchange)
//Clear request save in ThreadLocal to prevent memory overflow
.doFinally(s -> {
// 如果发生了异常,会导致线程上下文切换. tid 会变
GrayRequestContext.remove();
});
}
@Override
public int getOrder() {
return -900;
}
}
GrayReactiveLoadBalancerClientFilter
package com.xxxxxx.starter.gray.gateway.filter;
import com.xxxxxx.starter.gray.common.config.GrayRuleProperties;
import com.xxxxxx.starter.gray.gateway.loadbalancer.GrayLoadBalancer;
import com.xxxxxx.starter.gray.gateway.loadbalancer.GrayServiceInstance;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools;
import org.springframework.cloud.client.loadbalancer.reactive.DefaultRequest;
import org.springframework.cloud.client.loadbalancer.reactive.Request;
import org.springframework.cloud.client.loadbalancer.reactive.Response;
import org.springframework.cloud.gateway.config.LoadBalancerProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.LoadBalancerClientFilter;
import org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter;
import org.springframework.cloud.gateway.support.DelegatingServiceInstance;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
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;
private Map<String, GrayLoadBalancer> grayLoadBalancerMap = new ConcurrentHashMap<>();
private GrayRuleProperties grayRuleProperties;
public GrayReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory, LoadBalancerProperties properties, GrayRuleProperties grayRuleProperties) {
this.clientFactory = clientFactory;
this.properties = properties;
this.grayRuleProperties = grayRuleProperties;
log.info("GrayReactiveLoadBalancerClientFilter init completed");
}
@Override
public int getOrder() {
return LoadBalancerClientFilter.LOAD_BALANCER_CLIENT_FILTER_ORDER - 1;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if(log.isDebugEnabled()) {
log.debug("GrayReactiveLoadBalancerClientFilter");
}
URI url = (URI)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
String schemePrefix = (String)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR);
log.debug("GrayReactiveLoadBalancerClientFilter rules: {}" + grayRuleProperties);
if (url != null && ("lb".equals(url.getScheme()) || "lb".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(false, "Unable to find instance for " + url.getHost());
} else {
URI uri = exchange.getRequest().getURI();
String overrideScheme = null;
if (schemePrefix != null) {
overrideScheme = url.getScheme();
}
ServiceInstance instance = response.getServer()