灰度发布, 也叫金丝雀发布。是指在黑与白之间,能够平滑过渡的一种发布方式。AB test就是一种灰度发布方式,让一部分用户继续用A,一部分用户开始用B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。
灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度,而我们平常所说的金丝雀部署也就是灰度发布的一种方式。
具体到服务器上,实际操作中还可以做更多控制,譬如说,给最初更新的10台服务器设置较低的权重、控制发送给这10台服务器的请求数,然后逐渐提高权重、增加请求数。一种平滑过渡的思路, 这个控制叫做“流量切分”。
组件版本说明
我们这项目已经练习了两年半了使用的版本不是很新,我这里的Demo也会使用这个版本,有感情了,使用新版本的朋友自己调整一下就行,实现思路是一样的只是这些框架源码可能会有变化。
- spring-boot: 2.3.12.RELEASE
- spring-cloud-dependencies: Hoxton.SR12
- spring-cloud-alibaba-dependencies: 2.2.9.RELEASE
spring-cloud 对应版本关系图
❝
https://blog.csdn.net/weixin_44606481/article/details/131726688
❞
核心组件说明
- 注册中心: Nacos
- 网关: SpringCloudGateway
- 负载均衡器: Ribbon (使用SpringCloudLoadBalancer实现也是类似的)
- 服务间RPC调用: OpenFeign
灰度发布代码实现
要实现Spring Cloud项目灰度发布技术方案有很多,重点在于服务发现,怎么将灰度流量只请求到灰度服务,这里我们会使用Nacos作为注册中心和配置中心,核心就是利用Nacos的Metadata设置一个version值,在调用下游服务是通过version值来区分要调用那个版本,这里会省略一些流程,文章末尾提供了源码地址需要自提。
代码设计结构
这个是demo项目,结构都按最简单的来。
核心包spring-cloud-starter-kerwin-gray结构介绍
入口Spring Cloud Gateway实现灰度发布设计(一些基础信息类在下面)
在请求进入网关时开始对是否要请求灰度版本进行判断,通过Spring Cloud Gateway的过滤器实现,在调用下游服务时重写一个Ribbon的负载均衡器实现调用时对灰度状态进行判断。
存取请求灰度标记Holder(业务服务也是使用的这个)
使用ThreadLocal记录每个请求线程的灰度标记,会在前置过滤器中将标记设置到ThreadLocal中。
前置过滤器
在前置过滤器中会对请求是否要使用灰度版本进行判断,并且会将灰度状态枚举GrayStatusEnum
设置到GrayRequestContextHolder
中存储这一个请求的灰度状态枚举,在负载均衡器中会取出灰度状态枚举判断要调用那个版本的服务,同时这里还实现了Ordered 接口会对网关的过滤器进行的排序,这里我们将这个过滤器的排序设置为Ordered.HIGHEST_PRECEDENCE int
的最小值,保证这个过滤器最先执行。
后置过滤器
后置过滤器是为了在调用完下游业务服务后在响应之前将 GrayFlagRequestHolder
中的 ThreadLocal 清除避免照成内存泄漏。
全局异常处理器
全局异常处理器是为了处理异常情况下将 GrayFlagRequestHolder
中的 ThreadLocal 清除避免照成内存泄漏,如果在调用下游业务服务时出现了异常就无法进入后置过滤器。
自定义Ribbon负载均衡路由(业务服务也是使用的这个)
「灰度Ribbon负载均衡路由抽象类:」 这里提供了两个获取服务列表的方法,会对GrayFlagRequestHolder
中存储的当前线程灰度状态枚举进行判断,如果枚举值为GrayStatusEnum.ALL
则响应全部服务列表不区分版本,如果枚举值为GrayStatusEnum.PROD
则返回生产版本的服务列表,如果枚举值为GrayStatusEnum.GRAY
则返回灰度版本的服务列表,版本号会在GrayVersionProperties
中配置,通过服务列表中在Nacos的metadata中设置的version
和GrayVersionProperties
的版本号进行匹配出对应版本的服务列表。
「自定义轮询算法实现GrayRoundRobinRule:」 代码篇幅太长了这里只截取代码片段,我这里是直接拷贝了Ribbon的轮询算法,将里面获取服务列表的方法换成了自定义AbstractGrayLoadBalancerRule
中的方法,其它算法也可以通过类似的方式实现。
业务服务实现灰度发布设计
自定义SpringMVC请求拦截器
自定义SpringMVC请求拦截器获取上游服务的灰度请求头,如果获取到则设置到GrayFlagRequestHolder
中,之后如果有后续的RPC调用同样的将灰度标记传递下去。
自定义OpenFeign请求拦截器
自定义OpenFeign请求拦截器,取出自定义SpringMVC请求拦截器中设置到GrayFlagRequestHolder
中的灰度标识,并且放到调用下游服务的请求头中,将灰度标记传递下去。
基础信息设计
这里会定义一些基础参数,比如是否开启灰度还有什么请求需要使用灰度版本等,为后续业务做准备。
- 调用业务服务时设置的灰度统一请求头
- 灰度版本状态枚举
- 网关灰度配置信息类
- 全局版本配置信息类
- 全局自动配置类
项目运行配置
这里我会启动五个服务,一个网关服务、一个用户服务V1版本、一个订单服务V1版本、一个用户服务V2版本、一个订单服务V2版本,来演示灰度发布效果。
❝
PS:Nacos的命名空间我这里叫spring-cloud-gray-example可以自己创建一个也可以换成自己的命名空间,源码里面配置都是存在的,有问题看源码就行
❞
配置Nacos全局配置文件(common-config.yaml)
所有服务都会使用到这个配置
配置网关Nacos配置文件(gateway-app.yaml)
启动网关服务
网关服务启动一个就行,直接Debug启动即可,方便调试源码
启动业务服务V1 和 V2版本(用户服务和订单服务都用这种方式启动)
先直接Debug启动会在IDEA这个位置看到一个对应启动类名称的信息
点击Edit编辑这个启动配置
复制一个对应启动配置作为V2版本,自己将Name改成自己能区分的即可
配置启动参数,第一步点击Modify options
然后第二步将Add VM options
勾选上,第三步填写对应服务的启动端口和Nacos的metadata.version
,我这里用户服务V1版本配置为-Dserver.port=7201
-Dspring.cloud.nacos.discovery.metadata.versinotallow=V1
,用户服务V2版本配置为-Dserver.port=7202
-Dspring.cloud.nacos.discovery.metadata.versinotallow=V2
,订单服务配置类似,配置好后点Apply。
最后启动好的服务信息
灰度效果演示
源码中的user-app提供了一个获取用户信息的接口并且会携带当前服务的端口和版本信息,order-app服务提供了一个获取订单信息的接口,会去远程调用user-app获取订单关联的用户信息,并且也会携带当前服务的端口和版本信息响应。
场景一(关闭灰度开关:不区分调用服务版本)
关闭灰度开关有两个配置可以实现
1、在项目启动之前修改Nacos全局配置文件中的kerwin.tool.gray.load
配置是否加载灰度自动配置类,只要配置不为true就不会加载整个灰度相关类
2、关闭网关灰度开关,修改网关Nacos配置文件中的kerwin.tool.gray.gateway.enabled
,只要配置不为true就不会进行灰度判断。
调用演示
这里调用不一定就是Order服务版本为V1 User服务版本也为V1,也有可能Order服务版本为V1 User服务版本也为V2.
- 第一次调用,Order服务版本为V1,User服务版本也为V1
- 第二次调用,Order服务版本为V2,User服务版本也为V2
场景二(开启灰度开关:只调用生产版本)
修改网关Nacos配置文件中的kerwin.tool.gray.gateway.enabled
设置为true,其它灰度IP数组和城市数组配置匹配不上就行,这样怎么调用都是V1版本,因为在GrayVersionProperties
版本配置中设置的生产版本就是为V1灰度版本为V2。
场景三(开启灰度开关:通过请求头、ip、城市匹配调用灰度版本)
这里通过请求头测试,携带请求头gray=gray-996
访问网关那么流量就会都进入灰度版本V2。
源码
❝
https://gitee.com/kerwin_code/spring-cloud-gray-example
❞
存在问题
1、如果项目中使用到了分布式任务调度那怎么区分灰度版本
这里其实挺好解决的,就拿xxl-job来说,注册不同的执行器就行,在发布灰度版本时注册到灰度版本的执行器即可。
2、如果项目中使用的了MQ我们收发消息怎么控制灰度
这里和解决分布式任务调度思想是一样的灰度版本的服务发送消息的时候投递到另外一个MQ的服务端,就是弄两套MQ服务端,生产的服务使用生产的MQ,灰度发布使用灰度的MQ
3、这里整个实现流程不是很复杂,但也是很没必要,只是提供一种实现方案可以参考
其实通过Nginx + Lua脚本方式直接路由网关,然后给灰度整套服务都使用一个Nacos灰度的命名空间,生产的使用生产的命名空间,这样就能将两套服务都隔离了,分布式任务调度、MQ等配置都可以独立在自己命名空间的配置文件中岂不美哉