本文,我们来讨论一个企业里开发中的一个重要概念:灰度发布,及其解决方案。
内容主要分为如下几个方面
- 基本概念
- 理论与现实的冲突与思考
- 实战解决方案
基本概念
灰度发布的基本含义如下:
灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。
可以发现,“灰度发布”主要针对的是“产品特性”。那么什么叫产品特性呢?从我们技术的角度来看,大多数人可能理解为“功能接口”,就像这样:
![image-20211017142330674](/2021/10/17/shi-me-shi-hui-du-fa-bu-jin-si-que-fa-bu/image-20211017142330674.png)
对于用户(这里的用户不一定只是局限于使用产品的人,也可能是使用产品的其他产品或单位,泛指一切需要使用我们产品的角色),我们根据我们的业务需求,进行不同流量的转发。
比如可能一开始30%的请求流量打给新版功能,70%打给旧版功能。在用户反馈良好的情况下,慢慢提升新版功能的接入流量,直到彻底完成新旧更替。
理论与现实的思考冲突
可能会有同学觉得,灰度发布不是很简单直接吗,有什么好冲突的?
冲突点主要来源以下几个方面:
怎么设计流量的分配
之前的例子中,说的是从30-70的流量比例开始,慢慢调大新系统的接入流量比例,直到彻底完成新旧更替。
如果一切都很符合理想,没有什么外部约束的话,最好的方式一定是从0-100的比例开始,慢慢调整到100-0,是最安全最有稳定性的。
但现实总是有外部约束,例如:
- 来自效率的约束
- 产品经理:这个新功能三天内必须完全上线!
- 来自成本的约束
- 谁负责慢慢调节流量?谁来监控?或者交给算法来做,谁来写这个算法?涉及到的成本会很多。
那么如何权衡效率、成本与稳定性呢,就需要我们有充分的运维经验了。而这方面其实与开发是分不开的,我们开发者对于代码和业务复杂性的评估,将是我们去为灰度发布keep balance的重要依据。
怎么控制灰度的粒度
在这里我们提出两个问题
“系统级”还是“接口级”?
在V1版本的产品和V2版本的产品之间,进行灰度是十分合理的。但有时候并非只是这么简单,例如:
V1和V2之间有A、B、C三个新功能接口的迭代。其中A功能比较简单,也不是核心功能,基本不太需要灰度就可以发布;而C功能是非常核心且复杂的重点功能,一旦出bug可能导致严重的线上问题,需要谨慎发布。
那么如果我们只针对V1和V2进行灰度,就会发现整个版本迭代的效率是以A、B、C三个功能中最慢的那个为准的。我们是否有可能针对A、B、C分别进行灰度呢?这样就可以迅速上线A这样比较简单的新功能,而对于复杂的C功能则延缓彻底上线的速度。
那这样的话,是不是就意味着我们需要V1,V2,V3,V4四个版本?其中V2、3、4分别对A、B、C进行灰度。这样未免成本太高了。
思考一下,应该如何去做?
“接口级”还是“代码级”
如果刚才提到的C接口内部代码逻辑很复杂,但旧版本的C接口和新版本的C接口代码复用率很高。我们有必要开发出两种版本的C接口,然后再进行灰度吗?是不是可以只针对那一部分不一样的代码进行灰度呢?
如果想只针对一部分代码进行灰度,既然不再是针对接口了,那我们该如何把控流量呢?
停下来,再思考一下。在后面的解决方案中,我们再继续探讨。
实战解决方案
现在有了这样一个需求:
我们希望能够灵活地解决灰度问题,针对代码级的变更进行灰度发布。
与此同时,我们希望这种变更是可控的,是可以热配置的。例如现在某个功能的流量中,30%走的旧代码,70%走的新代码。我希望不需要通过手动重新修改并部署项目,就可以非常方便地将流量比例转为50-50,直到最后转为100-0。在这段流量比例的变更期间,都不需要我重新部署项目。
这将是一个非常灵活且低成本的灰度发布解决方案。应该如何去做呢?
1.准备外部资源,实现热配置
为了让我们能够热配置我们的灰度,那么这种配置就一定不能硬编码在我们的程序中。而且最好能以可读性较高、且较易编写的方式放在我们的配置文件/配置平台中。
毕竟我们现在想针对“代码级”的新旧版本进行灰度配置,那么可以想象,当需要灰度的代码块很多的时候,这种配置也会很多。如果不能以较好的格式存储,就会非常杂乱而不易维护。
那么应该如何设计这种配置的数据格式呢?比如可以像这样:
![image-20211017142314273](/2021/10/17/shi-me-shi-hui-du-fa-bu-jin-si-que-fa-bu/image-20211017142314273.png)
具体解释一下
- serviceId 服务名
唯一标识我们需要灰度的代码块。因为不同的服务可能需要不同的灰度策略(比如当A服务希望现 在是70%灰度的时候,B服务希望目前只是30%的灰度),所以以服务id为唯一标识。
- strategyType 灰度策略类型
在我们的示例中,只有PERCENT——按百分比进行灰度这种类型。但其实灰度策略可以有很多 种,比如黑名单策略,白名单策略……
- percent 百分比
显然,这个percent字段是为了PERCENT策略类型服的。我们需要知道该服务想要的百分比值, 才能为它提供百分比灰度策略服务。
- otherParam… 其他相关参数……
这个与percent参数类似,只是可能是为其他的灰度策略服务的。例如如果采取一个白名单策略, 也许我们就需要一个whitelist参数,看看有哪些用户可以纳入白名单。
那么这种数据结构,我们可以存在我们的数据库中,也可以写入我们的配置文件中,等等。
然后我们的程序则会通过访问这个配置,执行相应的灰度逻辑,来实现我们的灰度发布。而通过我们人为修改这个配置数据,就可以实现灰度的热配置。
2.控制权交由业务,实现粒度自由
既然要针对“代码级”进行灰度,也就是应该会设计有如下逻辑:
//判断接下来是走旧流程还是走新流程
boolean gray = isGray ( this.serviceId );
//如果判断为false,走旧流程
if ( gray == false ) {
//doSomething...
}
//如果判断为true,走新流程
if ( gray == true ) {
//doSomething...
}
而isGray(this.serviceId)
方法应当抽象出来,专门设计到一个模块中,允许所有业务方进行调用。
isGray(this.serviceId)
要做些什么呢?
- 获取到调用方的serviceId(通过方法传参获取到了)
- 根据serviceId,到配置文件/平台中获取相关数据,包括strategyType,percent等等
- 根据strategyType,并配合其他参数,执行相应的灰度逻辑。
- 返回true或false,告知业务方本次调用应当走入新旧哪个流程
也就是说,我们可以自设计一个通用灰度模块,在里面实现isGray方法。
而具体的调用,则交给业务方。
每个业务方是否想要灰度,想要走什么灰度策略,都完全可以由自己来控制。而具体的策略逻辑,则由 我们的通用灰度模块来提供即可。
总结
在真正接触灰度发布的实践之前,我们对灰度的理解是浅薄片面的。
而到小型系统上,或许我们也只会考虑到使用nginx等工具,利用负载均衡功能来实现灰度。
其实灰度的”水“还是挺深的,希望这样一篇文章可以开阔视野,打通思维。
想了解更多大厂就业相关,学习更多企业实用技术,可以参加下面课程哦!
![psc](/2021/10/17/shi-me-shi-hui-du-fa-bu-jin-si-que-fa-bu/psc.jpg)