SpringCloud灰度发布实践

服务实例

  • eureka-server
  • zuul -server
  • Apollo-config
  • provider-test
启动两个服务实例,一个带有版本号信息,一个没有
port:7770 version:无
port: 7771 version:v1
  • consumer-test
启动两个服务实例,一个带有版本号信息,一个没有任何信息.分别为:
port:8880 version:无
port:8881 version: v1

场景分析

  公司采用的是SpringCloud微服务体系,注册中心为eureka。我们知道,对于eureka-server而言,其他服务都是eureka-client的存在,我们在业务服务中只需要引入@EnableDiscoveryClient即可实现注册.
  比如我们想要调用order-service的服务,只需要通过resttemplate或者fegin的方式指定该服务的服务名即可完成调用。这是为什么呢?这一切还得从Ribbon这个背后的男人说起,因为ribbon会根据order-service的服务实例名获取该服务实例的列表,这些列表中包含了每个服务实例的IP和端口号,Ribbon会从中根据定义的负载均衡算法选取一个作为返回.
  看到这里一切都有点拨云见雾了,那么意味着是不是只要能够让Ribbon用我们自定义的负载均衡算法就可以为所欲为了呢?显然,是的!

简单分析下我们在调用过程中的集中情况:

外部调用:

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

内部调用:

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

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

设计思路

灰度发布

eureka预备知识

eureka元数据:

  • 标准元数据:主机名,IP地址,端口号,状态页健康检查等信息
  • 自定义元数据:通过eureka.instance.metadata-map配置

更改元数据:

  • 源码地址:com.netflix.eureka.resources.InstanceResource.updateMetadata()
  • 接口地址: /eureka/apps/appID/instanceID/metadata?key=value
  • 调用方式:PUTE

流程解析:

 1.在需要灰度发布的服务实例配置中添加eureka.instance.metadata-map.version=v1,注册成功后该信息后保存在eureka中.配置如下:
eureka.instance.metadata-map.version=v1

 2.自定义zuul拦截器GrayFilter。当请求带着token经过zuul时,根据token得到userId,然后从分布式配置中心Apollo中获取灰度用户列表,并判断该用户是否在该列表中(Apollo非必要配置,由于管理端比较完善所以笔者这里选择采用).
 若在列表中,则把version信息存在ThreadLocal中,从而使Ribbon中我们自定义的Rule能够拿到该信息;若不在,则不做任何处理。 但是我们知道hystrix是用线程池做隔离的,线程池中是无法拿到ThreadLocal中的信息的! 所以这个时候我们可以参考Sleuth做分布式链路追踪的思路或者使用阿里开源的TransmittableThreadLocal.
为了方便继承,这里采用Sleuth的方式做处理。Sleuth能够完整记录一条跨服务调用请求的每个服务请求耗时和总的耗时,它有一个全局traceId和对每个服务的SpanId.利用Sleuth全局traceId的思路解决我们的跨线程调用,所以这里可以使用HystrixRequestVariableDefault实现跨线程池的线程变量传递效果.

 3.zuul在转发之前会先到我们自定义的Rule中,默认Ribbon的Rule为ZoneAvoidanceRule.自定义编写的Rule只需要继承ZoneAvoidanceRule并且覆盖其父类的PredicateBasedRule#choose()方法即可.该方法是返回具体server,源码如下

public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule {
    public abstract AbstractServerPredicate getPredicate();
        
    @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;
        }       
    }
}

这里就完成了zuul转发到具体服务的灰度流程了,流程图如下
zuul灰度流程图

4.上面完成了zuul-->服务的灰度过程,接下来就是解决服务与服务间的调用.服务与服务间的调用依然可以用Sleuth的方式解决,只需要把信息添加到header里面即可.在使用RestTemplate调用的时候,可以添加它的拦截器ClientHttpRequestInterceptor,这样可以在调用之前就把header信息加入到请求中.

restTemplate.getInterceptors().add(YourHttpRequestInterceptor());

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

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工作机制的理解

6.这里就完成了灰度服务的正确路由,若灰度服务已经发布测试完毕想要把它变成正常服务,则只需要通过eureka的RestFul接口把它在eurekametadata-map存储的version信息置空即可.

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章