无忧微服务:如何实现大流量下新版本的发布自由

作者:项良、十眠

微服务上云门槛降低,用好微服务才是关键

据调研数据显示,约 70% 的生产故障是由变更引起的。在阿里云上的企业应用如茶百道、极氪汽车和来电等,他们是如何解决变更引起的稳定性风险,实现了在白天高流量情况下应用发布平滑无损。今天我们将揭开阿里云微服务全链路无损发布解决方案的面纱, 快来一起参与探讨,并动手实践吧。

随着微服务开源以及生态的成熟,大大地推进了微服务技术的标准化。下图是展示单体和微服务架构选型的图,其中横坐标代表的是系统复杂度,纵坐标代表着效率,绿色曲线代表着单体架构,蓝色曲线代表微服务架构。我们可以看到随着系统复杂度的升高,单体架构跟微服务之间选型存在着一个拐点,拐点向右更适合选型微服务架构。随着微服务技术的标准化,意味着微服务上云的门槛也大幅度降低,他们之间选型的拐点也在持续地左移,采用微服务架构的企业日益增多。当企业开始大规模推广使用微服务时,如何用好、用稳微服务成为了大家关注的点,用好微服务的关键就是稳定性跟效率。

微服务变更风险大,发布被迫选择半夜

我们来一起看下一些客户真实的诉求,某茶饮企业 变更引起事故占比一度超过 60%,每次发版都要避开高峰,在凌晨发版;某新能源企业 业务连续性要求非常高,核心服务需要 7*24 小时持续在线,但是业务快速发展又需要保证迭代的效率,为了保证业务的稳定性,只能在业务低峰期即凌晨进行发布;同样,某科技公司因为缺少线上灰度能力,每次发布全量发布会导致影响面不可控,存在一定风险。

据调研数据称,70% 的生产故障都是由于变更导致的,除应用本身问题外,运行时风险概括为:不确定流量、不稳定调用、不稳定基础设施。今天,我们聊的解决方案就是为了全面消除变更态风险,解决用户生产变更态的稳定性风险。

我们先来分析一下,为什么微服务应用不能白天发布?因为相比于单体应用来说,微服务架构本身就是复杂的拓扑网络状的调用跟依赖结构。那么就意味着如果我们的微服务应用没有处理好上下线有损的问题,那么我们的任一微服务在发布的过程中都会存在短暂的服务不可用的情况,短时间内会出现大量的异常,导致业务受损。

最直观的感受就是我们一不小心点击了某个应用的重新部署,然后报警群里就会收到大量订单成功率下跌的告警,想想就很吓人。其次,发布时整个功能上线到线上的最后一个环节,一些在设计、研发、测试过程中累计下来的问题,在最后发布的环节才会触发。如果发布涉及到多个应用,如何进行合理发布,并且不会因为版本的问题而导致流量损失?我们总不希望看到,同一个用户在下单页面时候访问到新的版本,到支付阶段又碰到了老版本。

左下图是我拿开源 Spring Cloud 应用进行压测,并且在压测过程中重启应用后,整体压测请求的表现,我们可以看到有大量的异常。再进一步想一下,如果此时是生产系统,并且在白天大流量的场景下,极小的问题都会由于大流量的因素被快速放大,影响面难以控制。所以很多企业跟业务团队,宁愿选择在凌晨进行发布。

MSE 微服务全链路无损发布方案,消除变更风险

如何彻底解决变更稳定性问题?那就介绍今天的主角,MSE 微服务全链路无损发布方案。

我们分别从两个角度来分析问题:

  1. 如果我们新版本的代码没任何问题,服务上下线过程还是会有损,应该怎么解决?

  2. 如果我们新版本的代码有问题,那么我们如何控制问题的影响面?

首先,代码没问题,为什么上下线的环节中流量会有损失。当我们的微服务提供者下线后,服务的消费者无法实时感知节点下线,持续调用已经下线的节点地址,那么流量就会出现持续的报错;如果服务提供者在下线的过程中,请求执行到一半时,应用节点就直接停止了,那么就会导致在途请求以及处理中的请求失败,严重时还会导致线上的数据不一致;同样,在应用上线环节中也会有许多问题,比如新启动的节点如果没有完成预热,就直接承受打流量,那么就会导致新启动的节点直接被打垮,更有严重情况下,大流量场景下服务扩容都扩不出来。同样,微服务体系的生命周期没有跟 K8s 维度的 Readiness/Liveness 生命周期没有对齐,那么在发布的过程中会出现没对齐而导致的请求异常。

如果新版本的代码存在问题,那我们希望在发布新版本的过程中尽可能地控制影响面,阿里巴巴安全生产最佳实践告诉我们,在发布变更的过程中需要做到安全生产三板斧,即可灰度、可观测、可回滚。可灰度就是通过灰度的方式控制问题的影响面,如果我们业务代码新版本存在 bug,大多数情况下我们只能选择在业务低峰期(凌晨)进行发布;在微服务架构下,灰度发布需要做到全链路灰度的能力,目前开源自建很难实现全链路灰度的能力,如果出现流量丢标的情况,会导致灰度流量打到生产环境,引入了额外不可控的风险;可回滚,要求我们在出问题后,需要快速恢复止血,但如果没有一套完整的回滚方案与策略,回滚速度慢或者在回滚过程中引入更大的问题都是不可接受的。

如何解决微服务上下线过程流量有损问题

如果新版本代码没问题,如何解决服务上下线过程中流量有损的问题?

减少不必要的 API 报错,是最好的用户体验,也是最好的微服务开发体验。如何解决这个在微服务领域内让人头疼的问题呢。在这之前我们先来了解一下为什么我们的微服务在下线的过程中会有可能出现流量损失的问题。

原理分析:无损下线

如上图右侧所示,是一个微服务节点下线的正常流程。

  1. 下线前,消费者根据负载均衡规则调用服务提供者,业务正常。

  2. 服务提供者节点 A 准备下线,先对其中的一个节点进行操作,首先是触发停止 Java 进程信号。

  3. 节点停止过程中,服务提供者节点会向注册中心发送服务节点注销的动作。

  4. 服务注册中心接收到服务提供者节点列表变更的信号后会,通知消费者服务提供者列表中的节点已下线。

  5. 服务消费者收到新的服务提供者节点列表后,会刷新客户端的地址列表缓存,然后基于新的地址列表重新计算路由与负载均衡。

  6. 最终,服务消费者不再调用已经下线的节点。

微服务下线的流程虽然比较复杂,但整个流程还是非常符合逻辑的,微服务架构是通过服务注册与发现实现的节点感知,自然也是通过这条路子实现节点下线变化的感知。

参考我们这边给出的一些简单的实践数据,我想你的看法可能就会变得不同。从第 2 步到第 6 步的过程中,Eureka 在最差的情况下需要耗时 2 分钟,即使是 Nacos 在最差的情况下需要耗时 50 秒;在第 3 步中,Dubbo 3.0 之前的所有版本都是使用的是服务级别的注册与发现模型,意味着当业务量过大时,会引起注册中心压力大,假设每次注册/注销动作需要花费 20~30ms,五六百个服务则需要注册/注销花费掉近 15s 的时间;在第 5 步中, Spring Cloud 使用的 Ribbon 负载均衡默认的地址缓存刷新时间是 30 秒一次,那么意味着及时客户端实时地从注册中心获取到下线节点的信号,依旧会有一段时间客户端会将请求负载均衡至老的节点中。

如上图左侧所示,只有到客户端感知到服务端下线并且使用最新的地址列表进行路由与负载均衡时,请求才不会被负载均衡至下线的节点上。那么在节点开始下线的开始到请求不再被打到下线的节点上的这段时间内,业务请求都有可能出现问题,这段时间我们可以称之为服务调用报错期。

通过对微服务下线的流程分析,我们理解了解决微服务下线问题的关键就是:保证每一次微服务下线过程中,尽可能缩短服务调用报错期,同时确保待下线节点处理完任何发往该节点的请求之后再下线。

那么如何缩短服务调用报错期呢?我们想到了一些策略:

  1. 将步骤 3 即节点向注册中心执行服务下线的过程提前到步骤 2 之前,即让服务注销的通知行为在应用下线前执行,考虑到 K8s 提供了 Prestop 接口,那么我们就可以将该流程抽象出来,放到 K8s 的 Prestop 中进行触发。

  2. 如果注册中心能力不行,那么我们是否有可能服务端在下线之前绕过注册中心直接告知客户端当前服务端节点下线的信号,该动作也可以放在 K8s 的 Prestop 接口中触发。

  3. 客户端在收到服务端通知后,是否可以主动刷新客户端的地址列表缓存。

如何尽可能得保证服务端节点在处理完任何发往该节点的请求之后再下线?站在服务端视角考虑,在告知了客户端下线的信号后,可以提供一种等待机制,保证所有的在途请求以及服务端正在处理的请求被处理完成之后再进行下线流程。

实战演练:缩容过程中无损下线的表现

下面我们就通过一个实践来看看无损下线的效果与表现:

该实践分为四个步骤,首先我们在 ACK 控制台配置定时伸缩任务,模拟应用扩缩容的场景,我们可以配置 5 分钟内第 2 分钟扩容,第 4 分钟缩容;第二步,应用接入 MSE 服务治理后默认会具备无损下线能力,我们需要将基线环境增加环境变量关闭无损下线能力,作为对照组;灰度环境接入 MSE 服务治理后,不做任何操作,即默认开启无损下线能力,作为实验组;第三步,我们同时发起基线跟灰度的流量,观察其表现;第四步,我们在 MSE 应用详情的 QPS 数据模块中可以看出,未打标环境(关闭无损下线的应用)在扩缩容过程中出现了 99 个异常,然而灰度环境则无任何错误数。

📝 更多实验细节详见 MSE 文档,结果验证:无损下线功能 [ 1]

原理分析:无损上线

为什么有了无损下线,还需要无损上线?应用启动的过程中也存在许多问题。我们将一个微服务应用的启动过程抽象总结成如下流程,在如下各个阶段我们都会遇到一些问题。

我们先看一下一个标准的微服务启动过程中分别要经过哪几个阶段:

  1. 首先是应用初始化阶段,在这段时间内会进行 Spring Bean 的加载与初始化逻辑;

  2. 第二阶段就是连接池连接建立阶段,比如我们有用到 Redis 的话,就有涉及到 JedisPool 的连接池创建,默认情况下连接池创建后不会立即创建连接,只有等待请求进入后才会去建立连接,之前有个头部客户,在应用启动的过程中就因为连接还没建立完成,但是有大流量涌入,导致大量线程阻塞在连接创建阶段,导致大量的请求错误,因此在这个阶段我们需要提前将连接池的核心连接建立完成;

  3. 第三阶段就是服务注册,我们知道一旦服务完成注册,那么就意味着 Consumer 应用可以发现当前服务,就会有微服务的流量进入当前应用,因此我们需要确保在服务注册之前应用初始化就绪;在某些场景下,比如大数据计算服务,服务在启动后需要异步加载一些准备资源,大数据服务需要提前从 OSS 拉取几百兆的数据,等就绪后才能提供服务,因此如果应用启动后直接注册服务,会导致在异步资源就绪前的流量都因为前置资源没就绪而导致报错;

  4. 第四个阶段就是需要联动 K8s 生命周期,一旦 K8s 的就绪检查通过就认为当前 Pod 已经启动完成。在 K8s 滚动发布的过程中,Readiness 通过后说明 Pod 启动就绪,K8s 就会自动进行下一批 Pod 的滚动发布。如果 Readiness 仅仅关联端口启动,会出现这样的情况,老的 Pod 都已经停机了,但是最早新起的 Pod 依旧没进行完服务注册,从而在短时间内会存在注册中心中该服务没有地址的情况,客户端就会在发布过程中出现 service no provider 异常。

  5. 第五个阶段就是应用跟 Pod 都就绪了,微服务应用在刚启动后由于 JIT 预热、框架资源懒加载等情况,会导致应用刚启动后容量会相比正常情况下小很多,Java 应用在这个场景下问题会显得更加明显。如果这时候不进行任何预处理,应用直接接收线上均分下来的大流量极易出现大量请求响应慢,资源阻塞,应用实例宕机的现象。因此在这阶段我们需要通过小流量预热的方式,合理分配少量的流量启到充分预热我们系统的作用。

只有妥善处理完上面的所有流程,我们的微服务应用在其启动后才可以从容地面对生产上的大流量。

实战演练:无损上线效果演示

下面我们来快速看一下无损上线的 Demo:

首先,我们需要在 MSE 无损上线页面开启无损上线开关,并配置预热时长;然后我们重启应用后,我们就可以看到上线 Pod 的流量曲线如上图所示,缓慢增长,符合小流量预热曲线。

📝 更多实验细节详见 MSE 文档,结果验证:无损上线功能 [ 2]

实战演练:压测态下的无损上下线表现

本次实践的 Demo 以开源 Spring Cloud 框架为例,我们准备了如下 demo。流量是阿里云性能测试服务 PTS 发起,通过开源的 Zuul 网关流入我们的系统。其中其服务调用链路如下图所示:

图中流量从 Netflix Zuul 对应的 Ingress 进来,会调用 SC-A 应用对应的服务,SC-A 应用内部调用 SC-B 应用的服务,SC-B 应用内部调用 SC-C 应用的服务。

我们看下真实压测(500qps 持续压测)场景下无损上下线的效果:

左图是未接入 MSE 的开源 Spring Cloud 应用表现,我们观察到从 17:53:42 开始出错,17:54:24 停止报错,其中报错时长持续 42 秒,总共出现 1 万多个异常。相比实验组,接入 MSE 无损上下线的应用在这个过程中,应用发布对业务流量来说没任何损失,整个过程非常平滑。

📝 更多实验细节详见 MSE 文档,揭秘大流量场景下发布如「丝般顺滑」背后的原因 [ 3]

一个简单的 Demo 应用表现都如此令人吃惊,那么在微服务架构下,生产系统面对每秒上万次请求的流量洪峰,在这个过程中即使服务调用报错期只有短短几秒,对于企业来说都是非常痛的影响。在一些更极端的情况下,服务调用报错期可能会恶化到数分钟,这导致许多企业不敢发布,最后不得不每次发版都安排在凌晨两三点。对于研发来说每次发版都是心惊胆颤,苦不堪言。无损上下线的能力就是为了解决这个问题,保证发布变更过程中流量无损。

如何控制微服务变更过程中的风险

如果我们新版本的代码有问题,那么我们如何可以有效地控制问题影响面?

为了控制变更过程中的风险,我们都知道通过灰度发布的方式来控制问题的影响面;但是在微服务架构的场景下,传统的灰度发布模式往往不能满足微服务交付的复杂、多样化的需求。这是因为:

  • 微服务调用链路比较长,比较复杂。在微服务架构中,服务之间的调用链路比较复杂,一个服务的改动可能会影响到整个调用链路,从而影响整个应用的稳定性。
  • 一次灰度可能涉及多个模块,整个链路都要调用新版本。由于微服务架构中服务之间相互依赖,一个服务的修改可能需要其他服务的相应调整。这就导致了在进行灰度发布时,需要同时调用多个服务的新版本,增加了发布的复杂度和不确定性。
  • 多个项目并行,需要部署多套环境,环境构建不灵活、成本高。在微服务架构中,往往会有多个项目并行开发,需要部署多套环境来支持不同的项目。这就增加了环境构建的难度和成本,从而导致发布效率低下。

为了解决这些问题,我们需要采用更加灵活、可控并且适用于微服务场景的发布方式, 全链路灰度发布的场景也就应运而生。通常每个微服务都会有灰度环境或分组来接受灰度流量。我们希望进入上游灰度环境的流量也能进入下游灰度的环境中,确保 1 个请求始终在灰度环境中传递,从而形成流量"泳道"。在"泳道"内的流量链路中,即使这个调用链路上有一些微服务应用不存在灰度环境,那么这些微服务应用在请求下游应用的时候依然能够回到下游应用的灰度环境中。

生产的流量是端到端的,实现全链路灰度"泳道"那么意味着我们需要控制流量在前端、网关、后端各个微服务等组件中闭环。不仅仅是 RPC/Http 的流量,对于异步调用比如 MQ 流量我们也需要符合全链路"泳道"调用的规则,这整个过程中涉及到的流量控制的复杂度也是非常高的。

MSE 全链路灰度方案

MSE 通过简单的 UI 页面透出流量"泳道"的模型,我们可以通过创建泳道组与泳道,快速实现微服务架构下的全链路灰度发布能力。

  1. MSE 支持在控制台动态配置流量匹配规则引入精细化流量

    1. 支持数值比较、正则匹配、百分比条件等灵活的条件匹配规则,从而满足复杂的灰度发布诉求;
    2. 关于 Http 流量支持 Header、param、cookie 等参数进行匹配;关于 Dubbo 流量可按照服务、方法、参数进行匹配;
  2. 全链路隔离流量"泳道"

    1. 通过设置流量规则对所需流量进行'染色','染色'流量会路由到灰度机器。
    2. 灰度流量携带灰度标签自动传递,形成灰度专属环境流量泳道,对于无灰度环境应用灰度流量会默认选择未打标的基线环境;其中流量标签传递默认支持RPC、MQ 等流量。
  3. 端到端的稳定基线环境

    1. 未打标的应用属于基线稳定版本的应用,即稳定的线上环境。当我们将发布对应的灰度版本代码,然后可以配置规则定向引入特定的线上流量,控制灰度代码的风险。
    2. 在每一跳请求路由的过程中,灰度流量优先走流量标签对应的机器,如果没有匹配则 fallback 到基线环境;
  4. 流量一键动态切流,流量规则配置后,可根据需求进行一键停启,增删改查,实时生效。灰度引流更便捷。

  5. 低成本接入,基于 Java Agent 技术实现无需修改一行业务代码;无缝支持市面上近 5 年的所有 Spring Cloud 和 Dubbo 的版本,用户不用改一行代码就可以使用,不需要改变业务的现有架构,随时可上可下,没有绑定。

同时 MSE 全链路灰度能力还提供了配套的可观测能力,我们通过全链路灰度观测可以实时看到灰度情况的流量曲线,无论是测试验证阶段、还是生产灰度发布过程中,对于灰没灰到的流量情况我们可以做到心中有数。

谁都在用 MSE 全链路无损发布方案?

在阿里云上的企业应用如茶百道、极氪汽车和来电等,他们是如何解决变更引起的稳定性风险,实现了在白天高流量情况下应用发布平滑无损。MSE 全链路无损发布方案助力云上微服务实现白天发布自由。

极氪汽车

随着极氪汽车销售越发火爆,其注册用户和每日活跃用户快速增长,需要支持的业务场景和新功能也越来越多,平均两三天一个小版本、半个月一个大版本的升级频率。为了不影响白天业务高峰,每次发版只能选择在凌晨业务低峰期进行,想象一下如果开发/运维/测试人员每次都集中在晚上发布,那么这些参与发布的同学第二天的工作效率将会受到影响;如果晚上选择较少的人参与发布,那么当出问题的时候止血措施很可能会来不及实施,故障责任也不好划分。

极氪汽车针对核心业务链路上多个微服务同时需要发版的场景,基于 MSE 全链路无损发布解决方案实现了多业务的全链路灰度,通过这种方式让客户在不需要更改任何业务代码的情况下实现多业务白天发版,同时通过逐步流量放大进行验证,如出现问题可及时回切流量,降低了白天发布可能导致的稳定性风险。 同时通过改造云效流水线,帮助客户实现核心业务自动化发布,进一步提升了发布的效率。

来电科技

来电科技在全面微服务化、容器化改造完成之后就面临了如何用好微服务的难题,他们核心架构师也是 Dubbo 社区的参与者,对比了自研微服务治理的成本后,他们决心使用 MSE 治理解决微服务化深入后面临的稳定性与效率的问题。来电科技通过 MSE 全链路无损发布解决方案有效解决变更过程中灰度的诉求,以及发布过程中流量抖动的问题, 这个过程中无需代码改动与架构调整,借助 MSE 服务治理以更经济的方式、更高效的路径在云上快速构建起完整微服务治理体系。

总结

如何稳定地使用微服务已成为大家关注的话题。本文详细介绍了阿里云微服务全链路无损发布最佳实践方案,相信这个方案可以帮助云上的企业更好地利用微服务。在过去几年许多企业客户的实践中,我们逐步完善了这个方案。我相信这些最佳实践就像大海中的明珠一样,在不断的实践和时间的磨砺中变得更加耀眼夺目。我们期待着将这些实践与经验分享给更多企业,让他们能够更好地应用微服务,取得更好的效果。

相关链接:

[1] 结果验证:无损下线功能

https://help.aliyun.com/zh/mse/use-cases/implement-graceful-start-and-shutdown-of-microservice-applications-by-using-mse?spm=a2c4g.11186623.0.0.727b5fffObARTt#section-qvv-175-948

[2] 结果验证:无损上线功能

https://help.aliyun.com/zh/mse/use-cases/implement-graceful-start-and-shutdown-of-microservice-applications-by-using-mse?spm=a2c4g.11186623.0.0.727b5fffObARTt#section-6gd-9lg-6zk

[3] 揭秘大流量场景下发布如「丝般顺滑」背后的原因

https://developer.aliyun.com/article/780231

相关推荐
雪球不会消失了1 小时前
MVC架构模式
架构·mvc
didiplus1 小时前
Kubernetes 镜像拉取策略全解析:如何根据需求选择最佳配置?
云原生·容器·kubernetes
云云3213 小时前
云手机:Facebook多账号管理的创新解决方案
服务器·线性代数·安全·智能手机·架构·facebook
窗外的寒风3 小时前
java工作流模式、背包模式、适配器工厂模式整合架构,让服务任务编排更便捷
架构·适配器模式
上海运维Q先生4 小时前
面试题整理17----K8s中request和limit资源限制是如何实现的
服务器·云原生·kubernetes
绝无仅有5 小时前
PHP语言laravel框架中基于Redis的异步队列使用实践与原理
后端·面试·架构
AI人H哥会Java5 小时前
【Spring】基于XML的Spring容器配置——<bean>标签与属性解析
java·开发语言·spring boot·后端·架构
会飞的土拨鼠呀5 小时前
Flannel是什么,如何安装Flannel
运维·云原生·kubernetes
木与子不厌5 小时前
微服务自定义过滤器
运维·数据库·微服务
微扬嘴角5 小时前
springcloud篇1(微服务技术栈、服务拆分与远程调用、Eureka、Nacos)
spring cloud·微服务·eureka