SpringCloud灰度发布实践(附源码) - 微服务实践 - SegmentFault 思否

标签: | 发表时间:2020-02-15 17:47 | 作者:
出处:https://segmentfault.com

代码GIT

前言

​ 在平时的业务开发过程中,后端服务与服务之间的调用往往通过 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

实现流程

灰度设计

原图链接

  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默认的规则从这些非灰度服务列表中返回一个服务。


  6. zuul通过Ribbon将请求转发到consumer服务后,可能还会通过 feginresttemplate调用其他服务,如provider服务。但是无论是通过 fegin还是 resttemplate,他们最后在选取服务转发的时候都会通过 Ribbon
  7. 那么在通过 feginresttemplate调用另外一个服务的时候需要设置一个拦截器,将请求头 version=xxx给带上,然后存入线程变量。
  8. 在经过 feginresttemplate的拦截器后最后会到Ribbon,Ribbon会从 线程变量里面取出 version信息。然后重复步骤(4)和(5)

灰度流程

设计思路

首先,我们通过更改服务在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

服务info信息

注意事项

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

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

测试演示

zuul==>provider服务

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

zuul-服务

zuul==>consumer服务>provider服务

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

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

Resttemplate请求方式

zuul-服务-resttemplate服务

fegin请求方式

zuul-服务-fegin

自动化配置

与Apollo实现整合,避免手动调用接口。实现配置监听,完成灰度。详情见 下篇文章

相关 [springcloud 灰度 实践] 推荐:

SpringCloud灰度发布实践(附源码) - 微服务实践 - SegmentFault 思否

- -
在平时的业务开发过程中,后端服务与服务之间的调用往往通过. resttemplate两种方式. 但是我们在调用服务的时候往往只需要写服务名就可以做到路由到具体的服务,这其中的原理相比大家都知道是. ribbon组件帮我们做了负载均衡的功能. 灰度的核心就是路由,如果我们能够重写ribbon默认的负载均衡算法是不是就意味着我们能够控制服务的转发呢.

基于springcloud实现的灰度发布

- -
基于springcloud实现的灰度发布. gray-config-server 配置中心. 端口:6007,方便起见直接读取配置文件,生产环境可以读取git. 先启动配置中心,所有服务的配置(包括注册中心的地址)均从配置中心读取. gray-xxx-service 服务消费者. 调用服务提供者和服务提供者,验证是否进入灰度服务.

SpringCloud项目接入Jaeger(下) - 掘金

- -
spring-cloud-sleuth这个组件时,会面临两个问题. 首先是日志中无法显示traceId和spanId这些链路信息,其次是不能在用. spring-cloud-sleuth所提供的方式进行链路传值. spring-cloud-sleuth是将traceId等链路信息保存在. slf4j的MDC(Mapped Diagnostic Contexts)中,然后通过%X{traceId}这种方式将traceId提取出来,比如打印到控制台的默认格式是:.

Springcloud + RocketMQ 解决分布式事务

- - 掘金架构
分布式事务有哪些实现方式. 随着互联网时代的高速发展,分布式成了大型系统的标配,这是时代发展的选择. 大型分布式系统不是每个公司和开发人员都能够涉及的领域,因为大型系统后面都 隐藏着众多代名词:复杂,昂贵,高科技,人才云集,大战略. 大部分领头互联网公司甚至依托自己的分布式经验逐步建立自己的体系,并使用这套体系搭建自己的平台对内,甚至对外提供服务, 就像现在众多的云平台提供的服务,甚至有些大战略提出促进发展:大中台小前台、大炮台支援单兵作战等等.

SpringCloud Gateway与k8s_zhangjunli的博客-CSDN博客

- -
接下来的内容由以下几部分组成:. 什么是SpringCloud Gateway. SpringCloud Gateway实战参考. kubernetes上的SpringCloud Gateway. 开发k8sgatewaydemo. 什么是SpringCloud Gateway. SpringCloud Gateway是SpringCloud技术栈下的网关服务框架,在基于SpringCloud的微服务环境中,外部请求会到达SpringCloud Gateway应用,该应用对请求做转发、过滤、鉴权、熔断等前置操作,一个典型的请求响应流程如下所示:.

一键实现自动化部署(灰度发布)实践

- - SegmentFault 最新的文章
在过去几年的DevOps的浪潮中,自动化、持续集成这两个概念早已深入人心(互联网技术人). 比尔盖茨先生曾经都说过:“任何技术在一个业务中使用的第一条规则就是,将自动化应用到一个高效的操作上将会放大高效. 第二条就是自动化应用到一个低效操作上,则放大了低效率. 自动化部署也逐渐成为各中小型企业追求的方向,那么,今天民工哥就自动化部署的概述、自动化部署的工具、自动化部署的流程、自动化部署实践等4个方面,与大家一同来讨论、交流一下关于中小企业自动部署的问题.

将应用从 SpringCloud 迁移到 k8s - Rason's Blog

- -
最近花了几天时间看了一下 k8s 和 istio 的文档,对容器化运维以及服务网格有了基础的了解. 俗话说读万卷书不如行万里路,于是先尝试用 minikube 练一下手,将现有了一个 Spring Cloud 项目迁移到 k8s 上来. 粗略地整理了一个整个流程,主要有以下几个改动点:. 安装 kubectl 和 minikube.

大规模微服务场景下灰度发布与流量染色实践

- - DockOne.io
最近微服务很热,与微服务相关的架构、流程、DevOps都很热. 很多公司,包括传统企业,到互联网公司做交流的时候,会问道,你们互联网公司号称能够加速业务创新、快速迭代,那我们是否也可以引入类似这样的机制. 我们做微服务,主要分为两个方面,一个是业务方面,另一个是技术方面. 最下面是运维部,不过现在我们的运维部已经拓展成云计算,DBA里的数据管理部门,已经发展成大数据,于是就有了技术中台和数据中台,另外还有共享用户中心的业务中台,总体构成了下层的中台部门,在上层业务一定要做微服务化.

SpringCloud基础教程(五)-配置中心热生效和高可用

- - 掘金后端
 我的博客: 兰陵笑笑生,欢迎浏览博客.  上一章 SpringCloud基础教程(四)-配置中心入门当中,我们在对Eureka的有了基本的基础认识之上,深入的了解Eureka高可用集群和其他的生产环境中用到的一些配置. 本章将开始了解分布式环境下的配置中心.  在实际的项目运行中,我们会根据实际需求修改配置内容,那么有没有一种方式,能够在不启动服务组件的情况向让配置文件动态的生效呢,Spring Cloud Conifg中提供了一种方式了.

SpringCloud实战十三:Gateway之 Spring Cloud Gateway 动态路由_zhuyu19911016520-CSDN博客

- -
前面分别对 Spring Cloud Zuul 与 Spring Cloud Gateway 进行了简单的说明,它门是API网关,API网关负责服务请求路由、组合及协议转换,客户端的所有请求都首先经过API网关,然后由它将匹配的请求路由到合适的微服务,是系统流量的入口,在实际生产环境中为了保证高可靠和高可用,尽量避免重启,如果有新的服务要上线时,可以通过动态路由配置功能上线.