本文说明经实践基于Spring Cloud全链路灰度解决方案设计与实现。
本人所在公司业务为支付交易类型,业务对线上变更稳定性要求较高,业务一直希望线上变更能够支持全链路灰度能力,提高线上变更稳定性。在设计实现本方案时一直不存在一套能解决业务诉求相对好用的方案。经与业务各负责人沟通提出可否能新开发一套全链路灰度解决方案解决他们线上业务变更灰度诉求?通过调研、设计与业务交流达成也就是本方案用于以后业务线上变更灰度发布方案。
截止写本文时,本方案所支持组件范围包括:网关:ApiSix;RPC:Spring Cloud 3.x,注册中心Eureka ;消息队列:RabbitMQ。后续有新组件落地,再逐步补充。
下面主要说明以上各组件如何建设灰度能力。
1.目标
- 路由能力。支持可以扩展的路由能力。接入层7层网关与服务RPC路由,支持运行时路由规则可配置并实时生效。
- 路由性能<=10us。控制7层网关与服务RPC路由规则条数,一般最大不超过20条。路由性能可<=10us。
- 运行时可观测。通过路由时关键信息打点并上传到监控平台,例如命中哪条策略,路由目标等信息。运行时通过监控平台即可对路由过程追溯。
- 路由安全。因流量进到接入层后属于内网范畴。7层网关与RPC路由规则配置与路由过程都在内网服务中执行,安全性相对可控,必要时增加路由规则配置审核能力。
2.整体设计
- 接入层:接入层使用ApiSix,支持配置路由规则,运行时匹配路由规则,命中路由规则后,通过插件从Eureka注册中心发现服务,并根据header中填写路由标识匹配服务实例,有可用服务实例,将流量打到对应服务实例。假如无匹配实例,可根据开关设置是否将流量打到无标识服务实例。
- 服务层:服务层主要使用Spring Cloud OpenFeign进行RPC调用。支持运行时改变服务实例状态,可从正常实例动态切换到灰度实例,反之亦然。支持运行时配置路由规则改变上游调用下游走向,支持三类路由规则:1.应用维度 2.TRACE维度 3. OpenFeign契约中方法参数维度。
- 消息层:消息层主要使用RabbitMQ。支持运行时动态切换生产消费正常队列、灰度队列,反之亦然。具体支持,生产时假如有多个生产队列,可根据路由规则将消息只生产到命中灰度队列。消费时假如灰度实例消费多灰度队列,支持只消费某一些灰度队列等细粒度控制能力。
基于Spring Cloud全链路灰度能力逻辑视图
3.详细设计
3.1 建设ApiSix灰度能力
ApiSix为流量入口,流量经过ApiSix调用下游服务协议一般为2类,暴漏Http接口的服务,暴漏RPC接口的服务。为统一ApiSix路由规则配置模型,将Http服务也统一注册到Eureka注册中心。详细逻辑如下:
控制面:主要复用了ApiSix控制面能力,路由标识填写在header中。
数据面:自定义路由插件,承载2部分核心功能,从Eureka中发现路由目标服务实例列表,根据header中路由标识做匹配。
在ApiSix配置路由规则时,除了配置详细路由规则,选择路由目标服务(因Http接口的服务也已注册,所以这里也包含Http服务),以外,还需在header中填写路由标识。这时通过自定义路由插件根据header路由标识,去匹配路由目标服务实例。有命中目标服务实例,将流量打到对应实例,无命中目标服务实例,根据开关设置,是否将流量随机打到其他服务实例。
3.2 建设Spring Cloud RPC灰度能力
RPC主要使用Spring Cloud的Open Feign组件,OpenFeign组件用户调用方式也是以接口的方式和其他开源RPC比较类似。经调研业务会按照整个应用、流量来进行灰度,在不改变用户使用方式大原则下。具体做法如下:
数据面:数据面主要从构建FeignBuilder拦截Feign请求,拦截Feign请求并计算路由目标区域,目标实例LoadBalancer三个维度说明:
- 构建FeignBuilder拦截Feign请求:在不改变业务使用方式对Fegin请求流量进行灰度,需要先拦截请求。拦截Feign请求主要有两种方式,1.构建FeignBuilder,在InvocationHandler中执行路由规则。2.实现RequestInterceptor接口,但RequestInterceptor接口有个缺陷,不能获取到业务传递的原始参数及类型,这对按照流量细粒度灰度不利。根据业务诉求驱动因素最终选择方式1。
- 计算路由目标区域:拦截Feign请求后,根据控制面下发路由规则,执行路由规则匹配,计算路由目标区域,根据路由目标区域调用目标服务实例。路由规则支持三种方式,1.应用级别 2.TRACE级别 3.Feign接口的方法参数级别。命中应用级别规则整个应用中任何一个请求都去调用目标灰度实例;命中TRACE级别规则,只有命中规则当前请求调用目标灰度实例;命中Feign接口的方法参数级别规则,只有当前满足匹配参数的请求去调用目标灰度实例。
- 目标实例LoadBalancer:根据计算路由目标区域到服务实例列表中筛选目标实例IP,假如存在多目标实例IP,通过负责均衡算法挑选一个目标实例IP去调用。主要实现ReactorServiceInstanceLoadBalancer接口。
控制面:控制面主要为路由规则相关管控功能。如下图:
3.3 建设RabbitMQ灰度能力
消息队列业务主要使用RabbitMQ。在没有消息灰度能力时,业务主要拦截消息内容来判断,非常不方便,还容易出错。RabbitMQ灰度能力具体做法如下:
主要有2种实现方案,1.利用RabbitMQ的Requeue特性,正常、灰度消息都在同一个队列,灰度节点消费到正常消息就Requeue,直到被正常节点轮训消费,反之亦然。但该方案有个致命问题,假如在RabbitMQ服务端配置了delivery-limit参数,那么该参数相当于削弱了Requeue特性,可能导致被Requeue消息满足参数值后进入死信队列影响业务正常消费。2.将正常队列的Exchange、Queue与灰度Exchange、Queue分开。正常消息进正常队列,灰度消息进灰度队列。经评估验证最终决定使用2这个实现方案。下面主要说明方案2的实现方式。
数据面:数据面主要主要从生产、消费两端说明:
- 生产端:封装的生产端默认情况根据流量路由标识与灰度实例标识匹配,匹配上,就将消息发送到业务原始exchange拼上灰度标识的新exchange,同时会在消息头上带上灰度标识标记,方便消息追溯时能看出消息是灰度消息。还支持通过路由规则打破默认生产消息逻辑,将命中路由规则请求生产消息exchange路由到规则上指定的exchange,也就是指定的灰度队列。这里有个问题,新exchange对应灰度队列可能没建怎么办?有两种方式,1.消费端启动时会根据自身是消费正常消息还是灰度消息声明相关正常、灰度exchange、bindingKey、queue等资源。2.生产灰度消息时先在控制面开启灰度资源,灰度完毕后,可一键关闭灰度资源,提高资源使用率。
- 消费端:封装的消费端支持运行时,根据是否灰度实例,动态切换所消费的正常队列、灰度队列。还支持假如消费者同时消费多队列,可细粒度控制,只消费某一个灰度队列,在找不到灰度队列根据开关设置是否允许消费正常队列。
控制面:控制面功能主要分为待开启灰度资源、已开启灰度资源。在待开启中开启灰度资源,在已开启中管理已开启灰度资源。
4.未来计划
随着业务的深化使用,随后会补充其他支持灰度能力组件及实现方案。