SaaS软件架构设计系列 | 6-1 灰度发布

396 阅读11分钟

灰度发布

灰度发布有多种方式,目的主要是提升系统的可用性,当出现问题时能够减少影响范围,同时能够快速回滚。灰度发布也有降低发布风险的作用,例如我们前面谈到的在多个团队协调一起迭代发布时,由于集成时间长,可能一晚上都无法完成的场景。本次我们主要探讨灰度发布中的一些共性问题,以及SaaS架构下灰度发布的一些特点。以下的讨论主要以蓝绿发布模式为主。

流量控制

流量分发维度

灰度发布首先要解决的问题是流量分发的问题,对于SaaS来讲,最好的流量分发维度是按租户来分发。不同的租户运行在不同版本的应用中,这样可以减少很多应用版本兼容和依赖的问题。例如某次发布涉及多个服务的更新,服务间的接口发生了变化,按正常的处理方式兼容成本可能会比较高,如果按租户灰度,可以将依赖双方的服务都升级到新版本,就不用处理兼容性问题。在微服务架构阶段,如果要支持不同服务发布周期的解耦,需要支持服务+租户的维度进行流量分发。

很多可能发布周期的解耦是按团队来的,如果都控制到服务会配置上会比较繁琐,这个时候可以在服务进行分组,设置时可以对一组服务进行流量设置,但实现上还是建议能够支持到服务+租户的颗粒度,这样能够更灵活的应对特殊场景。

流量入口

对于灰度发布来说,应用程序的所有流量入口都需要被管理并能够分流,下面是总结的常见流量入口清单。

流量入口分类建议解决方案
Web/H5如果域名相同,未登录前无法区分租户,登录页面和逻辑无法灰度,登录完成后再区分。
小程序共用的较难支持多版本,如果是租户单独的,可以控制版本,SaaS场景一般客户需要运营自己的小程序,可以让客户自己申请小程序,SaaS代管理。
微信公众号同Web/H5.需要注意微信公众号的OAuth2授权处理的部分不能部署多副本(会导致AccessToken覆盖),无法灰度。
APP共用且上架到应用市场的APP,如果采用H5混合开发的APP,可以根据灰度设置下载不同的应用程序版本进行更新(纯原生的没有研究过...)。对于不上架到应用市场的APP,可以按灰度租户分批进行自动更新。如果是租户自己的APP,则可以在分发APP时,内置租户标识,按租户灰度。
PC软件可以按灰度租户分批进行自动更新。
分享出去的链接建议使用短链接服务,然后在短链接中记录流量标识。这样可以简化分享链接的生成,而且能够应对转发后访问径发生变化的场景。
提供给第三方的API可以要求访问参数带流量标识或通过用户、授权ID转换为租户。
服务间相互调用如需支持服务+租户级别的分流,服务间调用也需要有流量标识。
前端回调调用第三方API时,一般可传递回调地址,需要带流量标识;如果不支持,需要一层转换,可以通过业务ID+租户映射缓存的方式来处理。
后台通知第三方推送的消息通知,需要带流量标识;如果不支持,需要一层转换。
消息发布订阅需要在元数据或消息体固定结构中有流量标识。

流量的分配

针对不同环境的流量分配可能有多种策略。我们定义有两个环境,一个是灰度环境,一个是正式环境。针对不同的时间段,流量分配如下:

  • 非发布时:灰度环境只有测试租户的流量,应用完成后就可以发布到灰度环境进行验证,而不用等到整个迭代都完成后再发布。这样能够有效的减少耦合和冲突,提升效率。而且是在生产环境(灰度环境和正式环境同属于生产环境)验证,质量比之前在预发布环境验证更有保障。
  • 灰度发布时:部分租户的流量切换到灰度环境,租户的选择需要认真考虑,一方面影响不能太大(比如VIP租户、正在进行活动的租户等不适合切换到灰度环境),另一方面新功能要被使用到。
  • 全量发布时:在使用低谷期,将所有的租户流量切换到灰度环境,然后升级正式环境,使用正式环境的测试租户进行验证,没有问题后再将所有租户的流量切换到正式环境(除灰度环境的测试租户外)。

消息

在灰度发布时,可能存在消息未消费完成的情况,这时候消息是旧版本应用发出的,使用新版本应用进行消费时可能会有兼容性问题,因此需要针对性的进行设计。一般来说,消息的发送难以进行开关控制,不能说升级时不能发消息,等消费完再升级,升级完成后才能发消息。在灰度环境的设计中,也存在多环境共用MQ还是不同环境独立MQ的区别。基于此,我们考虑以下场景:

  • 多环境共用MQ:此场景下,MQ需要支持根据消息所属租户,将其分流到不同环境进行消费。当发布的消息格式有变化时,根据不同的情况,可以采取以下不同处理方案:

    • 新消费者兼容老消息:新消费者识别到老消息时,针对性的进行逻辑补尝,例如通过接口获取老消息没有发送的数据后再进行处理;
    • 新发布都使用新的消息标识:保留老的消费逻辑用于消费老的消息,新发布的消息使用新标识,新的消费者只消费新的消息。这时候一般会涉及到老消息消费完成后的数据升级,可以通过工具来支持自动化处理。
  • 不同环境独立MQ:此场景下,各环境的消费者消费的是本环境的消息。当发布的消息格式有变化时,除多环境共用MQ中的处理方式外,还可以将租户流量切走后,等旧消息消费完成后,再升级环境。

通过以上场景分析,我们发现消费者不兼容老消息时,处理都比较麻烦,所以最好在设计时就考虑兼容的方案。

任务

对于SaaS系统的任务处理,很多时候是使用同一套逻辑,连接不同的租户库进行处理。在灰度发布时,需要保证新的任务处理逻辑,处理的是新的数据库版本(包括数据库结构和需要升级的数据)。否则可能会产生逻辑错误。可以考虑以下几种方案:

  • 使用消息发布任务,不同环境的租户在对应的环境消费进行处理。
  • 任务系统能够获取环境对应的租户清单,不同环境运行的任务处理自己环境的租户。
  • 任务系统不进行灰度,能够兼容新/旧两种数据库版本。

因为任务运行的时长一般较长,在升级时应该避免有正在运行的任务,否则可能出现逻辑或数据异常。所以任务系统应该有统一的开关,在升级时关闭开关,避免运行新的任务,等旧任务运行完成后,再更新系统,更新完成后再打开开关,允许新任务运行。

数据升级

SaaS系统对租户数据库进行升级时,因为每个租户都有自己的库,在灰度发布时,需要只针对灰度的租户进行升级,保持代码和数据库的版本一致。由于每次升级更新的数据库比较多,数据结构的升级往往需要很长的时间,如果处理不好,可能一晚上都执行不完,所以需要采用以下策略:

  • 数据结构升级提前执行:为了提升更新的效率,需要在更新前,在业务低谷期将数据结构先进行升级。
  • 数据结构升级兼容新旧版本代码:运行在旧版本环境的租户的库,也需要提前进行结构升级,所以数据库结构的修改不能进行删除字段、删除表、修改字段等操作。如果要删除字段,需要在全部环境完成升级后进行;如果要修改字段,需要新增一个字段,将数据迁移到新字段,全部环境完成升级后,再删除老字段。
  • 数据升级脚本需要可重复执行:数据升级可以在租户流量切换到新环境前执行,但在执行完流量切换前可能还会产生数据,所以需要在流量切换后再次执行。所以执行的脚本需要能够区分数据是否已升级,只针对未升级的数据进行升级。

在非停机更新时,不管是先升级数据还是先更新程序,都会有一段时间程序和数据库版本不一致,要么会导致程序异常,要么会导致数据错误。我们分别分析一下这两种方式:

  • 先更新程序,再升级数据:程序更新后,数据未升级前,访问的字段不存在,程序可能出现异常;如果字段未调整,只是调整了字段内容,新版程序运行进可能异常,也可能产生垃圾数据。
  • 先升级数据,再更新程序:在代码未更新完成时,可能产生旧版本的数据,代码更新完成后如果基于旧版本的数据运行,可能出现异常或数据错误。

在考虑有损的情况下,为避免大规模的异常,我们会采用“升级数据-更新程序-升级数据”的方式。尽量保证新代码基于旧数据运行时,不会产生垃圾数据,接受一定概率的系统错误。根据我们的实践来看,出这种错误的概率非常低。如果要做到程序和数据都不会出现异常,可以考虑以下方案之一:

  • 新版的程序支持旧版本的数据结构,可以根据系统可用性和数据一致性要求以及兼容成本来考虑。
  • 先上一个临时版本,旧逻辑运行后,会执行数据升级的逻辑。这样先升级数据后再更新程序,就算程序未完成更新,旧代码也会进行数据升级。

热修复及分支管理

在我们讨论的模式中,灰度环境存在的时间会比较长,实际上会造成代码有两个主干。我们分别来分析一下:

  • 非发布时:一部分在更新在灰度环境,还没有正式的租户流量,如果发现BUG,创新一个新的分支修复更新到灰度环境即可。如果发现正式环境的重要BUG,则需要从正式环境的主干创建分支进行修复,并将修复的代码合并到灰度环境的主干。

  • 灰度发布时:有部分租户流量在灰度环境,发现BUG时,创建一个热修复的分支修复即可。如果发现正式环境的重要BUG,则有两种方案:

    • 创建一个热修复的分支,在灰度环境修复,然后提前进行全量发布;这种适合灰度和正式环境都存在些BUG,而且灰度环境进行了完整验证的情况。
    • 从正式环境的主干创建分支进行修复,并将修复的代码合并到灰度环境的主干。这种适合灰度环境还未完整验证的情况。多了一次代码合并回来并验证工作。
  • 全量发布后发现的问题,就和非发布时的场景一样了。