一、设计背景
随着近几年微盟业务范围的快速拓展,商家用户不断增多对微盟业务研发团队提出更高的版本迭代速度与质量要求。在多环境推出之前,业务研发团队进行产品并行迭代开发时,因测试环境单一无法满足并行提测,而建设多套测试环境的维护与实际成本又过于高昂,遂转向对多环境灰度进行立项研发。
微盟业务线众多且存在相互依赖,而微盟所有应用均采用微服务模式。以上导致微盟一次发布迭代涉及的应用数众多,需要上下游人力协调进行发布,且发布过程中出现问题时需要挨个回滚。整个发布极不灵活且存在不可控风险。
二、全链路灰度带来的挑战
一次发布往往对应一次业务迭代,在单体架构盛行的时候,由于一次迭代往往都只涉及一个服务,灰度发布通常也只针对单个应用。但随着微服务的发展,单个服务的职责越来越单一,服务的拆分越来越细,服务的数量越来越多,一次业务迭代往往涉及到多个服务,这个时候为了实现一次迭代的灰度上线,我们需要考虑的不再是单个应用的灰度,而是满足了灰度策略的流量在全链路上的每个应用都需要进入对应的灰度环境,即:全链路灰度。二者区别如下所示:
相比传统的灰度发布,全链路灰度复杂度更高,需要解决的问题更多。
1、资源隔离
资源隔离的必要性在于:
- 实现全链路灰度的前提在于能够区分灰度环境跟生产环境,根据流量标签确保灰度流量进入灰度环境,这就要求我们对灰度环境内的资源进行隔离。当然这里说的隔离并不意味着要重新部署一套环境,而是基于各种资源的差异性、实际的需求以及成本各方面考虑决定的隔离方案,也有可能是逻辑上的隔离。
- 尽可能避免灰度环境影响生产环境的稳定性。隔离程度越高,对生产环境影响更低。
- 我们需要对灰度环境建立完善的监控。隔离程度越高,监控成本更低,更方便建立完善的监控机制。
- 合适的隔离机制,会让我们管理维护更加方便。
K8s
对于 K8s 集群有两种隔离方式:
1、物理隔离,灰度环境跟生产环境使用两套不同的集群。
这种方案的缺陷在于在其中部署服务的灰度版本,由于与正式环境隔离,正式环境中的其他服务无法访问到需要灰度的服务,所以需要在灰度环境中冗余部署这些线上服务,以便整个调用链路正常进行流量转发。此外,注册中心等一些其他依赖的中间件组件也需要冗余部署在灰度环境中,保证微服务之间的可见性问题,确保获取的节点 IP 地址只属于当前的网络环境。
这个方案一般用于企业的测试、预发开发环境的搭建,对于线上灰度发布引流的场景来说其灵活性不够。况且,微服务多版本的存在在微服务架构中是家常便饭,需要为这些业务场景采用堆机器的方式来维护多套灰度环境。如果应用数目过多,会造成运维、机器成本过大,成本和代价远超收益。
2、目前业界常见的做法是通过标签对 K8s 中的资源:Deployment、Svc、Pod等做逻辑隔离,灰度跟生产使用同一套物理集群,如下图所示:
RPC
对于 PRC 来说,涉及 Provider(服务提供者)、Consumer(服务消费者)、注册中心。Provider 跟 Consumer 都部署在 K8s 集群中,通常都是通过标签进行逻辑隔离,对于注册中心我们有两种选择:
- 灰度跟生产使用同一套集群;
- 灰度注册中心独立部署一套集群;
我们先看第一种方案:
使用这种方案时,Provider 在注册服务时需要将环境标签,通常就是读特定的取环境变量,作为服务元信息的一部分注册到注册中心。Consumer 同时订阅灰度跟生产环境的所有 Provider,通过**「标签路由」**的方式进行调用。关于标签路由可以参考 Dubbo2.7.x 的实现。
第二种方案:
相比于第一种方案,此方案的隔离程度更高,不会对现有的生产环境的注册中心造成过大压力,同时注册中心的监控也更加准确,但是实现成本,维护难度也更高。
MQ
对于消息队列的隔离,目前业界主要两种隔离方式1、Topic级别隔离,灰度的消息发送到灰度的topic;
Producer在生产消息时,需要根据流量标签将消息划分为生产消息、灰度消息,同时将不同的消息发送到不同的环境。
对于需要灰度的应用,灰度的 consumer 消费灰度的消息,生产环境的 consumer 消费生产环境的 topic 的消息。
基于 topic 的隔离,隔离程度高,很容易对灰度的 topic 进行高效的监控,但成本高,同时由于消费被拆分到两个不同的 topic,因此对于不需要灰度的应用需要同时监听灰度的topic 跟生产的 topic 才能保证消息的不丢失。
2、灰度消息跟生产消息发送到同一个topic;
producer 在生产消息时,需要将流量标签设置到消息中,consumer 在消费时检查 tag,灰度环境内的 consumer 只消费灰度的消息,对于生产消息直接回应 ack,生产环境内的consumer 只消费生产消息,对于灰度消息直接回复 ack。
相比于第一种方案,此方案的好处在于:成本低、消费方处理简单。但是由于其隔离程度低,弊病也显而易见,首先监控难度大,其次不论是灰度的 consumer 还是生产的 consumer 都可能拉取到不需要进行处理的消息,这势必会对消息的消费速率造成影响。
2、流量标传递
解决了资源隔离的问题后,接下来一个很重要的议题就是流量标签如何在整个调用的过程中传递。在前文中我们知道,RPC的consumer 需要一个流量标签来实现标签路由,消息队列的 consumer 也可能需要一个流量标签来决定自己是否进行真正消费。在一个请求链路中可能有多次 RPC 调用+多次消息转发,那么如何让流量标签在整个链路中正确传递呢?
分布式链路追踪技术(例如Skywalking、Zipkin)对大型分布式系统中请求调用链路进行详细记录,核心思想就是通过一个全局唯一的 traceId 和每一条的 spanId 来记录请求链路所经过的节点以及请求耗时,其中 traceId 是需要整个链路传递的。
借助于分布式链路追踪思想,我们也可以传递一些自定义信息,比如灰度标识。业界常见的分布式链路追踪产品都支持链路传递用户自定义的数据。笔者就以下图为例阐述一下整个传递过程。
1、请求经过前端后会带上定制标签,这个标签跟需要灰度的维度相关,如果是用户维护的灰度那么可能就是userId=xxx;2、前端的标签通过通过header、queryString的方式传递网关;3、假设请求1为Http调用,那么网关在发起调用时需要将标签继续通过headerr、queryString的方式传递;4、服务A接收到调用请求后,需要从Http请求中解析出对于的标签并放入到本地上下文;5、当服务A发起请求2时,需要从本地上下文中获取到标签。假设此次调用是RPC调用,那么一种可选的方式是将标签放入到rpcContext中;6、服务B接收到调用请求后,需要从rpcContext中解析出对于的标签并放入到本地上下文;
、当服务B发起请求3时,需要从本地上下文中获取到标签。假设此次调用是通过MQ发送消息,那么我们需要将本地上下文中的标签设置到消息中,继续向下传递;
3、多框架支持
通过上文我们知道,不同的组件在实现灰度时都有细节上的差异,那么不可避免的,我们需要对业务使用的开发框架 SDK 能力进行增强。首先,对于 RPC 框架,需要支持动态路由功能,可以对出口流量实现自定义 Filter,在该 Filter 中完成流量识别以及标签路由。同时需要借助分布式链路追踪技术完成流量标识链路传递以及流量自动染色。
其次,对于 MQ,我们需要对生产消息跟消费消息的能力进行包装,保证消息的正确消费跟正确路由。
为了实现对原有框架的增强,通常我们有两种方式:包装原有 SDK 或者使用 JavaAgent 进行无侵入增强。
SDK
如果使用 SDK 的方式,意味着对于不同的框架我们都需要包装不同的 SDK,并且 SDK 的实现强依赖于灰度的具体实现细节,即使我们可以通过抽象层的方式降低依赖,但是后期维护成本以及升级成本巨大。
SDK的好处在于,灰度的实现逻辑可见,方便调试,如果灰度涉及到的框架并不复杂,也不失为一种好的方案。
JavaAgent
如果灰度涉及到的框架众多,那么再使用包装 SDK 的方式就不明智了。这种情况下,更加推荐使用 JavaAgent 进行无侵入的增强。实际上前文提高的分布式链路追踪技术Skywalking 其客户端的核心逻辑就是通过 skywalking.jar 这个 JavaAgent 实现的,github地址:github.com/apache/skyw… JavaAgent 也需要慎重,其弊病也是显而易见的:调试难度大,往往出问题难以定位;技术侵入性大,大部分业务开发并不清楚什么是 JavaAgent,往往会增大沟通成本。
三、全链路灰度在微盟的落地
目前微盟的灰度平台不仅仅解决上文中提到的问题,还在其基础上提供了更加丰富的功能
1、使用 Agent 无侵入的方式实现了各个组件的灰度路由,保证业务部分低成本接入;2、灵活的策略调整方式,支持逐步放大流量同时支持流量一键回滚;3、打通配置中心,支持灰度配置,配置的发布跟环境状态保持一致;4、接入公司其它发布系统,如:微前端发布平台,支持多个发布平台联动完成灰度5、支持工作流相关配置同步灰度;整体架构设计如下图所示:
1、UI层,通过UI层用户可以创建灰度环境并执行策略编辑、灰度转正等操作。目前用户可以在界面上做如下状态流转:
【初始状态】 :此时环境已经创建完成,用户还没有在页面上点击开始灰度按钮。可能存在应用可能还未完成发布,对应界面如下:
在这个状态下可以自由的向环境中增删应用,也可以多次发布应用,「灰度环境内发布的应用不会承接任何流量,下线发布成功的应用对生产流量也不会造成任何影响」。 【灰度状态】 :在初始状态完成所有准备操作后,点击开始灰度,成功后,环境流转至此状态。此状态是整个灰度的核心,在这个阶段会完成对整个灰度的验证。
在灰度状态下我们需要满足以下诉求:
- a、灰度的过程可以逐步放大流量;
- b、由于灰度环境的流量会逐步方法,所以要求灰度的副本数能够随流量一起调整;
- c、流量放大到一定比例后可以将所有流量接管到灰度环境,实际上此时灰度进入蓝绿发布状态;
- d、如果在验证过程中发现不可修复问题,需要快速将流量切换到生产环境;
- 问题在我们提供的策略编辑模块都得到了解决。
我们提供了流量划拨的功能,分为:
a、部分流量灰度,在进行部分流量灰度时可以通过调整灰度条件或者灰度流量比例逐步放大流量。b、全流量灰度,此时所有流量进入灰度环境,实际就是进入蓝绿发布状态。c、流量全切基准,在灰度出现不可修复问题时,可以一键将所有流量切换到基准。
同时,在每次划拨的过程中,支持对灰度环境内应用进行副本数量调整。
需要注意的是在灰度状态下,应用不可编辑、不可下线,如果要向环境内添加或下线应用可以通过【环境调整】过程,将环境状态重置为【初始状态】。
【灰度转正】 :在灰度环境完成了版本验证之后,我们接下来需要将灰度的版本发布到生产环境。我们将其定义为**「灰度转正」**。灰度转正会自动将灰度版本发布到基准环境,灰度转正成功代表本次灰度发布成功,灰度转正是环境状态的终态,转正成功后灰度资源将会被全部释放,环境不可恢复,转正过程中对流量是无损的。
【灰度下线】 :在灰度状态下,如果遇到不可修复的问题,此时我们要放弃本次灰度的话,可以通过点击灰度下线按钮将环境状态流转到灰度下线状态,灰度下线是环境状态的终态,下线成功后所有灰度资源会被释放,环境不可恢复。
2、业务层,即灰度平台,实现了UI层向外暴露的功能,并提供了权限控制的能力并将数据持久化到MySQL中。
3、策略层,向业务层提供策略校验等能力,同时接受业务层推送过来的策略,待策略生效后写入到路由层,不同的组件在实现上存在一定差异。
4、路由层,使用已经生效的路由策略进行路由,域名访问通过公司统一网关进行路由,Dubbo调用通过Javaagent进行路由,微前端应用通过我们自建的微前端路由器进行路由。
四、未来规划
1、监控
目前基于公司 APM 的现有能力,我们已经能通过链路对灰度环境内的应用做有效的监控,包括接口的 QPS、接口执行次数等。
除此之外,链路也能区分基准环境跟灰度环境。
接下来,我们会进一步强化监控方面的能力,将灰度监控能力跟基准环境监控能力对齐,提供更加全面的监控能力!
2、灰度开放能力
在前文中给大家介绍了为了实现全链路灰度,应用中的不同的组件的实现细节。实际上,随着灰度平台的推广,我们逐渐认识到灰度的诉求不仅仅来自于应用内的组件,还可以来自于管理了不同应用的平台,我们不仅仅要实现应用所涉及到的组件的灰度,还要将灰度平台跟其他应用管理系统甚至业务系统进行打通。
基于此,就对灰度平台提出了更高的要求:我们需要提供一套灵活的扩展机制,将外部系统以插件的方式接入到灰度平台。如下图所示:
实际上,为了提高灰度平台的扩展性,以更开放的态度容纳更多组件、应用、甚至系统接入到灰度平台,我们不仅会提供上诉针对外部生态系统的扩展机制,还会暴露一些SDK提供更加灵活的接入方式。
目前这些能力,或在开发,或在测试,在不久的将来我们一定能迎来一个更加开放、更加稳定的灰度平台!