在上一篇文章《记一次反射应用实践》中,灰度判断逻辑仅仅使用随机数进行模拟。由于示例Demo使用了Dubbo+Nacos技术,而Nacos也是可以作为配置中心的。所以我就现学现卖,把Nacos集成进来做下尝试,让我们的Demo功能更加丰富。
灰度设计
首先回到我们的问题本身。对于大型的系统,新老系统交替意味着引入变更,变更就有带来风险的可能,为了让我们的新老系统交替时风险可控,需要让新系统支持灰度发布,逐步放量。
批量灰度就是常用的一种手段,比如在以用户维度的业务系统中,可以对用户ID对100取模,就可以把用户分为100份。我们可以先对10%的用户开放新功能,灰度一段时间后,若系统运行正常,可以指逐步开放到20%、50%直到100%;并且灰度应该支持动态修改,一来可以在不走线上发布的情况下动态实现灰度批量的扩大或缩小,二来可以在发现新系统bug时立刻关闭灰度开关,及时止血。另外,新系统需要有联调、测试的阶段,尤其在线上系统需要挑选个别测试用户试用,所以需要通过白名单功能开放特殊通道;同理,若个别用户由于某些原因不能使用新功能,可以通过黑名单功能对这些用户进行屏蔽。
总结一下就是,批量灰度可以通过灰度批量百分比、白名单、黑名单来实现,三者之间的优先级为白名单>黑名单>灰度百分比,下面的代码体现了这一逻辑。
1/** 2 * 百分比灰度配置 3 *
4 * 优先级:白名单 > 黑名单 > 灰度规则 5 * 6 * @author raysonxin 7 * @since 2021/1/23 8 */
9@Data
10@NoArgsConstructor
11@AllArgsConstructor
12public class PercentGrayModel {
13
14 /**15 * 百分比,整数。取值范围:[0,100],0代表关闭,100代表全量16 */
17 private Integer percent;
18
19 /**20 * 白名单21 */
22 private List whiteList;2324 /**25 * 黑名单26 */27 private List blackList;2829 /**30 * 检查目标是否命中灰度31 *32 * @param target 目标33 * @return true-命中,false-未命中34 */35 public boolean hitGray(Long target) {36 // 数据异常37 if (null == target || target 0L) {38 return false;39 }4041 // 命中白名单,返回true42 if (!ObjectUtils.isEmpty(whiteList) && whiteList.contains(target)) {43 return true;44 }4546 // 命中黑名单,返回false47 if (!ObjectUtils.isEmpty(blackList) && blackList.contains(target)) {48 return false;49 }5051 // 无百分比灰度规则52 if (percent == null || percent 0) {53 return false;54 }5556 // 大于等于100,相当于全量57 if (percent >= 100) {58 return true;59 }6061 // 按照灰度百分比计算是否命中62 return target % 100 63 }64}
Nacos配置管理
新建配置
为了方便,我直接使用了Nacos的Docker版本,安装Docker CE后,大家可以直接到这个地址下载并运行。浏览器输入http://localhost:8848/nacos,默认用户名和密码都是nacos。点击“配置管理-配置列表-➕”新增配置,然后发布即可(如下图):
Data ID:com.rsxtech.demo.consumer:gray-rule.json
Group:DEFAULT_GROUP
配置内容:如下图Json结构。
![10b03d19376cc23e6a21ee9ba1964b65.png](https://img-blog.csdnimg.cn/img_convert/10b03d19376cc23e6a21ee9ba1964b65.png)
代码集成
Nacos提供了Java SDK,同时提供了Spring、Spring Boot、Spring Cloud等方式的支持。本来我想使用Spring Boot版本的API,但是一直有问题未解决,最好只好使用Java SDK了。Java SDK集成也比较简单,可以参考这里查看SDK的使用姿势。
我们主要使用Nacos配置管理的两个API:
获取配置:在应用程序启动时,主动从Nacos获取配置内容;
监听配置修改:当Nacos配置修改时,可实时接收最新配置并生效。
首先在application.properties中增加配置项nacos.server-addr
,然后新增配置管理类TransferGray,代码如下所示:
1/** 2 * 灰度配置获取 3 * 4 * @author raysonxin 5 * @since 2021/1/23 6 */
7@Component
8@Slf4j
9public class TransferGray {
10
11 @Value("${nacos.server-addr}")
12 private String nacosServer;
13
14 private static final String dataId = "com.rsxtech.demo.consumer:gray-rule.json";
15 private static final String group = "DEFAULT_GROUP";
16
17 /**18 * 配置内容19 */
20 public static PercentGrayModel grayConfig;
21
22 @PostConstruct
23 public void init() throws NacosException, JsonProcessingException {
24 Properties properties = new Properties();
25 properties.put("serverAddr", nacosServer);
26
27 ConfigService configService = NacosFactory.createConfigService(properties);
28
29 // 启动后,主动拉取配置。
30 String content = configService.getConfig(dataId, group, 5000);
31 log.info("TransferGrayConfig get config={}", content);
32 if (ObjectUtils.isEmpty(content)) {
33 return;
34 }
35
36 ObjectMapper objectMapper = new ObjectMapper();
37 grayConfig = objectMapper.readValue(content, PercentGrayModel.class);
38
39 // 当Nacos中配置变更时,可接收新的内容。
40 configService.addListener(dataId, group, new Listener() {
41 @Override
42 public Executor getExecutor() {
43 return null;
44 }
45
46 @Override
47 public void receiveConfigInfo(String s) {
48 log.info("TransferGrayConfig get config={}", s);
49 try {
50 ObjectMapper objectMapper = new ObjectMapper();
51 grayConfig = objectMapper.readValue(content, PercentGrayModel.class);
52 } catch (Exception ex) {
53 log.error("TransferGrayConfig parse config error", ex);
54 }
55 }
56 });
57 }
58}
配置获取
在代码中打个断点,启动应用程序,会看到我们获取到了配置内容。
![eb8365eb5f9f007cd6046224b168b7b8.png](https://img-blog.csdnimg.cn/img_convert/eb8365eb5f9f007cd6046224b168b7b8.png)
配置发布与监听
在receiveConfigInfo方法中打个断点,然后打开刚才新建的配置项,进入编辑页;修改一下percent值,点击发布,即可发现我们的断点命中了并获取到了最新的配置内容。如下两图所示:
![2ded7778017f8c95e6da0a853623f1fc.png](https://img-blog.csdnimg.cn/img_convert/2ded7778017f8c95e6da0a853623f1fc.png)
![e87a843f3a0f94a386046c4a55ad4c06.png](https://img-blog.csdnimg.cn/img_convert/e87a843f3a0f94a386046c4a55ad4c06.png)
灰度接入
现在就可以修改之前的needTransfer方法了。这里我在原来的接口参数中增加了userId,便于按照灰度策略做判断。
1@Data
2@ToString
3public class HelloRequestCmd implements Serializable {
4
5 private static final long serialVersionID = 1L;
6
7 /** 8 * 用户id 9 * */
10 private Long userId;
11
12 /**13 * 姓名14 */
15 private String name;
16
17 /**18 * 问候语19 */
20 private String greeting;
21}
为了简单起见,接口转发是针对当前方法参数做了转换与判断,如下所示:
1 /** 2 * 灰度判断逻辑 3 * 4 * @return true-命中灰度,执行转发;false-不转发 5 */
6 private boolean needTransfer(Object[] args) {
7 // 这里先简单处理,实际情况下需要根据参数特点,编写一个方法解析灰度参数。
8 // 比如我们的老系统,所有接口都包含一个用户请求对象,就可以找个找个参数,然后执行灰度判断。
9 HelloRequestCmd cmd = (HelloRequestCmd) args[0];
10 PercentGrayModel percentGrayModel = TransferGray.grayConfig;
11 return percentGrayModel.hitGray(cmd.getUserId());
12 }
实际情况下,判断接口是否转发需要根据每个方法的参数特点编写合适的解析方式,因为需要转发的方法参数是已知的,所以我们是有办法拿到的。往往我们系统中,为了统一封装请求参数,会把一些公共的参数放到参数基类或者HTTP请求头中,这会大大增加我们处理的便利。
测试效果
现在就可以做下测试了,这里我使用了IDEA自带的HTTP测试工具。对应的白名单、黑名单、灰度测试结果截图如下:
白名单测试
![f7478f83e631171076b108ba28a75ef0.png](https://img-blog.csdnimg.cn/img_convert/f7478f83e631171076b108ba28a75ef0.png)
黑名单测试
![d8aa7eed36cdd218a39cfec5e6eb7818.png](https://img-blog.csdnimg.cn/img_convert/d8aa7eed36cdd218a39cfec5e6eb7818.png)
正常灰度测试
![47727941632bd1b73e9a3714f22df4fa.png](https://img-blog.csdnimg.cn/img_convert/47727941632bd1b73e9a3714f22df4fa.png)
![42d2834887036c30321f4216ae5a7f3c.png](https://img-blog.csdnimg.cn/img_convert/42d2834887036c30321f4216ae5a7f3c.png)