在我们使用spring mvc单体架构时, 我们可以通过uri,或者请求头做多版本路由,虽然同一个功能需要维护多个版本的接口,但是对于系统而言,不会因为新增一个接口版本而影响到老用户。当我们使用spring cloud构建微服务平台时,也希望能做到这一点,然而spring cloud并没有提供这个功能。
在springcloud的微服务体系中,大多是使用eureka做为注册中心,ribbon做为负载均衡,hystrix做为断路器。但是在国内网络中却鲜少关于spring-cloud的接口多版本控制的开源项目,而在国内,spring cloud做为越来越被创业公司认同的微服务框架,多版本控制的需求也越来越明显,于是就有了fm-cloud-bamboo这个多版本控制的项目。在开发这个项目的过程,发现只要再做一些扩展,就可以实现灰度管理,于是又有了fm-cloud-graybunny。
多版本控制
该项目是在spring-cloud-ribbon的基础上进行扩展,以实现接口的多个版本的调用及负载均衡,支持feign方式和断路器(spring-cloud-hystrix)。
场景
服务A部署了两个实例 serivceA-1,serviceA-2,spring cloud ribbon默认是轮询的方式将请求分别转到两个实例上。如果由于业务原因,服务需要从1.0升级到2.0。
场景1:将所有服务实例平缓的过度到2.0。
场景2:2.0的服务实例需要兼容1.0的服务接口。
思路
在spring cloud微服务体系中,服务的请求来源无外乎两个方面:
来源1:外部请求通过网关(zuul)转发而来。
来源2:内部服务之间的调用请求。
不论网关转发过来的请求,还是内部服务调用过来的请求,都需要ribbon做负载均衡,所以可以扩展ribbon的负载均衡策略从而实现不同版本的请求转发到不同的服务实例上。
网关的转发过程是:zuul > hystrix > ribbon
内部服务调用的过程有两种:
RestTemplate > hystrix > ribbon
Feign > hystrix > ribbon
而其中hystrix有一个线程池隔离的能力,会创建另一个线程去请求服务,拥有更好的控制并发访问量、以及服务降级等能力,但是会出现一个问题,就是线程变量(ThreadLocal)的传递问题,这可以通过com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableDefault对象解决。
代码设计
虽然整个项目实现起来代码量不少, 但是在接口设计上,却只有三个简单的接口负责数据传递,路由的逻辑依然是封装在实现了IRule接口的实现类中(后面分析)。
BambooRibbonConnectionPoint:
这个接口是负责将bamboo跟ribbon连接起来的,将请求的信息, 以及根据业务需要添加的一些路由信息,和获取请求接口的目标版本,还有触发执行LoadBanceRequestTrigger等,都是由该接口的实现类DefaultRibbonConnectionPoint负责实现。
RequestVersionExtractor:
这个接口负责获取请求需要访问的目标接口的版本。比如有些接口版本是放在路径上,如:/v1/api/test/get。也有放在uri参数中:/api/test/get?v=1。也有可能放到header中,所以在bamboo抽象出来一个接口, 具体的实现由开发者根据业务去实现。
LoadBalanceRequestTrigger:
Ribbon请求的触发器,在ribbon请求发起时, 会被执行。这个接口有三个方法,分别是判断是否需要执行的方法(shouldExecute),以及请求之前执行(before)和请求完成之后执行(after),如果出现异常,after方法依然会被执行。
代码实现
上面三个接口只是简单的实现了获取请求的目标版本、触发ribbon请求的触发器,以及将信息向下一步传递。在这一段中,将介绍如何与zuul、feign、RestTemplate以及ribbon和hystrix衔接起来。
RestTemplate衔接
ClientHttpRequestInterceptor是RestTemplate的拦截器接口,可以通过这个接口添加bamboo的逻辑, 从而将RestTemplate和bamboo衔接起来。
BambooClientHttpRequestIntercptor是ClientHttpRequestInterceptor接口的实现类,它加入了bamboo的逻辑。
Feign衔接
BambooFeignClient类实现了feign.Client接口,该类是一个代理类,主要的Feign的调用逻辑依然由被代理的类去执行,在该类中添加了bamboo的逻辑,从而将Feign和bamboo衔接起来。
Zuul衔接
实现两个ZuulFilter接口,分别是pre和post类型,将bamboo的逻辑加入其中。Pre类型的ZuulFilter获取请求信息,并执行LoadBalanceRequestTrigger#before方法。Post类型的ZuulFilter执行LoadBalanceRequestTrigger#after方法,并清除存在ThradLocal中的相关信息。
Hystrix衔接
Hystrix实现降级、断路器等功能,但是在使用线程池隔离时,ThreadLocal存储的信息如何传递下去呢?使用HystrixRequestVariableDefault可以解决这个问题。可以查看com.netflix.hystrix.strategy.concurrency包下的HystrixContexSchedulerAction、HystrixContextCallable、HystrixContextRunnable,它们都有一段相同功能的代码
parentThreadState也是一个HystrixRequestContext对象,它是在hystrix创建线程之前的,也就是处理http请求的线程的HystrixRequestContext对象,我们一般也是维护这个对象。在使用线程池隔离时,hystrix会将parentThreadState中的信息复到到新线程中,实现跨线程的数据传递,从而在后面的逻辑中可以获取到parentThreadState中维护的信息,包括ribbon的路由信息。在bamboo中,将一步骤的逻辑放到BambooRequestContext中,将BambooRequestContext实例本身传递下去。
Ribbon 路由规则
Bamboo中的BambooZoneAvoidanceRule继承了ZoneAvoidanceRule,所以它会有ZvoidanceRule的一切特性,在此基础上,还加入了版本过滤的逻辑,这个逻辑主要是由BambooApiVersionPredicate实现。从BambooRequestContext中获取请求的接口的版本,如果有该没有获取到版本,就返回true;如果有获取到版本,就获取服务实例的metadata中的version信息,并进行匹配校验,返回结果。
灰度发布
灰度发布是在多版本控制的基础上进一步扩展实现出来的项目 -> fm-cloud-graybunny,抽象出灰度服务、灰度服务实例、灰度策略、灰度决策等。支持A/B test, 金丝雀 test。 灰度策略可以从request ip, request patameter, request header等方面进行去创建,也可以根据bamboo的LoadBalanceRequestTrigger结合graybuanny的接口去扩展灰度策略和灰度决策。
场景
有两个服务,共四个服务实例,分别是ServiceA-1, ServiceA-2,ServiceB-1。其中ServiceA-2是灰度实例。
场景1:所有请求头usertype:old,ip:10.217.***.***的请求或者请求头usertype:test, url 参数action:create的请求,都会被转发到的灰度服务ServiceA-2 。
场景2:ServiceA-2通过一段时间的观察,判定运行稳定,开始ServiceA-2删除灰度标记,开始和ServiceA-1一样会加入正常的负载均衡规则当中。
场景3:服务ServiceB发布新版本,ServiceB-2需要灰度注册,注册成功后所有的请求不能转发到ServiceB-2, 在为ServiceB-2设置灰度策略后,符合策略的请求才会被转发到ServiceB-2上。
思路
从上面的场景分析,可以归纳出两个对象:服务实例和调用请求;服务实例的灰度管理是基础,调用请求时如何决策路由,都是根据服务实例的灰度策略去判断的。既然有灰度管理这个概念,那么从功能上分,就会有client-server之分,所以又可以从graybunny-client和graybunny-server去分析。接下来将一步一步去分析这四个方面。
灰度实例
实例注册:服务实例添加到灰度管理中。
实例下线:服务实例下线,从灰度管理中删除。
灰度开关:调整服务实例的灰度状态,有启用、禁用两个状态,禁用的实例不纳入灰度列表中。
灰度策略:请求是否可以被转发到该服务实例的条件,只有通过,请求才有可能会被转发到该实例上。
调用请求
灰度决策:根据请求的信息去匹配灰度服务实例的灰度策略,如果匹配上,会将服务实例加入到通过列表中。如果都没有匹配上,就按bamboo的路由规则去筛选非灰度的服务实例进行转发。
灰度客户端
调用请求的服务消费者,和提供服务的服务提供者都可以是灰度客户端,因为微服务中,大多服务实例既是服务提供者,同时也是服务消费者。
灰度服务注册:服务实例在启动时,就会向灰度服务端发起请求,将实例自身的灰度开关打开。
灰度服务下线:在服务实例下线前,会触发钩子,向灰度服务端发起请求将实例自身从灰度列表中删除。
接收灰度实例调整消息:接收由灰度服务端推送过来的灰度列表更新消息比如新增灰度实例,删除灰度实例等,维护缓存在实例上的灰度列表。
定时拉取灰度列表:定时从灰度服务端拉取最新的灰度列表,维护实例自身缓存的灰度列表。
灰度服务端
灰度服务端负表维护灰度列表,可以新增、删除、编辑灰度信息。
编辑灰度实例:新增灰度实例,删除灰度实例,修改实例灰度状态。
编辑灰度策略:新增实例灰度策略,删除实例灰度策略,修改灰度策略状态。
推送灰度服务调整消息:向灰度客户端推送灰度列表变动消息,比如新增灰度实例,删除灰度实例,修改实例灰度状态等。
定时检查服务实例是否下线:定时检查灰度服务实例是否下线,下线的的实例将从灰度列表中删除。
代码设计
根据上面的思路,设计以下对象和接口。共6个接口,4个模型对象。
对象:
GrayService:
灰度服务
GrayInstance:
灰度实例,有状态属性
GrayPolicyGroup:
灰度策略组,有状态属性
GrayPolicy:
灰度策略
接口:
GrayManager:
灰度客户端管理器,维护灰度列表,维护自身灰度状态,创建灰度决策对象。抽象实现类AbstractGrayManager实现了基础的获取灰度列表,创建灰度决策对象的能力。BaseGrayManger在期基础上进行了扩展,将灰度列表缓存起来,定时从灰度服务端更新灰度列表。
InformationClient:
该接口主要是负责和灰度服务端进行通信,获取灰度列表,编辑灰度实例等能力。其实现类HttpInformationClient默认使用http方式访问灰度服务端。
子类InformationClientDecorator是一个适配器类,RetryableInformationClient继承了InformationClientDecorator类,实现了重试的功能。
GrayDecision:
该接口是灰度决策,用来判断请求是否匹配灰度策略。实现了ip匹配、request parameter匹配、request header匹配、BambooRequestContext中的参数匹配器以及合并匹配等多个匹配能力。
GrayDecisionFactory:
灰度决策的工厂类,其默认实现类支持上述几种灰度决策的创建。
GrayServiceManager:
灰度服务管理类,属于服务端的类。主要是编辑服务实例,编辑灰度策略,以及维护最新的灰度列表。
GrayBunnyServerEvictor:
如果灰度服务实例下线后, 由于意外情况,没有向灰度服务端发送删除请求, 服务端会每隔一段时间调用该接口的方法,检查灰度列表中的实例是否下线,如果实例已下线,就将其从灰度列表中删除。
代码实现
将模型抽象成接口和对象设计出来之后,实现思路就清晰了。
灰度路由:
灰度路由是客户端必须要实现的能力,graybunny是在bamboo的基础上扩展的,所以graybunny的路由规则对象GrayLoadBalanceRule继承了BambooZoneAvoidanceRule,
逻辑是这样的:
1、判断目标服务是否有灰度实例。
2.1、 如果没有, 执行父类逻辑。结束。
2.2、有灰度实例,先将灰度实例和非灰度实例筛选出来。
3、挑选灰度实例, 筛选调用请求匹配上灰度实例的策略。
4.1、如果没有匹配的灰度实例, 将非灰度实例列表传递过去执行父类的筛选逻辑。结束。
4.2、 如果有匹配的灰度实例, 从其中按轮询的方式挑选出一个实例。结束。
灰度决策的执行代码在GrayDecisionPredicate中
灰度管理:
灰度管理是灰度服务端的功能,主要是维护灰度列表。其实现类DefaultGrayServiceManger有一个Map, 用来维护GrayService,key是serviceid。并且每隔一段时间就调用EurekaGrayBunnyServerEvictor,检查列表中的实例是否下线,将下线的服务从灰度列表中删除。
EurekaGrayBunnyServerEvictor是依赖EurekaClient来检查服务实例是否下线。
使用指导
多版本控制
在使用多版本控制时,需要修改服务提供方的两个文件,分别是pom.xml和application.yaml。
1、将bamboo-start项目添加到maven中。
2、在application.yaml中添加versions属性,标明服务支持哪些版本。
在服务消费方,只需要在pom.xml添加bamboo-starter到maven中即可。
在一个名为eureka-client的项目中加入1,2两个步骤, 启动服务。网关做为服务消费方,在pom.xml中加入fm-cloud-starter-bamboo, 并在application.yaml中加入zuul的配置:
启动服务后,访问http://localhost:10002/gateway/client/api/test/get?version=2会返回数据,因为eureka-client支持version=2
如果访问http://localhost:10002/gateway/client/api/test/get?version=3会报错, 因为找不到支持版本3的服务实例
灰度管理
灰度管理的配置和bamboo的配置是一样的,配置方式差别不大。下面先说gray-server的配置。
Gray-Server:
在项目的pom.xml加入spring-boot相关的依赖,再加入bamboo-start、graybunny-server-starter,然后启动就可以了。
在启动类中,需要雇用服务发现。
启动后,可以访问http://localhost:10202/swagger-ui.html#/service-gray-resouce查看接口列表,也可以调用其中的接口。
以上介绍完了gray-server的配置,下面再看gray-client的配置。
Gray-Client:
1、在pom.xml中加入gm-cloud-graybunny。
1、在application.yaml中加入灰度配置。
1、在启动类中加入灰度客户端的注解@EnableGrayBunny
这样灰略度的服务端和客户端都配置好了, 只要在灰度服务端开启灰度实例和灰度策,在灰度客户端就会自动进行灰度路由。
不足
graybunny目前只有灰度管理的基本功能, 像数据持久化,高可用,推送灰度调整消息等,都没有实现。 也没有界面化, 仅仅只有接口列表。
扩展思考
graybunny目前仅仅只支持springcloud eureka, 但是在spring cloud中,eureka只是做为其中一个注册中心,如果要做spring cloud的灰度管理, 就还需要兼容其中的注册中心, 比如zookeeper, consul等。