本篇中,我们来讨论一个重要概念:“灰度发布”,及其解决方案。
“灰度发布”的基本含义如下:
灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行A/B testing,即让一部分用户继续用产品特性A,一部分用户开始用产品特性B,如果用户对B没有什么反对意见,那么逐步扩大范围,把所有用户都迁移到B上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。
来自 百度百科
可以发现,“灰度发布”主要针对的是“产品特性”。那么什么叫产品特性呢?从我们技术的角度来看,大多数人可能理解为“功能接口”,就像这样:
对于用户(这里的用户不一定只是局限于使用产品的人,也可能是使用产品的其他产品或单位,泛指一切需要使用我们产品的角色),我们根据我们的业务需求,进行不同流量的转发。
比如可能一开始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。在这段流量比例的变更期间,都不需要我重新部署项目。
这将是一个非常灵活且低成本的灰度发布解决方案。应该如何去做呢?
为了让我们能够热配置我们的灰度,那么这种配置就一定不能硬编码在我们的程序中。而且最好能以可读性较高、且较易编写的方式放在我们的配置文件/配置平台中,
毕竟我们现在想针对“代码级”的新旧版本进行灰度配置,那么可以想象,当需要灰度的代码块很多的时候,这种配置也会很多。如果不能以较好的格式存储,就会非常杂乱而不易维护。
那么应该如何设计这种配置的数据格式呢?比如可以像这样:
serviceId 服务名 | strategyType 灰度策略类型 | percent 百分比 | otherParam… 其他相关参数…… |
---|---|---|---|
loginService | PERCENT | 50 |
具体解释一下:
serviceId 服务名
唯一标识我们需要灰度的代码块。因为不同的服务可能需要不同的灰度策略(比如当A服务希望现在是70%灰度的时候,B服务希望目前只是30%的灰度),所以以服务id为唯一标识。
strategyType 灰度策略类型
在我们的示例中,只有PERCENT——按百分比进行灰度这种类型。但其实灰度策略可以有很多种,比如黑名单策略,白名单策略……
percent 百分比
显然,这个percent字段是为了PERCENT策略类型服务的。我们需要知道该服务想要的百分比值,才能为它提供百分比灰度策略服务。
otherParam… 其他相关参数……
这个与percent参数类似,只是可能是为其他的灰度策略服务的。例如如果采取一个白名单策略,也许我们就需要一个whitelist参数,看看有哪些用户可以纳入白名单。
那么这种数据结构,我们可以存在我们的数据库中,也可以写入我们的配置文件中,等等。
然后我们的程序则会通过访问这个配置,执行相应的灰度逻辑,来实现我们的灰度发布。
而通过我们人为修改这个配置数据,就可以实现灰度的热配置。
既然要针对**“代码级“**进行灰度,也就是应该会设计有如下逻辑:
//判断接下来是走旧流程还是走新流程
boolean gray = isGray(this.serviceId);
//如果判断为false,走旧流程
if(gray == false){
//doSomething...
}
//如果判断为true,走新流程
if(gray == true){
//doSomething...
}
而isGray(this.serviceId)
方法应当抽象出来,专门设计到一个模块中,允许所有业务方进行调用。
isGray(this.serviceId)
要做些什么呢?
也就是说,我们可以自设计一个通用灰度模块,在里面实现isGray方法。
而具体的调用,则交给业务方。
每个业务方是否想要灰度,想要走什么灰度策略,都完全可以由自己来控制。而具体的策略逻辑,则由我们的通用灰度模块来提供即可。
在真正接触灰度发布的实践之前,我们对灰度的理解是浅薄片面的。
而到小型系统上,或许我们也只会考虑到使用nginx等工具,利用负载均衡功能来实现灰度。
其实灰度的”水“还是挺深的,希望这样一篇文章可以开阔你的视野,打通你的思维。