Spring Cloud Gateway 网关如何实现灰度发布?

简介: Spring Cloud Gateway 网关如何实现灰度发布?

前言

这篇文章来源于粉丝提出的一个问题:如何解决多环境统一注册中心服务实例乱窜?

怎么理解呢?

假设现在开发环境的AccountService已经在Nacos中注册了,现在小张需要对它进行修改升级,本地启动AccountService后也注册到了Nacos,但是在调试的时候请求通过网关经常直接跳转到开发环境,这样的话小张就没办法安心debug了。

其实这个问题归根结底是如何基于SpringCloud Gateway实现灰度发布,通过指定的规则让请求流量到达特定的实例。

在SpringCloud 2020 版本中官方推荐使用Spring Cloud LoadBalancer 来替换原Ribbon的负载均衡器。所以本篇文章我们直接基于Spring Cloud LoadBalancer来实现。

tips:何为灰度发布

灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。

实现目标

目标很明确,小张希望在调试的时候发出的请求能直接到达自己的本地开发环境,方便调试。

实现思路

要实现此目标我们需要解决两个关键的问题:

  1. 如何区分不同的实例
    需要给小张本地启动的AccountService服务实例一个特殊标识,让它与开发环境的区分开。
    这里我们可以使用注册中心的元数据metadata来区分,可以通过spring.cloud.nacos.discovery.metadata.version = dev配置指定,也可以在nacos服务列表中直接添加元数据信息。
  2. 实现自定义的负载均衡规则,通过自定义规则让负载均衡器能找到我们需要的服务实例
    小张在请求服务的时候需要在请求头上添加标签,version=dev,自定义负载均衡器在获取到请求头信息后去服务实例中查找配置了mtadata.version=dev的服务实例。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

Spring Cloud LoadBalancer(SCL)

SCL 负载均衡策略

在Spring Cloud LoadBalancer 官方文档上有这样一段说明:

Spring Cloud provides its own client-side load-balancer abstraction and implementation. For the load-balancing mechanism, ReactiveLoadBalancer interface has been added and a Round-Robin-based and Random implementations have been provided for it. In order to get instances to select from reactive ServiceInstanceListSupplier is used. Currently we support a service-discovery-based implementation of ServiceInstanceListSupplier that retrieves available instances from Service Discovery using a Discovery Client available in the classpath.

结合文档中的其他内容,提取出几条关键信息:

  1. Spring Cloud LoadBalancer提供了两种负载均衡算法:Round-Robin-basedRandom ,默认使用Round-Robin-based
  2. 可以通过实现ServiceInstanceListSupplier来筛选符合要求的服务实例
  3. 需要通过 LoadBalancerClient 注解,指定服务级别的负载均衡策略以及实例选择策略

提示:如果大家需要探究SCL的实现原理,可以通过GatewayReactiveLoadBalancerClientAutoConfiguration入手。

自定义灰度发布

结合上文,利用Spring Cloud LoadBalancer实现灰度我们有两种实现方式:

  1. 简单粗暴,直接实现一个新的负载均衡策略,然后通过LoadBalancerClient注解指定服务实例使用此策略。
  2. 自定义服务实例筛选逻辑,在返回给前端实例时筛选出符合要求的服务实例,当然也需要通过LoadBalancerClient注解指定服务实例使用此选择器。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

代码实现

版本说明

SpringCloud 项目使用的版本是SpringCloud alibaba推荐的毕业版本

<spring-boot.version>2.4.2</spring-boot.version>
<alibaba-cloud.version>2021.1</alibaba-cloud.version>
<springcloud.version>2020.0.0</springcloud.version>

自定义负载均衡策略

首先我们来看第一种实现方式,通过自定义负载均衡策略来实现。

  1. 在网关模块引入 SCL ,同时需要剔除nacos注册中心自带的Ribbon负载均衡器。
<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>
  1. 自定义负载均衡策略 VersionGrayLoadBalancer
/**
 * Description:
 * 自定义灰度
 * 通过给请求头添加Version 与 Service Instance 元数据属性进行对比
 * @author Jam
 * @date 2021/6/1 17:26
 */
@Log4j2
public class VersionGrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
    private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
    private final String serviceId;
    private final AtomicInteger position;
    public VersionGrayLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) {
        this(serviceInstanceListSupplierProvider,serviceId,new Random().nextInt(1000));
    }
    public VersionGrayLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
                                   String serviceId, int seedPosition) {
        this.serviceId = serviceId;
        this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
        this.position = new AtomicInteger(seedPosition);
    }
    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        ServiceInstanceListSupplier supplier = this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
        return supplier.get(request).next()
                .map(serviceInstances -> processInstanceResponse(serviceInstances,request));
    }
    private Response<ServiceInstance> processInstanceResponse(List<ServiceInstance> instances, Request request) {
        if (instances.isEmpty()) {
            log.warn("No servers available for service: " + this.serviceId);
            return new EmptyResponse();
        } else {
            DefaultRequestContext requestContext = (DefaultRequestContext) request.getContext();
            RequestData clientRequest = (RequestData) requestContext.getClientRequest();
            HttpHeaders headers = clientRequest.getHeaders();
            // get Request Header
            String reqVersion = headers.getFirst("version");
            if(StringUtils.isEmpty(reqVersion)){
                return processRibbonInstanceResponse(instances);
            }
            log.info("request header version : {}",reqVersion );
   // filter service instances
            List<ServiceInstance> serviceInstances = instances.stream()
                    .filter(instance -> reqVersion.equals(instance.getMetadata().get("version")))
                    .collect(Collectors.toList());
            if(serviceInstances.size() > 0){
                return processRibbonInstanceResponse(serviceInstances);
            }else{
                return processRibbonInstanceResponse(instances);
            }
        }
    }
    /**
     * 负载均衡器
     * 参考 org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer#getInstanceResponse
     * @author javadaily
     */
    private Response<ServiceInstance> processRibbonInstanceResponse(List<ServiceInstance> instances) {
        int pos = Math.abs(this.position.incrementAndGet());
        ServiceInstance instance = instances.get(pos % instances.size());
        return new DefaultResponse(instance);
    }
}

获取请求头中的version属性,然后根据服务实例元数据中的version属性进行匹配,对于符合条件的实例参考Round-Robin-based实现方法。

  1. 编写配置类VersionLoadBalancerConfiguration,用于替换默认的负载均衡算法
/**
 * Description:
 * 自定义负载均衡器配置实现类
 * @author javadaily
 * @date 2021/6/3 16:02
 */
public class VersionLoadBalancerConfiguration {
    @Bean
    ReactorLoadBalancer<ServiceInstance> versionGrayLoadBalancer(Environment environment,
                                                                 LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new VersionGrayLoadBalancer(
                loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
    }
}

VersionLoadBalancerConfiguration 配置类不能添加@Configuration注解。

  1. 在网关启动类使用注解@LoadBalancerClient指定哪些服务使用自定义负载均衡算法
    通过@LoadBalancerClient(value = "auth-service", configuration = VersionLoadBalancerConfiguration.class),对于auth-service启用自定义负载均衡算法;或通过@LoadBalancerClients(defaultConfiguration = VersionLoadBalancerConfiguration.class)为所有服务启用自定义负载均衡算法。

自定义服务实例筛选逻辑

接下来我们看第二种实现方法,通过实现ServiceInstanceListSupplier来自定义服务筛选逻辑,我们可以直接继承DelegatingServiceInstanceListSupplier来实现。

  1. 在网关模块引入Spring Cloud LoadBalancer(同上)
  2. 自定义服务实例筛选逻辑VersionServiceInstanceListSupplier
/**
 * 自定义服务实例筛选逻辑
 * @author javadaily
 * 参考:org.springframework.cloud.loadbalancer.core.ZonePreferenceServiceInstanceListSupplier
 */
@Log4j2
public class VersionServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier {
    public VersionServiceInstanceListSupplier(ServiceInstanceListSupplier delegate) {
        super(delegate);
    }
    @Override
    public Flux<List<ServiceInstance>> get() {
        return delegate.get();
    }
    @Override
    public Flux<List<ServiceInstance>> get(Request request) {
        return delegate.get(request).map(instances -> filteredByVersion(instances,getVersion(request.getContext())));
    }
    /**
     * filter instance by requestVersion
     * @author javadaily
     */
    private List<ServiceInstance> filteredByVersion(List<ServiceInstance> instances, String requestVersion) {
        log.info("request version is {}",requestVersion);
        if(StringUtils.isEmpty(requestVersion)){
            return instances;
        }
        List<ServiceInstance> filteredInstances = instances.stream()
                .filter(instance -> requestVersion.equalsIgnoreCase(instance.getMetadata().getOrDefault("version","")))
                .collect(Collectors.toList());
        if (filteredInstances.size() > 0) {
            return filteredInstances;
        }
        return instances;
    }
    private String getVersion(Object requestContext) {
        if (requestContext == null) {
            return null;
        }
        String version = null;
        if (requestContext instanceof RequestDataContext) {
            version = getVersionFromHeader((RequestDataContext) requestContext);
        }
        return version;
    }
    /**
     * get version from header
     * @author javadaily
     */
    private String getVersionFromHeader(RequestDataContext context) {
        if (context.getClientRequest() != null) {
            HttpHeaders headers = context.getClientRequest().getHeaders();
            if (headers != null) {
                //could extract to the properties
                return headers.getFirst("version");
            }
        }
        return null;
    }
}

实现原理跟自定义负载均衡策略一样,根据version匹配符合要求的服务实例。

  1. 编写配置类VersionServiceInstanceListSupplierConfiguration,用于替换默认服务实例筛选逻辑
public class VersionServiceInstanceListSupplierConfiguration {
    @Bean
    ServiceInstanceListSupplier serviceInstanceListSupplier(ConfigurableApplicationContext context) {
        ServiceInstanceListSupplier delegate = ServiceInstanceListSupplier.builder()
                .withDiscoveryClient()
                .withCaching()
                .build(context);
        return new VersionServiceInstanceListSupplier(delegate);
    }
}
  1. 在网关启动类使用注解@LoadBalancerClient指定哪些服务使用自定义负载均衡算法 通过@LoadBalancerClient(value = "auth-service", configuration = VersionServiceInstanceListSupplierConfiguration.class),对于auth-service启用自定义负载均衡算法;或通过@LoadBalancerClients(defaultConfiguration = VersionServiceInstanceListSupplierConfiguration.class)为所有服务启用自定义负载均衡算法。

测试

  1. 启动多个AccountService实例,对于58302端口的实例配置元数据version = dev

  1. postman 调用接口时指定请求头

  1. 通过debug模式观察两种实现逻辑,观察结果是否符合预期。

小结

本篇文章咱们基于SCL通过扩展负载均衡算法以及修改服务实例筛选逻辑两种方式实现了简单的灰度发布功能,大家可以参考此实现扩展SCL的负载均衡算法或者定制自己的服务筛选逻辑。

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
相关文章
|
3月前
|
存储 缓存 负载均衡
Gateway 网关坑我! 被这个404 问题折腾了一年?
小富分享了一个困扰团队一年多的 SpringCloud Gateway 路由 404 问题。通过日志追踪和源码分析,发现是网关在 Nacos 配置更新后未能正确清理旧的路由权重缓存,导致负载均衡时仍使用已删除的路由数据。最终通过监听路由刷新事件并手动更新缓存,成功解决了问题。
780 125
Gateway 网关坑我! 被这个404 问题折腾了一年?
|
3月前
|
负载均衡 监控 Java
Spring Cloud Gateway 全解析:路由配置、断言规则与过滤器实战指南
本文详细介绍了 Spring Cloud Gateway 的核心功能与实践配置。首先讲解了网关模块的创建流程,包括依赖引入(gateway、nacos 服务发现、负载均衡)、端口与服务发现配置,以及路由规则的设置(需注意路径前缀重复与优先级 order)。接着深入解析路由断言,涵盖 After、Before、Path 等 12 种内置断言的参数、作用及配置示例,并说明了自定义断言的实现方法。随后重点阐述过滤器机制,区分路由过滤器(如 AddRequestHeader、RewritePath、RequestRateLimiter 等)与全局过滤器的作用范围与配置方式,提
Spring Cloud Gateway 全解析:路由配置、断言规则与过滤器实战指南
|
2月前
|
缓存 JSON NoSQL
别再手写过滤器!SpringCloud Gateway 内置30 个,少写 80% 重复代码
小富分享Spring Cloud Gateway内置30+过滤器,涵盖请求、响应、路径、安全等场景,无需重复造轮子。通过配置实现Header处理、限流、重试、熔断等功能,提升网关开发效率,避免代码冗余。
372 1
|
2月前
|
机器学习/深度学习 Kubernetes API
【Azure APIM】自建网关(self-host gateway)收集请求的Header和Body内容到日志中的办法
在Azure API Management中,通过配置trace策略可完整记录API请求的Header和Body信息。在Inbound和Outbound策略中分别使用context.Request/Response.Headers和Body.As&lt;string&gt;方法捕获数据,并写入Trace日志,便于排查与审计。
127 7
|
4月前
|
安全 虚拟化
Omnissa Secure Email Gateway 2.33 - 电子邮件网关
Omnissa Secure Email Gateway 2.33 - 电子邮件网关
111 0
|
5月前
|
前端开发 Java API
Spring Cloud Gateway Server Web MVC报错“Unsupported transfer encoding: chunked”解决
本文解析了Spring Cloud Gateway中出现“Unsupported transfer encoding: chunked”错误的原因,指出该问题源于Feign依赖的HTTP客户端与服务端的`chunked`传输编码不兼容,并提供了具体的解决方案。通过规范Feign客户端接口的返回类型,可有效避免该异常,提升系统兼容性与稳定性。
377 0
|
6月前
|
缓存 监控 Java
说一说 SpringCloud Gateway 堆外内存溢出排查
我是小假 期待与你的下一次相遇 ~
877 5
|
6月前
|
Java API Nacos
|
10月前
|
前端开发 Java Nacos
🛡️Spring Boot 3 整合 Spring Cloud Gateway 工程实践
本文介绍了如何使用Spring Cloud Alibaba 2023.0.0.0技术栈构建微服务网关,以应对微服务架构中流量治理与安全管控的复杂性。通过一个包含鉴权服务、文件服务和主服务的项目,详细讲解了网关的整合与功能开发。首先,通过统一路由配置,将所有请求集中到网关进行管理;其次,实现了限流防刷功能,防止恶意刷接口;最后,添加了登录鉴权机制,确保用户身份验证。整个过程结合Nacos注册中心,确保服务注册与配置管理的高效性。通过这些实践,帮助开发者更好地理解和应用微服务网关。
1832 0
🛡️Spring Boot 3 整合 Spring Cloud Gateway 工程实践
|
12月前
|
JSON Java API
利用Spring Cloud Gateway Predicate优化微服务路由策略
Spring Cloud Gateway 的路由配置中,`predicates`​(断言)用于定义哪些请求应该匹配特定的路由规则。 断言是Gateway在进行路由时,根据具体的请求信息如请求路径、请求方法、请求参数等进行匹配的规则。当一个请求的信息符合断言设置的条件时,Gateway就会将该请求路由到对应的服务上。
1071 69
利用Spring Cloud Gateway Predicate优化微服务路由策略

热门文章

最新文章