基于云原生网关的全链路灰度实践
作者: 倪海峰(海迩)
前言
随着企业规模的不断扩大,传统单体应用已很难进一步支持业务的发展,业务的迭代速度已经难以满足业务的增长,此时企业会对应用系统做微服务化的改造,降低业务的耦合度,提升开发迭代的效率,让开发更加敏捷。
系统架构微服务化的,原本的愿景是希望通过将系统的颗粒度变小,提升业务的迭代效率。但是在实践微服务架构的过程中,尤其是在服务数量越来越多之后,那么引发的效率问题可能会大于微服务架构本身所带来的架构红利。
微服务架构下的发布挑战
系统拆分为微服务之后,其中一项业务目标便是希望通过将服务粒度变小,实现业务的高频交付。但是在实践微服务架构的过程中,将上下游服务完全解耦几乎能够存在于理想状态下。常见的情况是,频繁地对微服务做变更发布,通常都会导致业务流量出现大量损失,于是研发人员不得不在晚上业务低峰期做变更。并且在发布过程中,涉及到的上下游团队必须全程待命,以便于在发布阶段发现问题之后,立刻修复,极大地降低了研发人员的幸福感。
如何落地可灰度、可观测、可回滚的安全生产能力,满足业务高速发展情况下快速迭代和小新验证的诉求,是企业在微服务化深入过程中必须面对的问题。
本文将重点讲述在阿里云 EDAS ACK 环境下,对 Spring Cloud 微服务应用进行全链路流量控制的整体方案。通过全链路流量控制功能,可以快速创建流量控制环境,将具有一定特征的流量路由到目标版本应用。
灰度发布实践原则
在微服务架构下,灰度发布实践的关键在于:分层、隔离、兼容这三大要素,在此基础之上,具备业务可观测的能力。分层是在设计灰度发布方案前的前期准备,而隔离和兼容则是实现灰度的主要手段。
在实现全链路灰度场景的流量隔离有两种实现思路:基于物理环境隔离和基于逻辑环境隔离两种。
基于物理环境的隔离,需要为要灰度的服务搭建一套网络隔离、资源独立的环境,在其中部署服务的灰度版本。由于正式环境与灰度环境相对隔离,正式环境无法访问到灰度环境的服务,因此即便是未做版本更新的服务以及各组件,同样也需要在灰度环境中部署。从实现原理上来讲,常见的蓝绿部署便是其中一种技术实现。但是在线上服务足够多的场景下,基于物理环境隔离的方案灵活度相对不足,同时会造成大量的冗余节点以及额外的资源开销。
逻辑环境隔离方案的核心在于对流量染色,当流量在调用链上转发请求时,经过网关以及各个中间件及微服务来识别到被染色的灰度流量,并将请求动态转发至对应的灰度版本。再根据规则作出动态决策。因此当版本发生变化时,调用链路的转发规则同样也会实时作出改变。相比于通过搭建一套物理上隔离的灰度环境,基于逻辑动态调整策略的方式能够节省大量的资源及运维成本,并可以帮助开发者实现更为复杂的全链路控制场景。
标签路由通过标签将一个或多个服务的提供者划分到同一个分组,从而约束流量只在指定分组中流转,实现流量隔离的目的。标签路由可以作为多版本开发测试、同应用的多版本流量隔离以及 A/B Testing 等场景的能力基础。实际上,标签路由的使用场景还有许多,例如实现全链路流控、同 AZ 优先、全链路压测、容灾多活等等。
最后,在具体的工程实践中,并非所有组件通过隔离便能够有效地实现流量的精细化控制,例如数据库等有状态组件,无论是从实施成本还是从技术视角,都不会在每次上线时都重新搭建一套全新的表库,对数据同步之后再刷新对应版本后的 SQL 脚本。因此,在特定场景下的新老版本兼容就成为了必要的前置条件。
架构分析
该项目中后端架构技术栈为 Spring Cloud Alibaba,使用了一整套的阿里云云原生的最佳实践,其中也包括 EDAS、MSE 云原生网关。前端应用使用 VUE 开发,其资源均为静态资源。从应用架构中可以得知,静态资源以 Nginx 为 HTTP 服务对外提供。
在此次架构设计中,业务需求如下:
前端到后端能够按照按照不同的复杂规则(如 header 中的城市、UserID 等)做精细化路由控制,同时当下游服务灰度版本存在异常或不存在时,能够降级至基线服务处理请求。
按照一定百分比随机对线上流量做灰度发布。
支持对消息队列中消息打标,并由对应的 Consumer 消费。
尽可能的零代码改造。
需要对消息队列中的消息做灰度分组,并由对应的 Consumer 将消息消费。
需要具备灰度流量的可观测问题。
EDAS 流量控制
EDAS 是应用托管和微服务管理的云原生 PaaS 平台,提供应用开发、部署、监控、运维等全栈式解决方案,同时支持 Spring Cloud 和 Apache Dubbo 等微服务运行环境。
在 EDAS 平台中,用户可以通过 WAR 包、JAR 包或镜像等多种方式快速部署应用到多种底层服务器集群,轻松部署应用的基线版本和灰度版本。同时,EDAS 能够无缝接入 MSE 的服务治理能力,无需额外安装 Agent 即可零代码入侵获得应用无损上下线、金丝雀发布、全链路流量控制等高级特性。
MSE 云原生网关是阿里云推出的新一代网关,将传统流量网关和微服务网关合并,降低资源成本的同时为用户提供精细化的流量治理能力,支持 ACK 容器服务、Nacos、Eureka、固定地址、FaaS 等多种服务发现方式,支持多种认证登录方式快速构建安全防线,提供全方面、多视角的监控体系,如指标监控、日志分析以及链路追踪。
利用 EDAS 微服务治理能力,配合云原生网关,可以轻松利用多套逻辑环境实现全链路灰度。EDAS 实现了基于字节码增强技术在编译时对开发框架进行功能拓展,这种方案业务无感知,不需要修改任何一行业务代码,即可拥有全链路灰度的治理能力。
全链路流量灰度
流量入口: 入口应用微服务体系内的流量入口,该场景下对应的是 MSE 云原生网关。
泳道: 为相同版本应用定义的一套隔离环境。只有满足了流控路由规则的请求流量才会路由到对应泳道里的打标应用。一个应用可以属于多个泳道,一个泳道可以包含多个应用,应用和泳道是多对多的关系。
基线环境: 未打标的应用属于基线稳定版本的应用,即稳定的线上环境。
流量回退: 泳道中所部署的服务数量并非要求与基线环境完全一致,当泳道中并不存在调用链中所依赖的其他服务时,流量需要回退至基线环境,进一步在必要的时候路由回对应标签的泳道。
泳道组: 泳道的集合。泳道组的作用主要是为了区分不同团队或不同场景。
入口应用: 将符合流控规则的流量打上对应的灰度标识,并使该流量在下游应用中走到对应的应用版本中。由于在本案例中实际使用场景中为 MSE 云原生网关+EDAS,其打标能力全部集中在 MSE 云原生网关上。
从上图中可以看到,用户分别创建了泳道 A 与泳道 B,里面涉及了交易中心、商品中心两个应用,分别是标签标签 2,其中 A 泳道分流了线上 30% 的流量,B 泳道分流了线上 20% 的流量, 基线环境(即未打标的环境)分流了线上 50% 的流量。
通过在 deployment 上配置注解 alicloud.service.tag: gray 标识应用灰度版本,并带标注册到注册中心中,在灰度版本应用上开启全链路泳道(经过机器的流量自动染色),支持灰度流量自动添加灰度 x-mse-tag: gray 标签,通过扩展 consumer 的路由能力将带有灰度标签的流量转发到目标灰度应用。
创建泳道组和泳道
在创建泳道时需要选择入口类型,目前仅支持部署在 EDAS 中的入口应用作为泳道入口应用,需要将泳道中涉及的基线版本和灰度版本添加到泳道组涉及应用中。
创建分流泳道,定义泳道名称,配置泳道流控规则。
创建完成后会生成新泳道,点击泳道名称,将泳道标签值记录下来,本示例为 f2bb906。
灰度应用部署
通过 edas 将应用克隆出灰度(gray)版本,选中所有需要灰度的应用。
给所有灰度应用重命名,命名规则是在基线应用名称的后面加上 -gray 作区分,点击确定,等待应用克隆完成。
当看到所有应用(包括灰度版本应用)已经处于运行中状态且实例数正常,即可进行下一步。
加入泳道组及泳道
回到全链路流量控制页面,找到之前创建的泳道组,点击编辑,将基线和灰度应用都添加进泳道组中,然后点击确定。
在创建完成的灰度泳道中添加灰度应用,找到灰度泳道,点击编辑,添加泳道应用,选择 gray 版的应用。
MSE 云原生网关路由配置
在 EDAS 控制台左侧菜单栏中选中流量管理-应用路由-MSE 网关路由,点击创建路由。
定义路由名称,选择 MSE 网关实例,配置关联域名、匹配规则、请求方法等,这里的路由是到基线应用,无需配置请求头(即无需匹配灰度规则)。
服务来源选择 EDAS 注册中心,目标填入基线应用 a 的配置。注:这里如果选择应用后没有可选的服务,需要检查 k8s 集群上的 agent 状态。
创建 MSE 云原生网关灰度路由
再创建一条 MSE 网关路由用于灰度版本应用,定义路由名称,选择 MSE 网关,添加请求头,key=tag,value=gray,在目标服务中选择灰度应用版本的 a 应用。
给灰度路由添加策略配置,添加 Header 规则,其中 header key:x-mse-tag,header value: 的值为在第 4 步获取的泳道标签,添加规则后并将开启状态开关打开。
添加完成后将对应 base 以及 gray 路由发布上线。
在完成路由策略配置后,对网关后对应的下一跳服务路由,设置 fallback 目标服务到基线服务。
消息灰度
流量入口: 入口应用微服务体系内的流量入口,该场景下对应的是 MSE 云原生网关。
RocketMQ 的订阅关系(Subscription)是系统中消费者获取消息、处理消息的规则和状态配置。订阅关系由消费者分组动态注册到服务端系统,并在后续的消息传输中按照订阅关系定义的过滤规则进行消息匹配和消费进度维护。消息队列 RocketMQ 的订阅关系按照消费者分组和主题粒度设计,因此一个订阅关系指的是某个消费者分组对于某个主题的订阅。
不同消费者分组对于同一个主题的订阅相互独立。
同一个消费者分组对于不同主题的订阅也相互独立。
在消息队列 RocketMQ 的领域模型中,消息由生产者初始化并发送到消息队列 RocketMQ 的服务端,消息按照到达消息队列服务端的顺序存储到对应 Topic 的指定队列中,消费者再按照指定的订阅关系从消息队列 RocketMQ 中获取消息并消费。
在实际业务场景中,同一个主题下的消息往往会被多个不同的下游业务方处理,各下游的处理逻辑不同,只关注自身逻辑需要的消息子集。针对这类场景,RocketMQ 的订阅关系支持在消息传输中按照订阅关系定义过滤规则进行消息匹配和消费进行维护。例如控制消费者在消费消息时,选择主题内的哪些消息进行消费,设置消费过滤规则可以高效地过滤消费者需要的消息集合,灵活根据不同业务场景设置不同的消息接受范围。
消息过滤主要通过以下几个关键流程实现:
生产者:生产者在初始化消息时预先为消息设置一些属性和标签,用于后续消费时指定过滤目标。
消费者:消费者在初始化及后续消费流程中通过调用订阅关系注册接口,向服务端上报需要订阅指定主题的哪些消息,即过滤条件。
服务端:消费者获取消息时会触发服务端的动态过滤计算,消息队列 RocketMQ 版服务端根据消费者上报的过滤条件的表达式进行匹配,并将符合条件的消息投递给消费者。
RocketMQ 支持 Tag 标签过滤和 SQL 属性过滤两种场景。由于一条消息只能打一个 string 类型的标签,因此更适合一些简单过滤场景,而后者是通过生产者为消息设置 K/V 键值对作为属性,并设置 SQL92 语法的过滤表达式过滤多个属性,因此更适合一些复杂过滤场景。此外,Tag 本身也是消息的系统属性,因此 SQL 过滤也兼容 Tag 过滤。在 SQL 语法中,Tag 的属性名称为 TAGS。
在介绍完了 RocketMQ 订阅模式之后,再一起看下消息灰度的实现方案。灰度发布的本质是解决环境隔离的问题。通过不同消息者分组区分正常和灰度版本。在不改变代码及应用配置的情况下,通过服务治理层的 agent 完成消息从发送到订阅的处理逻辑,整体方案思路如下:
1. 创建对应消费者组 : 首先需要用户对当前每个消费者分组预创建的对应的消费者分组 _grayID。例:serviceA_group 对应的分组名为 service_A_group_grayID。(备注:命名必须按照 _gray 的规范创建,否则 agent 在获取到对应的灰度标识之后,无法将 service 同消费者分组建立关联,或直接报错消费者分组不存在)
2. 消息染色: 通过用户定义的灰度环境分组,为对应的服务染色并打上环境标识。当服务开始正常发送消息到对应的 Topic 时,通过服务的 agent 劫持到对应的消息后,打上自定义标签 putUserProerty("gray_tag", "$灰度标识"),并将消息放到对应的 Topic。
3. 动态建立订阅关系: 当消费者被添加到对应的环境之后,会基于环境标识被分配到对应的消费者分组中。并基于消费者组同 Topic 动态建立订阅关系。
4. 消息过滤: 由于生产者会将无论是灰度环境还是生产环境的消息推送到同一个 Topic 中,因此不同的消费者组需要基于对应环境的消息过滤进行消费。在技术实现上,RocketMQ 基于 Tag 和消息属性两种方式均能满足业务场景。方案中之所以采用自定义消息属性的方式,更多的是出于工程实践的考虑。由于消息 Tag 对于一条消息仅支持添加一个 string 类型的消息,在一些业务场景下,可能 Tag 会被应用的业务场景所占用,从而出现不可控的场景,因此通过自定义消息属性,再消费者的 agent 端基于 SQL92 语法实现消息过滤,从容错性上相对更优,因此推荐使用自定义消息属性实现消息分组。
该方案的主要优势在于对于管理和使用人员来说,没有太高的使用成本,在实际完成灰度发布过程中,无需频繁修改代码及对应配置信息。
消息灰度配置
在泳道列表中查询到已创建应用的基线、灰度泳道,点击泳道名称查看泳道详情,并获取泳道标签。
如上图,取得 dev1 泳道标签为 9e4be42,dev2 泳道标签为 e396fee。登录消息服务 RocketMQ 控制台,根据上一步骤获取的泳道标签创建灰度消费者组。如图所示,根据步骤 3 获取的泳道标分别创建消费组 MyConsumerGroup_e396fee 和 MyConsumerGroup_9e4be42。
在 EDAS 控制台选择应用管理,进入应用列表。找到(需要进行灰度流控的消息生产者和消费者)应用 B、应用 B-dev1、应用 B-dev2、应用 C、应用 C-dev1、应用 C-dev2,进入应用详情页进行部署并添加以下环境变量:所有应用均配置环境变量:
基线应用 C 额外配置环境变量:
注: profiler.micro.service.mq.gray.cunsumer.base.excluded.tags 的值通过步骤 3 获取的泳道标签通过逗号“,”拼接得到,若仅有一个灰度环境不需要用逗号拼接。需按实际泳道修改。
消息服务订阅关系检查
登录消息服务 RocketMQ 控制台,查看消费组的订阅关系,如下图:
默认消费组 MyConsumerGroup 由基线应用 C 订阅,并过滤带有灰度标的消息。
灰度消费组 MyConsumerGroup_e396fee 和 MyConsumerGroup_9e4be42 分别只订阅带有对应灰度标的消息。
基于客户端请求实现前端灰度
基于客户端请求 IP 实现前端灰度策略通常的做法是通过 nginx 配置,在用户流量到达 nginx 时,检查用户请求中 http_x_forwarded_for 中的关键字,根据关键字值的不同,重定向请求到不同的前端版本,如下图所示:
Nginx 配置代码如下:
在实际项目中,基于 http_x_forwarded_for 的灰度请求,由于需要枚举出对应的请求 IP,对于客户端有明确的指向性,主要能够解决开发测试人员在发布完成后,在生产环境做业务验证的场景。
基于 IP 的灰度策略也有其自身的局限性,在生产环境下,显然无法满足需要真实随时流量做业务验证的场景。比较通用的方式可通过城市、省份信息做灰度规则判断。在此次方案设计中,前端站点使用阿里云 CDN 做静态加速,而通过阿里云 CDN 的边缘脚本,能够将客户端请求 IP 转换为对应的城市信息。
边缘脚本(EdgeScript,简称 ES)是一个可快速实现 CDN 定制配置的工具箱,当 CDN 控制台上的标准配置无法满足业务需求时,可以尝试使用边缘脚本简单编程实现。
边缘脚本内置了 CDN 节点可以识别的变量、简单的判断语句,同时提供了大量阿里云 CDN 封装好的函数供用户直接调用。通过简单的变量判断并调用现成的函数,即可满足绝大部分定制的鉴权、缓存、限速、请求头增减等定制配置需求,可以有效地解决定制化配置需求无法实现、业务变更不敏捷的问题。
边缘脚本的执行位置如图所示,当客户端请求到达 CDN 节点后,节点网关会根据在控制台上设置的标准配置、边缘脚本规则对请求进行处理。以 CDN 控制台上的标准配置为参照物,边缘脚本可选择在请求处理的最前面或最后面生效。
基于省份城市对流量打标,可通过 IP 地址库识别到请求 IP 所在城市。当前链路后端 API 使用到了阿里云 CDN。
EdgeScript 规则代码如下:
在 Nginx 上对 X-Client-Ip-City 所获取的值做判断,当满足灰度规则时,将请求带上灰度 header,请求到对应网关的灰度规则。
最后,在 MSE 云原生网关控制台,选择对应的 MSE 云原生网关,进入路由管理-路由配置中找到对应的路由进行编辑。
请求头(Header)中设置为:x-client-ip-city 正则匹配 .guangdong.,如下图所示:
灰度流量观测与告警
在全链路灰度发布场景下,由于生产环境会存在两个应用版本,具备较高的运维复杂度。为了能够在出现流量逃逸问题时尽早识别,需要具备灰度流量的可观测能力与逃逸告警能力,当出现非预期流量请求的情况下,开发人员能够通过监控视图对逃逸情况快速分析,并在第一时间通知到相应职能团队对告警进行处理。
基于上述可观测需求,在流量经过 MSE 云原生网关的同时,在路由配置中对应 Header 以满足对所有流量走向的可观测需求。
具体的判定规则为:若 base 路由存在灰度流量、或灰度路由中匹配到基线流量,此时便认为存在流量逃逸。
总结
本文完整介绍了基于物理环境隔离和基于逻辑环境隔离两种方案,其中对基于逻辑环境隔离方案进行详细分析对涉及到的各个技术点做了相关介绍,并基于 EDAS 及 MSE 云原生网关的落地方案,并给出相关产品配置用例。
其中 MSE 为微服务提供了云原生网关、注册和配置中心、微服务治理等丰富的微服务相关能力,EDAS 以应用为视角,深度融合集成 MSE 的各个原子能力,为微服务应用管理提供了一个最佳实践参考,欢迎大家体验交流。
版权声明: 本文为 InfoQ 作者【阿里巴巴云原生】的原创文章。
原文链接:【http://xie.infoq.cn/article/3466e65905e8a533e0b50b964】。文章转载请联系作者。
评论