Spring Cloud灰度发布方案

简介: Spring Cloud灰度发布方案

前言

在平时的业务开发过程中,后端服务与服务之间的调用往往通过fegin或者RestTemplate两种方式。但是我们在调用服务的时候往往只需要写服务名就可以做到路由到具体的服务,这其中的原理相比大家都知道是SpringCloudribbon组件帮我们做了负载均衡的功能。

灰度发布的核心就是路由,如果我们能够重写ribbon默认的负载均衡算法是不是就意味着我们能够控制服务的转发呢?

是的!

调用链分析

外部调用

  • 请求==>zuul==>服务

zuul在转发请求的时候,也会根据 Ribbon从服务实例列表中选择一个对应的服务,然后选择转发.

内部调用

  • 请求==>zuul==>服务Resttemplate调用==>服务
  • 请求==>zuul==>服务Fegin调用==>服务

无论是通过 Resttemplate还是 Fegin的方式进行服务间的调用,他们都会从 Ribbon选择一个服务实例返回.

   上面几种调用方式应该涵盖了我们平时调用中的场景,无论是通过哪种方式调用(排除直接ip:port调用),最后都会通过Ribbon,然后返回服务实例.

预备知识

eureka元数据

Eureka的元数据有两种,分别为标准元数据和自定义元数据。

标准元数据:主机名、IP地址、端口号、状态页和健康检查等信息,这些信息都会被发布在服务注册表中,用于服务之间的调用。

自定义元数据:自定义元数据可以使用eureka.instance.metadata-map配置,这些元数据可以在远程客户端中访问,但是一般不会改变客户端的行为,除非客户端知道该元数据的含义

eureka RestFul接口

请求名称 请求方式 HTTP地址 请求描述
注册新服务 POST /eureka/apps/{appID} 传递JSON或者XML格式参数内容,HTTP code为204时表示成功
取消注册服务 DELETE /eureka/apps/{appID}/{instanceID} HTTP code为200时表示成功
发送服务心跳 PUT /eureka/apps/{appID}/{instanceID} HTTP code为200时表示成功
查询所有服务 GET /eureka/apps HTTP code为200时表示成功,返回XML/JSON数据内容
查询指定appID的服务列表 GET /eureka/apps/{appID} HTTP code为200时表示成功,返回XML/JSON数据内容
查询指定appID&instanceID GET /eureka/apps/{appID}/{instanceID} 获取指定appID以及InstanceId的服务信息,HTTP code为200时表示成功,返回XML/JSON数据内容
查询指定instanceID服务列表 GET /eureka/apps/instances/{instanceID} 获取指定instanceID的服务列表,HTTP code为200时表示成功,返回XML/JSON数据内容
变更服务状态 PUT /eureka/apps/{appID}/{instanceID}/status?value=DOWN 服务上线、服务下线等状态变动,HTTP code为200时表示成功
变更元数据 PUT /eureka/apps/{appID}/{instanceID}/metadata?key=value HTTP code为200时表示成功

更改自定义元数据

配置文件方式:

eureka.instance.metadata-map.version = v1

接口请求:

PUT /eureka/apps/{appID}/{instanceID}/metadata?key=value

实现流程

image.png

  1. 用户请求首先到达Nginx然后转发到网关zuul,此时zuul拦截器会根据用户携带请求token解析出对应的userId
  2. 网关从Apollo配置中心拉取灰度用户列表,然后根据灰度用户策略判断该用户是否是灰度用户。如是,则给该请求添加请求头线程变量添加信息version=xxx;若不是,则不做任何处理放行
  3. zuul拦截器执行完毕后,zuul在进行转发请求时会通过负载均衡器Ribbon。
  4. 负载均衡Ribbon被重写。当请求到达时候,Ribbon会取出zuul存入线程变量version。于此同时,Ribbon还会取出所有缓存的服务列表(定期从eureka刷新获取最新列表)及其该服务的metadata-map信息。然后取出服务metadata-mapversion信息与线程变量version进行判断对比,若值一直则选择该服务作为返回。若所有服务列表的version信息与之不匹配,则返回null,此时Ribbon选取不到对应的服务则会报错!
  5. 当服务为非灰度服务,即没有version信息时,此时Ribbon会收集所有非灰度服务列表,然后利用Ribbon默认的规则从这些非灰度服务列表中返回一个服务。
  1. zuul通过Ribbon将请求转发到consumer服务后,可能还会通过feginresttemplate调用其他服务,如provider服务。但是无论是通过fegin还是resttemplate,他们最后在选取服务转发的时候都会通过Ribbon
  2. 那么在通过feginresttemplate调用另外一个服务的时候需要设置一个拦截器,将请求头version=xxx给带上,然后存入线程变量。
  3. 在经过feginresttemplate 的拦截器后最后会到Ribbon,Ribbon会从线程变量里面取出version信息。然后重复步骤(4)和(5)

image.png

设计思路

首先,我们通过更改服务在eureka的元数据标识该服务为灰度服务,笔者这边用的元数据字段为version 

1.首先更改服务元数据信息,标记其灰度版本。通过eureka RestFul接口或者配置文件添加如下信息eureka.instance.metadata-map.version=v1

2.自定义zuul拦截器GrayFilter。此处笔者获取的请求头为token,然后将根据JWT的思想获取userId,然后获取灰度用户列表及其灰度版本信息,判断该用户是否为灰度用户。

若为灰度用户,则将灰度版本信息version存放在线程变量里面。此处不能用Threadlocal存储线程变量,因为SpringCloud用hystrix做线程池隔离,而线程池是无法获取到ThreadLocal中的信息的! 所以这个时候我们可以参考Sleuth做分布式链路追踪的思路或者使用阿里开源的TransmittableThreadLocal方案。此处使用HystrixRequestVariableDefault实现跨线程池传递线程变量。

3.zuul拦截器处理完毕后,会经过ribbon组件从服务实例列表中获取一个实例选择转发。Ribbon默认的Rule为ZoneAvoidanceRule`。而此处我们继承该类,重写了其父类选择服务实例的方法。

以下为Ribbon源码:

public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule {
   // 略....
    @Override
    public Server choose(Object key) {
        ILoadBalancer lb = getLoadBalancer();
        Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
        if (server.isPresent()) {
            return server.get();
        } else {
            return null;
        }       
    }
}

以下为自定义实现的伪代码:

public class GrayMetadataRule extends ZoneAvoidanceRule {
   // 略....
    @Override
    public Server choose(Object key) {
      //1.从线程变量获取version信息
        String version = HystrixRequestVariableDefault.get();
      //2.获取服务实例列表
        List<Server> serverList = this.getPredicate().getEligibleServers(this.getLoadBalancer().getAllServers(), key);
       //3.循环serverList,选择version匹配的服务并返回
                for (Server server : serverList) {
            Map<String, String> metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata();
            String metaVersion = metadata.get("version);
            if (!StringUtils.isEmpty(metaVersion)) {
                if (metaVersion.equals(hystrixVer)) {
                    return server;
                }
            }
        }
    }
}

4.此时,只是已经完成了 请求==》zuul==》zuul拦截器==》自定义ribbon负载均衡算法==》灰度服务这个流程,并没有涉及到 服务==》服务的调用。

服务到服务的调用无论是通过resttemplate还是fegin,最后也会走ribbon的负载均衡算法,即服务==》Ribbon 自定义Rule==》服务。因为此时自定义的GrayMetadataRule并不能从线程变量中取到version,因为已经到了另外一个服务里面了。

5.此时依然可以参考Sleuth的源码org.springframework.cloud.sleuth.Span,这里不做赘述只是大致讲一下该类的实现思想。就是在请求里面添加请求头,以便下个服务能够从请求头中获取信息。

此处,我们可以通过在 步骤2中,让zuul添加添加线程变量的时候也在请求头中添加信息。然后,再自定义HandlerInterceptorAdapter拦截器,使之在到达服务之前将请求头中的信息存入到线程变量HystrixRequestVariableDefault中。

然后服务再调用另外一个服务之前,设置resttemplate和fegin的拦截器,添加头信息。

resttemplate拦截器

public class CoreHttpRequestInterceptor implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        HttpRequestWrapper requestWrapper = new HttpRequestWrapper(request);
        String hystrixVer = CoreHeaderInterceptor.version.get();
        requestWrapper.getHeaders().add(CoreHeaderInterceptor.HEADER_VERSION, hystrixVer);
        return execution.execute(requestWrapper, body);
    }
}

fegin拦截器

public class CoreFeignRequestInterceptor implements RequestInterceptor {
   @Override
   public void apply(RequestTemplate template) {
        String hystrixVer = CoreHeaderInterceptor.version.get();
        logger.debug("====>fegin version:{} ",hystrixVer); 
      template.header(CoreHeaderInterceptor.HEADER_VERSION, hystrixVer);
   }
}

6.到这里基本上整个请求流程就比较完整了,但是我们怎么让Ribbon使用自定义的Rule?这里其实非常简单,只需要在服务的配置文件中配置一下代码即可.

yourServiceId.ribbon.NFLoadBalancerRuleClassName=自定义的负载均衡策略类

但是这样配置需要指定服务名,意味着需要在每个服务的配置文件中这么配置一次,所以需要对此做一下扩展.打开源码org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration类,该类是Ribbon的默认配置类.可以清楚的发现该类注入了一个PropertiesFactory类型的属性,可以看到PropertiesFactory类的构造方法

public PropertiesFactory() {
        classToProperty.put(ILoadBalancer.class, "NFLoadBalancerClassName");
        classToProperty.put(IPing.class, "NFLoadBalancerPingClassName");
        classToProperty.put(IRule.class, "NFLoadBalancerRuleClassName");
        classToProperty.put(ServerList.class, "NIWSServerListClassName");
        classToProperty.put(ServerListFilter.class, "NIWSServerListFilterClassName");
    }

所以,我们可以继承该类从而实现我们的扩展,这样一来就不用配置具体的服务名了.至于Ribbon是如何工作的,这里有一篇方志明的文章(传送门)可以加强对Ribbon工作机制的理解

7.到这里基本上整个请求流程就比较完整了,上述例子中是以用户ID作为灰度的维度,当然这里可以实现更多的灰度策略,比如IP等,基本上都可以基于此方式做扩展

灰度使用

配置文件示例

spring.application.name = provide-test
server.port = 7770
eureka.client.service-url.defaultZone = http://localhost:1111/eureka/
#启动后直接将该元数据信息注册到eureka
#eureka.instance.metadata-map.version = v1

测试案例

分别启动四个测试实例,有version代表灰度服务,无version则为普通服务。当灰度服务测试没问题的时候,通过PUT请求eureka接口将version信息去除,使其变成普通服务.

实例列表

  • [x] zuul-server
  • [x] provider-test
    port:7770 version:无
    port: 7771 version:v1
  • [x] consumer-test
    port:8880 version:无
    port: 8881 version:v1

修改服务信息

服务在eureka的元数据信息可通过接口http://localhost:1111/eureka/apps访问到。

服务信息实例:

访问接口查看信息http://localhost:1111/eureka/apps/PROVIDE-TEST

image.png

image.png

注意事项

通过此种方法更改server的元数据后,由于ribbon会缓存实力列表,所以在测试改变服务信息时,ribbon并不会立马从eureka拉去最新信息m,这个拉取信息的时间可自行配置。

同时,当服务重启时服务会重新将配置文件的version信息注册上去。

测试演示

zuul==>provider服务

用户andy为灰度用户。
1.测试灰度用户andy,是否路由到灰度服务
provider-test:7771
2.测试非灰度用户andyaaa(任意用户)是否能被路由到普通服务
provider-test:7770

image.png

zuul==>consumer服务>provider服务

以同样的方式再启动两个consumer-test服务,这里不再截图演示。

请求从zuul==>consumer-test==>provider-test,通过feginresttemplate两种请求方式测试

Resttemplate请求方式

image.png

fegin请求方式

image.png

自动化配置

与Apollo实现整合,避免手动调用接口。实现配置监听,完成灰度。


相关实践学习
部署高可用架构
本场景主要介绍如何使用云服务器ECS、负载均衡SLB、云数据库RDS和数据传输服务产品来部署多可用区高可用架构。
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
目录
相关文章
|
1天前
|
监控 安全 Java
Spring cloud原理详解
Spring cloud原理详解
10 0
|
5天前
|
消息中间件 负载均衡 Java
【Spring Cloud 初探幽】
【Spring Cloud 初探幽】
14 1
|
7天前
|
Java 开发者 微服务
Spring Cloud原理详解
【5月更文挑战第4天】Spring Cloud是Spring生态系统中的微服务框架,包含配置管理、服务发现、断路器、API网关等工具,简化分布式系统开发。核心组件如Eureka(服务发现)、Config Server(配置中心)、Ribbon(负载均衡)、Hystrix(断路器)、Zuul(API网关)等。本文讨论了Spring Cloud的基本概念、核心组件、常见问题及解决策略,并提供代码示例,帮助开发者更好地理解和实践微服务架构。此外,还涵盖了服务通信方式、安全性、性能优化、自动化部署、服务网格和无服务器架构的融合等话题,揭示了微服务架构的未来趋势。
32 6
|
11天前
|
JSON Java Apache
Spring Cloud Feign 使用Apache的HTTP Client替换Feign原生httpclient
Spring Cloud Feign 使用Apache的HTTP Client替换Feign原生httpclient
|
11天前
|
负载均衡 Java 开发者
Spring Cloud:一文读懂其原理与架构
Spring Cloud 是一套微服务解决方案,它整合了Netflix公司的多个开源框架,简化了分布式系统开发。Spring Cloud 提供了服务注册与发现、配置中心、消息总线、负载均衡、熔断机制等工具,让开发者可以快速地构建一些常见的微服务架构。
|
13天前
|
消息中间件 Java RocketMQ
Spring Cloud RocketMQ:构建可靠消息驱动的微服务架构
【4月更文挑战第28天】消息队列在微服务架构中扮演着至关重要的角色,能够实现服务之间的解耦、异步通信以及数据分发。Spring Cloud RocketMQ作为Apache RocketMQ的Spring Cloud集成,为微服务架构提供了可靠的消息传输机制。
28 1
|
13天前
|
Dubbo Java 应用服务中间件
Spring Cloud Dubbo: 微服务通信的高效解决方案
【4月更文挑战第28天】在微服务架构的发展中,服务间的高效通信至关重要。Spring Cloud Dubbo 提供了一种基于 RPC 的通信方式,使得服务间的调用就像本地方法调用一样简单。本篇博客将探讨 Spring Cloud Dubbo 的核心概念,并通过具体实例展示其在项目中的实战应用。
15 2
|
13天前
|
监控 Java Sentinel
Spring Cloud Sentinel:概念与实战应用
【4月更文挑战第28天】在分布式微服务架构中,确保系统的稳定性和可靠性至关重要。Spring Cloud Sentinel 为微服务提供流量控制、熔断降级和系统负载保护,有效预防服务雪崩。本篇博客深入探讨 Spring Cloud Sentinel 的核心概念,并通过实际案例展示其在项目中的应用。
24 0
|
13天前
|
Cloud Native Java Nacos
Spring Cloud Nacos:概念与实战应用
【4月更文挑战第28天】Spring Cloud Nacos 是一个基于 Spring Cloud 构建的服务发现和配置管理工具,适用于微服务架构。Nacos 提供了动态服务发现、服务配置、服务元数据及流量管理等功能,帮助开发者构建云原生应用。
21 0