CategoryResourceRepost/极客时间专栏/设计模式之美/开源与项目实战:项目实战/96 | 项目实战三:设计实现一个支持自定义规则的灰度发布组件(分析).md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

111 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<audio id="audio" title="96 | 项目实战三:设计实现一个支持自定义规则的灰度发布组件(分析)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ad/c5/addd96d5c442c65748339e71d8fd71c5.mp3"></audio>
到现在为止,我已经带你学习了接口限流框架和接口幂等框架两个实战项目。接下来,我再带你实战一个新的项目:灰度发布组件。这也是我们专栏的最后一个实战项目。还是老套路,我们把它分为分析、设计、实现三个部分、对应三节课来讲解。今天,我们对灰度发布组件进行需求分析,搞清楚这个组件应该具有哪些功能性和非功能性需求。
话不多说,让我们正式开始今天的学习吧!
## 需求场景
还记得我们之前接口限流和幂等框架的项目背景吗?我们开发了一个公共服务平台,提供公共业务功能,给其他产品的后端系统调用,避免重复开发相同的业务代码。
最初公共服务平台提供的是基于某个开源RPC框架的RPC格式的接口。在上线一段时间后我们发现这个开源RPC框架的Bug很多多次因为框架本身的Bug导致整个公共服务平台的接口不可用但又因为团队成员对框架源码不熟悉并且框架的代码质量本身也不高排查、修复起来花费了很长时间影响面非常大。所以我们评估下来觉着这个框架的可靠性不够维护成本、二次开发成本都太高最终决定替换掉它。
对于引入新的框架我们的要求是成熟、简单并且与我们现有的技术栈Spring相吻合。这样即便出了问题我们也能利用之前积累的知识、经验来快速解决。所以我们决定直接使用Spring框架来提供RESTful格式的远程接口。
把RPC接口替换成RESTful接口除了需要修改公共服务平台的代码之外调用方的接口调用代码也要做相应的修改。除此之外对于公共服务平台的代码尽管我们只是改动接口暴露方式对业务代码基本上没有改动但是我们也并不能保证就完全不出问题。所以为了保险起见我们希望灰度替换掉老的RPC服务而不是一刀切在某个时间点上让所有的调用方一下子都变成调用新的Resful接口。
**我们来看下具体如何来做。**
因为替换的过程是灰度的所以老的RPC服务不能下线同时还要部署另外一套新的RESTful服务。我们先让业务不是很重要、流量不大的某个调用方替换成调用新的RESTful接口。经过这个调用方一段时间的验证之后如果新的RESTful接口没有问题我们再逐步让其他调用方替换成调用新的RESTful接口。
但是如果万一中途出现问题我们就需要将调用方的代码回滚再重新部署这就会导致调用方一段时间内服务不可用。而且如果新的代码还包含调用方自身新的业务代码简单通过Git回滚代码重新部署会导致新的业务代码也被回滚。所以为了避免这种情况的发生我们就得手动将调用新的RESTful接口的代码删除再改回为调用老的RPC接口。
除此之外为了不影响调用方本身业务的开发进度调用方基于回滚之后的老代码来做新功能开发那替换成新的RESTful接口的那部分代码要想再重新merge回去就比较难了有可能会出现代码冲突需要再重新开发。
**怎么解决代码回滚成本比较高的问题呢?**你可以先思考一下,再看我的讲解。
在替换新的接口调用方式的时候调用方并不直接将调用RPC接口的代码逻辑删除而是新增调用RESTful接口的代码通过一个功能开关灵活切换走老的代码逻辑还是新的代码逻辑。代码示例如下所示。如果callRestfulApi为true就会走新的代码逻辑调用RESTful接口相反就会走老的代码逻辑继续调用RPC接口。
```
boolean callRestfulApi = true;
if (!callRestfulApi) {
// 老的调用RPC接口的代码逻辑
} else {
// 新的调用Resful接口的代码逻辑
}
```
不过更改callRestfulApi的值需要修改代码而修改代码就要重新部署这样的设计还是不够灵活。优化的方法我想你应该已经想到了把这个值放到配置文件或者配置中心就可以了。
为了更加保险不只是使用功能开关做新老接口调用方式的切换我们还希望调用方在替换某个接口的时候先让小部分接口请求调用新的RESTful接口剩下的大部分接口请求还是调用老的RPC接口验证没有问题之后再逐步加大调用新接口的请求比例最终将所有的接口请求都替换成调用新的接口。这就是所谓的“灰度”。
**那这个灰度功能又该如何实现呢?**同样,你还是先思考一下,再来看我的讲解。
首先我们要决定使用什么来做灰度也就是灰度的对象。我们可以针对请求携带的时间戳信息、业务ID等信息按照区间、比例或者具体的值来做灰度。我举个例子来解释一下。
假设我们要灰度的是根据用户ID查询用户信息接口。接口请求会携带用户ID信息所以我们就可以把用户ID作为灰度的对象。为了实现逐渐放量我们先配置用户ID是918、879、123具体的值的查询请求调用新接口验证没有问题之后我们再扩大范围让用户ID在1020~1120区间值之间的查询请求调用新接口。
如果验证之后还是没有问题我们再继续扩大范围让10%比例比例值的查询请求调用新接口对应用户ID跟10取模求余小于1的请求。以此类推灰度范围逐步扩大到20%、30%、50%直到100%。当灰度比例达到100%,并且运行一段时间没有问题之后,调用方就可以把老的代码逻辑删除掉了。
实际上类似的灰度需求场景还有很多。比如在金融产品的清结算系统中我们修改了清结算的算法。为了安全起见我们可以灰度替换新的算法把贷款ID作为灰度对象先对某几个贷款应用新的算法如果没有问题再继续按照区间或者比例扩大灰度范围。
除此之外,为了保证代码万无一失,提前做好预案,添加或者修改一些复杂功能、核心功能,即便不做灰度,我们也建议通过功能开关,灵活控制这些功能的上下线。在不需要重新部署和重启系统的情况,做到快速回滚或新老代码逻辑的切换。
## 需求分析
从实现的角度来讲,调用方只需要把灰度规则和功能开关,按照某种事先约定好的格式,存储到配置文件或者配置中心,在系统启动的时候,从中读取配置到内存中,之后,看灰度对象是否落在灰度范围内,以此来判定是否执行新的代码逻辑。但为了避免每个调用方都重复开发,我们把功能开关和灰度相关的代码,抽象封装为一个灰度组件,提供给各个调用方来复用。
这里需要强调一点,我们这里的灰度,是代码级别的灰度,目的是保证项目质量,规避重大代码修改带来的不确定性风险。实际上,我们平时经常讲的灰度,一般都是产品层面或者系统层面的灰度。
所谓产品层面有点类似A/B Testing让不同的用户看到不同的功能对比两组用户的使用体验收集数据改进产品。所谓系统层面的灰度往往不在代码层面上实现一般是通过配置负载均衡或者API-Gateway来实现分配流量到不同版本的系统上。系统层面的灰度也是为了平滑上线功能但比起我们讲到的代码层面的灰度就没有那么细粒度了开发和运维成本也相对要高些。
**现在,我们就来具体看下,灰度组件都有哪些功能性需求。**
我们还是从使用的角度来分析。组件使用者需要设置一个key值来唯一标识要灰度的功能然后根据自己业务数据的特点选择一个灰度对象比如用户ID在配置文件或者配置中心中配置这个key对应的灰度规则和功能开关。配置的格式类似下面这个样子
```
features:
- key: call_newapi_getUserById
enabled: true // enabled为true时rule才生效
rule: {893,342,1020-1120,%30} // 按照用户ID来做灰度
- key: call_newapi_registerUser
enabled: true
rule: {1391198723, %10} //按照手机号来做灰度
- key: newalgo_loan
enabled: true
rule: {0-1000} //按照贷款(loan)的金额来做灰度
```
灰度组件在业务系统启动的时候,会将这个灰度配置,按照事先定义的语法,解析并加载到内存对象中,业务系统直接使用组件提供的灰度判定接口,给业务系统使用,来判定某个灰度对象是否灰度执行新的代码逻辑。配置的加载解析、灰度判定逻辑这部分代码,都不需要业务系统来从零开发。
```
public interface DarkFeature {
boolean enabled();
boolean dark(String darkTarget); //darkTarget是灰度对象比如前面提到的用户ID、手机号码、金额等
}
```
所以总结一下的话灰度组件跟限流框架很类似它也主要包含两部分功能灰度规则配置解析和提供编程接口DarkFeature判定是否灰度。
跟限流框架类似,除了功能性需求,我们还要分析非功能性需求。不过,因为前面已经有了限流框架的非功能性需求的讲解,对于灰度组件的非功能性需求,我就留给你自己来分析。在下一节课中,我会再给出我的分析思路,到时候,你可以对比一下。
## 重点回顾
好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。
灰度发布可以分为三个不同层面的灰度:产品层面的灰度、系统层面的灰度和代码层面的灰度。我们今天重点讲解代码层面的灰度,通过编程来控制是否执行新的代码逻辑,以及灰度执行新的代码逻辑。
代码层面的灰度,主要解决代码质量问题,通过逐渐放量灰度执行,来降低重大代码改动带来的风险。在出现问题之后,在不需要修改代码、重新部署、重启系统的情况下,实现快速地回滚。相对于系统层面的灰度,它可以做得更加细粒度,更加灵活、简单、好维护,但也存在着代码侵入的问题,灰度代码跟业务代码耦合在一起。
灰度组件跟之前讲过的限流框架很相似,主要包含配置的解析加载和灰度判定逻辑。除此之外,对于非功能性需求,我们留在下一节课中讲解。
## 课堂讨论
参照限流框架的非功能性需求,分析一下灰度组件的非功能性需求。
欢迎留言和我分享你的想法。如果有收获,也欢迎你把这篇文章分享给你的朋友。