最近在工作中遇到需要使用灰度发布的功能,遇到了很多坑.记录一下!
本文结合网上查找的方案,但代码中始终报错,找不到nacos上的服务实例(ServiceInstance),最后看了gateway源码中的LoadBalancerClientFilter类.仿效着更改代码.
在一般情况下,升级服务器端应用,需要将应用源码或程序包上传到服务器,然后停止掉老版本服务,再启动新版本。但是这种简单的发布方式存在两个问题,一方面,在新版本升级过程中,服务是暂时中断的,另一方面,如果新版本有BUG,升级失败,回滚起来也非常麻烦,容易造成更长时间的服务不可用
<spring-cloud.version>Hoxton.SR8</spring-cloud.version>
<java.version>1.8</java.version>
<spring-cloud-alibaba.version>2.1.2.RELEASE</spring-cloud-alibaba.version>
<Springboot.version>2.4.3</Springboot.version>
<!-- Nacos注册中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Nacos配置中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- spring-cloud网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--Spring Webflux-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
server:
port: 9888
spring:
application:
name: gray-gateway
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
routes:
- id: hello-service
uri: grayLb://hello-service
predicates:
- Path=/hello/**
#grayLb使用灰度发布, lb正常使用源码原本的策略
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.jeecg.utils.VersionUtil;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.client.loadbalancer.reactive.DefaultRequest;
import org.springframework.cloud.client.loadbalancer.reactive.Request;
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.support.DelegatingServiceInstance;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.util.CollectionUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.util.*;
import java.util.stream.Collectors;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.addOriginalRequestUrl;
/**
* @author lyg
* @description : 灰度发布过滤器
* @date 2021/8/12 10:21
*/
@Slf4j
public class GrayFilter implements GlobalFilter, Ordered {
private static final int LOAD_BALANCER_CLIENT_FILTER_ORDER = 10150;
private final LoadBalancerClient clientFactory;
private LoadBalancerProperties properties;
private DiscoveryClient discoveryClient;
public GrayFilter(LoadBalancerClient clientFactory, LoadBalancerProperties properties, DiscoveryClient discoveryClient) {
this.clientFactory = clientFactory;
this.properties = properties;
this.discoveryClient = discoveryClient;
}
@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))) {
return chain.filter(exchange);
}
// preserve the original url
addOriginalRequestUrl(exchange, url);
ServiceInstance instance = this.choose(exchange);
if (instance == null) {
throw NotFoundException.create(properties.isUse404(),
"Unable to find instance for " + url.getHost());
}
URI uri = exchange.getRequest().getURI();
// if the `lb:` mechanism was used, use `` as the default,
// if the loadbalancer doesn't provide one.
String overrideScheme = instance.isSecure() ? "https" : "http";
if (schemePrefix != null) {
overrideScheme = url.getScheme();
}
URI requestUrl = clientFactory.reconstructURI(
new DelegatingServiceInstance(instance, overrideScheme), uri);
if (log.isTraceEnabled()) {
log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
}
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
return chain.filter(exchange);
}
private ServiceInstance choose(ServerWebExchange exchange) {
URI uri = (URI) exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
String serviceId = uri.getHost();
Request request = this.createRequest(exchange);
HttpHeaders headers = (HttpHeaders) request.getContext();
if (this.discoveryClient != null) {
// 如果serviceId获取不到服务实例,则可以用其他办法
List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);
return getInstanceResponse(instances, headers);
}
return null;
}
/**
* 获取version对应服务实例
*/
private ServiceInstance getInstanceResponse(List<ServiceInstance> instances,HttpHeaders headers) {
if (instances.isEmpty()) {
return getServiceInstanceEmptyResponse();
} else {
return getServiceInstanceResponseByVersion(instances, headers);
}
}
private ServiceInstance getServiceInstanceEmptyResponse() {
log.warn("No servers available for service");
return null;
}
private Request createRequest(ServerWebExchange exchange) {
HttpHeaders headers = exchange.getRequest().getHeaders();
Request<HttpHeaders> request = new DefaultRequest<>(headers);
return request;
}
/**
* 根据版本进行分发
*
* @param instances
* @param headers
* @return
*/
private ServiceInstance getServiceInstanceResponseByVersion(List<ServiceInstance> instances, HttpHeaders headers) {
String versionNo = headers.getFirst("version");
// 获取最新版本实例
List<ServiceInstance> latestInst = getLatestInst(instances);
// 当接口访问获取不到版本信息时,默认访问最新版本或无版本的实例
if (StringUtils.isBlank(versionNo)) {
ServiceInstance instanceNoVer = instances.stream().filter(instance -> {
Map<String, String> metadata = instance.getMetadata();
String version = metadata.get("version");
if (StringUtils.isBlank(version)) {
return true;
}
return false;
}).findFirst().orElse(null);
// 返回实例
if (Objects.nonNull(instanceNoVer)) {
return instanceNoVer;
} else {
// 若实例不存在,返回最新版本的实例
// if (CollectionUtils.isEmpty(latestInst)) {
// return getServiceInstanceEmptyResponse();
// }
// // 随机返回一个最新版本实例
// int randomIndex = new Random().nextInt(latestInst.size());
// return new DefaultResponse(latestInst.get(randomIndex));
// 实例不存在
// return getServiceInstanceEmptyResponse();
}
}
log.info("====接口访问版本:{}====", 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)) {
if (CollectionUtils.isEmpty(latestInst)) {
return getServiceInstanceEmptyResponse();
}
}
return serviceInstance;
}
/**
* 获取最新版本的实例
* 可兼容集群情况
*
* @param instances
* @return
*/
private List<ServiceInstance> getLatestInst(List<ServiceInstance> instances) {
Map<String, List<ServiceInstance>> versionMap = instances.stream()
.filter(inst -> {
Map<String, String> metadata = inst.getMetadata();
if (StringUtils.isNotBlank(metadata.get("version"))) {
return true;
} else {
return false;
}
})
.collect(Collectors.groupingBy(inst -> {
Map<String, String> metadata = inst.getMetadata();
return metadata.get("version");
}));
// 比较key值(version格式:xx.yy.zz)大小,版本最大的排最前面
List<String> versionList = new ArrayList<>(versionMap.keySet());
Collections.sort(versionList, (v1, v2) -> VersionUtil.compareVersion(v2, v1));
return versionMap.get(versionList.get(0));
}
}
import org.jeecg.filter.GrayFilter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.gateway.config.LoadBalancerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author lyg
* @description : 灰度发布配置类
* @date 2021/8/12 11:49
*/
@Configuration
public class GrayFilterConfig {
public GrayFilterConfig() {
}
@Bean
@ConditionalOnMissingBean({GrayFilter.class})
public GrayFilter grayReactiveLoadBalancerClientFilter(LoadBalancerClient clientFactory, LoadBalancerProperties properties,
DiscoveryClient discoveryClient) {
return new GrayFilter(clientFactory, properties, discoveryClient);
}
}
public class VersionUtil {
/**
* @description 比较版本号
* @param version1
* @param version2
* @return
*/
public static int compareVersion(String version1, String version2){
// if (version1 == null || version2 == null) {
// }
version1 = version1.replaceAll("([^(\\d|\\.)])", "");
version2 = version2.replaceAll("([^(\\d|\\.)])", "");
String[] versionArray1 = version1.split("\\.");//注意此处为正则匹配, 不能用.;
String[] versionArray2 = version2.split("\\.");
int idx = 0;
int minLength = Math.min(versionArray1.length, versionArray2.length);//取最小长度值
int diff = 0;
while (idx < minLength
&& (diff = versionArray1[idx].length() - versionArray2[idx].length()) == 0//先比较长度
&& (diff = versionArray1[idx].compareTo(versionArray2[idx])) == 0) {//再比较字符
++idx;
}
//如果已经分出大小,则直接返回,如果未分出大小,则再比较位数,有子版本的为大;
diff = (diff != 0) ? diff : versionArray1.length - versionArray2.length;
return diff;
}
}
请求走网关访问,带上请求头version,有则找到对应的服务,无则抛出异常.(记录动态路由使用garyLb)