Nacos+gateway实现灰度发布

基于SpringCloudAlibaba Nacos + gateway实现

最近在工作中遇到需要使用灰度发布的功能,遇到了很多坑.记录一下!
本文结合网上查找的方案,但代码中始终报错,找不到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>

使用的pom依赖(由于是继承的项目,所以依赖不一定齐全):

<!-- 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正常使用源码原本的策略

自定义filter:

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:<scheme>` mechanism was used, use `<scheme>` 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)

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值