设计模式-设计实现一个支持自定义规则的灰度发布组件

灰度组件功能需求整理

灰度规则的格式和存储方式

我们希望支持不同格式(JSON、YAML、XML 等)、不同存储方式(本地配置文件、Redis、Zookeeper、或者自研配置中心等)的灰度规则配置方式。实际上,这一点跟之前的限流框架中限流规则的格式和存储方式完全一致,代码实现也是相同的

灰度规则的语法格式

我们支持三种灰度规则语法格式:具体值(比如 893)、区间值(比如 1020-1120)、比例值(比如 %30)。除此之外,对于更加复杂的灰度规则,比如只对 30 天内购买过某某商品并且退货次数少于 10 次的用户进行灰度,我们通过编程的方式来实现。

灰度规则的内存组织方式

类似于限流框架中的限流规则,我们需要把灰度规则组织成支持快速查找的数据结构,能够快速判定某个灰度对象(darkTarget,比如用户 ID),是否落在灰度规则设定的范围内。

灰度规则热更新

修改了灰度规则之后,我们希望不重新部署和重启系统,新的灰度规则就能生效,所以,我们需要支持灰度规则热更新。

在 V1 版本中,对于第一点灰度规则的格式和存储方式,我们只支持 YAML 格式本地文件的配置存储方式。对于剩下的三点,我们都要进行实现。考虑到 V1 版本要实现的内容比较多,我们分两步来实现代码,第一步先将大的流程、框架搭建好,第二步再进一步添加、丰富、优化功能。

实现灰度组件基本功能

在第一步中,我们先实现基于 YAML 格式的本地文件的灰度规则配置方式,以及灰度规则热更新,并且只支持三种基本的灰度规则语法格式。基于编程实现灰度规则的方式,我们留在第二步实现。

我们先把这个基本功能的开发需求,用代码实现出来。它的目录结构及其 Demo 示例如下所示。代码非常简单,只包含 4 个类。接下来,我们针对每个类再详细讲解一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 代码目录结构
com.monochrome.darklaunch
--DarkLaunch(框架的最顶层入口类)
--DarkFeature(每个feature的灰度规则)
com.monochrome.darklaunch.rule
--DarkRule(灰度规则)
--DarkRuleConfig(用来映射配置到内存中)
com.monochrome.darklaunch.rule.datasource
--DarkRuleConfigSource(灰度规则源)
--FileDarkRuleConfigSource(文件型灰度规则源)
com.monochrome.darklaunch.rule.parser
--DarkRuleConfigParser(灰度规则转换器)
--JsonDarkRuleConfigParser(Json格式灰度规则转换器)
--YamlDarkRuleConfigParser(Yaml格式灰度规则转换器)
1
2
3
4
5
6
7
8
// Demo示例
public class DarkDemo {
public static void main(String[] args) {
DarkLaunch darkLaunch = new DarkLaunch();
DarkFeature darkFeature = darkLaunch.getDarkFeature("call_newapi_getUserById");
System.out.println(darkFeature.enabled());
System.out.println(darkFeature.dark(893));
}
1
2
3
4
5
6
7
8
9
10
11
#灰度规则配置(dark-rule.yaml)放置在classpath路径下
features:
- key: call_newapi_getUserById
enabled: true
rule: {893,342,1020-1120,%30}
- key: call_newapi_registerUser
enabled: true
rule: {1391198723, %10}
- key: newalgo_loan
enabled: true
rule: {0-1000}

从 Demo 代码中,我们可以看出,对于业务系统来说,灰度组件的两个直接使用的类是 DarkLaunch 类和 DarkFeature 类。

我们先来看 DarkLaunch 类。这个类是灰度组件的最顶层入口类。它用来组装其他类对象,串联整个操作流程,提供外部调用的接口。

DarkLaunch 类先读取灰度规则配置文件,映射为内存中的 Java 对象(DarkRuleConfig),然后再将这个中间结构,构建成一个支持快速查询的数据结构(DarkRule)。除此之外,它还负责定期更新灰度规则,也就是前面提到的灰度规则热更新。

为了避免更新规则和查询规则的并发执行冲突,在更新灰度规则的时候,我们并非直接操作老的 DarkRule,而是先创建一个新的 DarkRule,然后等新的 DarkRule 都构建好之后,再“瞬间”赋值给老的 DarkRule。你可以结合着下面的代码一块看下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.monochrome.darklaunch;

import com.monochrome.darklaunch.rule.DarkRule;
import com.monochrome.darklaunch.rule.DarkRuleConfig;
import com.monochrome.darklaunch.rule.datasource.DarkRuleConfigSource;
import com.monochrome.darklaunch.rule.datasource.FileDarkRuleConfigSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.Yaml;

import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
* @author monochrome
* @date 2022/10/27
*/

public class DarkLaunch {
private static final Logger log = LoggerFactory.getLogger(DarkLaunch.class);
private static final int DEFAULT_RULE_UPDATE_TIME_INTERVAL = 60; // in seconds
private DarkRuleConfigSource ruleConfigSource;
private DarkRule rule;
private ScheduledExecutorService executor;

public DarkLaunch(int ruleUpdateTimeInterval) {
ruleConfigSource = new FileDarkRuleConfigSource();
DarkRuleConfig ruleConfig = ruleConfigSource.load();
this.rule = new DarkRule(ruleConfig);
this.executor = Executors.newSingleThreadScheduledExecutor();
this.executor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
DarkRuleConfig newRuleConfig = ruleConfigSource.load();
DarkRule newDarkRule = new DarkRule(newRuleConfig);
rule = newDarkRule;
}
}, ruleUpdateTimeInterval, ruleUpdateTimeInterval, TimeUnit.SECONDS);
}

public DarkLaunch() {
this(DEFAULT_RULE_UPDATE_TIME_INTERVAL);
}

public DarkFeature getDarkFeature(String featureKey) {
DarkFeature darkFeature = this.rule.getDarkFeature(featureKey);
return darkFeature;
}
}

我们再来看下 DarkRuleConfig 类。这个类功能非常简单,只是用来将灰度规则映射到内存中。具体的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.monochrome.darklaunch.rule;

import java.util.List;

/**
* @author monochrome
* @date 2022/10/27
*/
public class DarkRuleConfig {

private List<DarkFeatureConfig> features;

public List<DarkFeatureConfig> getFeatures() {
return features;
}

public void setFeatures(List<DarkFeatureConfig> features) {
this.features = features;
}

public static class DarkFeatureConfig {

private String key;
private boolean enabled;
private String rule;

public String getKey() {
return key;
}

public void setKey(String key) {
this.key = key;
}

public boolean isEnabled() {
return enabled;
}

public void setEnabled(boolean enabled) {
this.enabled = enabled;
}

public String getRule() {
return rule;
}

public void setRule(String rule) {
this.rule = rule;
}
}
}

从代码中,我们可以看出来,DarkRuleConfig 类嵌套了一个内部类 DarkFeatureConfig。这两个类跟配置文件的两层嵌套结构完全对应。我把对应关系标注在了下面的示例中,你可以对照着代码看下。

1
2
3
4
5
6
7
8
9
10
features:
- key: call_newapi_getUserById
enabled: true
rule: "{893,342,1020-1120,%30}"
- key: call_newapi_registerUser
enabled: true
rule: "{1391198723, %10}"
- key: newalgo_loan
enabled: true
rule: "{0-1000}"

我们再来看下 DarkRule。DarkRule 包含所有要灰度的业务功能的灰度规则。它用来支持根据业务功能标识(feature key),快速查询灰度规则(DarkFeature)。代码也比较简单,具体如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.monochrome.darklaunch.rule;

import com.monochrome.darklaunch.DarkFeature;

import javax.validation.constraints.NotNull;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* @author monochrome
* @date 2022/10/27
*/
public class DarkRule {
private Map<String, DarkFeature> darkFeatures = new HashMap<>();

public DarkRule(@NotNull DarkRuleConfig darkRuleConfig) {
List<DarkRuleConfig.DarkFeatureConfig> darkFeatureConfigs = darkRuleConfig.getFeatures();
for (DarkRuleConfig.DarkFeatureConfig darkFeatureConfig : darkFeatureConfigs) {
darkFeatures.put(darkFeatureConfig.getKey(), new DarkFeature(darkFeatureConfig));
}
}

public DarkFeature getDarkFeature(String featureKey) {
return darkFeatures.get(featureKey);
}
}

我们最后来看下 DarkFeature 类。DarkFeature 类表示每个要灰度的业务功能的灰度规则。DarkFeature 将配置文件中灰度规则,解析成一定的结构(比如 RangeSet),方便快速判定某个灰度对象是否落在灰度规则范围内。具体的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
package com.monochrome.darklaunch;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Range;
import com.google.common.collect.RangeSet;
import com.google.common.collect.TreeRangeSet;
import com.monochrome.darklaunch.rule.DarkRuleConfig;
import org.apache.commons.lang3.StringUtils;

/**
* @author monochrome
* @date 2022/10/27
*/
public class DarkFeature {

private String key;
private boolean enable;
private int percentage;
private RangeSet<Long> rangeSet = TreeRangeSet.create();

public DarkFeature(DarkRuleConfig.DarkFeatureConfig darkFeatureConfig) {
this.key = darkFeatureConfig.getKey();
this.enable = darkFeatureConfig.isEnabled();
String darkRule = darkFeatureConfig.getRule().trim();
parseDarkRule(darkRule);
}

@VisibleForTesting
protected void parseDarkRule(String darkRule) {
if (!darkRule.startsWith("{") && !darkRule.endsWith("}")) {
throw new RuntimeException("Failed to parse dark rule:" + darkRule);
}
String[] rules = darkRule.substring(1, darkRule.length() - 1).split(",");
this.rangeSet.clear();
this.percentage = 0;
for (String rule : rules) {
rule = rule.trim();
if (StringUtils.isEmpty(rule)) {
continue;
}
if (rule.startsWith("%")) {
int newPercentage = Integer.parseInt(rule.substring(1));
if (newPercentage > this.percentage) {
this.percentage = newPercentage;
}
} else if (rule.contains("-")) {
String[] parts = rule.split("-");
if (parts.length != 2) {
throw new RuntimeException("Failed to parse dark rule:" + darkRule);
}
long start = Long.parseLong(parts[0]);
long end = Long.parseLong(parts[1]);
if (start > end) {
throw new RuntimeException("Failed to parse dark rule:" + darkRule);
}
this.rangeSet.add(Range.closed(start, end));
} else {
long val = Long.parseLong(rule);
this.rangeSet.add(Range.closed(val, val));
}
}
}

public boolean isEnable() {
return enable;
}

public boolean dark(String darkTarget) {
long target = Long.parseLong(darkTarget);
return this.dark(target);
}

public boolean dark(long darkTarget) {
boolean selected = this.rangeSet.contains(darkTarget);
if (selected) {
return true;
}

long reminder = darkTarget % 100;
if (reminder >= 0 && reminder <= this.percentage) {
return true;
}

return false;
}
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.monochrome.darklaunch;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;

/**
* @author monochrome
* @date 2022/10/28
*/
class DarkLaunchTest {

DarkLaunch darkLaunch;

@BeforeEach
void setUp() {
darkLaunch = new DarkLaunch();
}

@Test
void getDarkFeature() {
DarkFeature darkFeature = darkLaunch.getDarkFeature("call_newapi_getUserById");
boolean dark = darkFeature.dark(893);
assertThat(dark).isTrue();
boolean dark1 = darkFeature.dark(894);
assertThat(dark1).isFalse();
boolean dark2 = darkFeature.dark(1021);
assertThat(dark2).isTrue();

}
}

添加、优化灰度组件功能

在第一步中,我们完成了灰度组件的基本功能。在第二步中,我们再实现基于编程的灰度规则配置方式,用来支持更加复杂、更加灵活的灰度规则。

我们需要对于第一步实现的代码,进行一些改造。改造之后的代码目录结构如下所示。其中,DarkFeature、DarkRuleConfig 的基本代码不变,新增了 IDarkFeature 接口,DarkLaunch、DarkRule 的代码有所改动,用来支持编程实现灰度规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// MVP版代码目录结构
com.monochrome.darklaunch
--DarkLaunch(框架的最顶层入口类)
--DarkFeature(每个feature的灰度规则)
com.monochrome.darklaunch.rule
--DarkRule(灰度规则)
--DarkRuleConfig(用来映射配置到内存中)
com.monochrome.darklaunch.rule.datasource
--DarkRuleConfigSource(灰度规则源)
--FileDarkRuleConfigSource(文件型灰度规则源)
com.monochrome.darklaunch.rule.parser
--DarkRuleConfigParser(灰度规则转换器)
--JsonDarkRuleConfigParser(Json格式灰度规则转换器)
--YamlDarkRuleConfigParser(Yaml格式灰度规则转换器)
// 第二版代码目录结构
com.monochrome.darklaunch
--DarkLaunch(框架的最顶层入口类,代码有改动)
--IDarkFeature(feature抽象接口)
--DarkFeature(实现IDarkFeature接口,基于配置文件的灰度规则,代码不变)
com.monochrome.darklaunch.rule
--DarkRule(灰度规则,代码有改动)
--DarkRuleConfig(用来映射配置到内存中)
com.monochrome.darklaunch.rule.datasource
--DarkRuleConfigSource(灰度规则源)
--FileDarkRuleConfigSource(文件型灰度规则源)
com.monochrome.darklaunch.rule.parser
--DarkRuleConfigParser(灰度规则转换器)
--JsonDarkRuleConfigParser(Json格式灰度规则转换器)
--YamlDarkRuleConfigParser(Yaml格式灰度规则转换器)

我们先来看下 IDarkFeature 接口,它用来抽象从配置文件中得到的灰度规则,以及编程实现的灰度规则。具体代码如下所示:

1
2
3
4
5
6
7
package com.monochrome.darklaunch;

public interface IDarkFeature {
boolean enabled();
boolean dark(long darkTarget);
boolean dark(String darkTarget);
}

基于这个抽象接口,业务系统可以自己编程实现复杂的灰度规则,然后添加到 DarkRule 中。为了避免配置文件中的灰度规则热更新时,覆盖掉编程实现的灰度规则,在 DarkRule 中,我们对从配置文件中加载的灰度规则和编程实现的灰度规则分开存储。按照这个设计思路,我们对 DarkRule 类进行重构。重构之后的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.monochrome.darklaunch.rule;

import com.monochrome.darklaunch.DarkFeature;
import com.monochrome.darklaunch.IDarkFeature;

import javax.validation.constraints.NotNull;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* @author monochrome
* @date 2022/10/27
*/
public class DarkRule {
// 从配置文件加载的灰度规则
private Map<String, IDarkFeature> darkFeatures = new HashMap<>();
// 编程实现的灰度规则
private ConcurrentHashMap<String, IDarkFeature> programmedDarkFeatures = new ConcurrentHashMap<>();

public DarkRule(@NotNull DarkRuleConfig darkRuleConfig) {
List<DarkRuleConfig.DarkFeatureConfig> darkFeatureConfigs = darkRuleConfig.getFeatures();
for (DarkRuleConfig.DarkFeatureConfig darkFeatureConfig : darkFeatureConfigs) {
darkFeatures.put(darkFeatureConfig.getKey(), new DarkFeature(darkFeatureConfig));
}
}

public void addProgrammedDarkFeature(String featureKey, IDarkFeature darkFeature) {
programmedDarkFeatures.put(featureKey, darkFeature);
}

public IDarkFeature getDarkFeature(String featureKey) {
IDarkFeature darkFeature = programmedDarkFeatures.get(featureKey);
return darkFeature == null ? darkFeatures.get(featureKey) : darkFeature;
}
}

因为 DarkRule 代码有所修改,对应地,DarkLaunch 的代码也需要做少许改动,主要有一处修改和一处新增代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.monochrome.darklaunch;

import com.monochrome.darklaunch.rule.DarkRule;
import com.monochrome.darklaunch.rule.DarkRuleConfig;
import com.monochrome.darklaunch.rule.datasource.DarkRuleConfigSource;
import com.monochrome.darklaunch.rule.datasource.FileDarkRuleConfigSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.Yaml;

import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
* @author monochrome
* @date 2022/10/27
*/

public class DarkLaunch {
private static final Logger log = LoggerFactory.getLogger(DarkLaunch.class);
private static final int DEFAULT_RULE_UPDATE_TIME_INTERVAL = 60; // in seconds
private DarkRuleConfigSource ruleConfigSource;
private DarkRule rule;
private ScheduledExecutorService executor;

public DarkLaunch(int ruleUpdateTimeInterval) {
ruleConfigSource = new FileDarkRuleConfigSource();
DarkRuleConfig ruleConfig = ruleConfigSource.load();
this.rule = new DarkRule(ruleConfig);
this.executor = Executors.newSingleThreadScheduledExecutor();
this.executor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
DarkRuleConfig newRuleConfig = ruleConfigSource.load();
DarkRule newDarkRule = new DarkRule(newRuleConfig);
rule = newDarkRule;
}
}, ruleUpdateTimeInterval, ruleUpdateTimeInterval, TimeUnit.SECONDS);
}

public DarkLaunch() {
this(DEFAULT_RULE_UPDATE_TIME_INTERVAL);
}

public void addProgrammedDarkFeature(String featureKey, IDarkFeature darkFeature) {
this.rule.addProgrammedDarkFeature(featureKey, darkFeature);
}

public IDarkFeature getDarkFeature(String featureKey) {
IDarkFeature darkFeature = this.rule.getDarkFeature(featureKey);
return darkFeature;
}
}

编程实现灰度规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.monochrome.darklaunch;

/**
* @author monochrome
* @date 2022/10/28
*/
public class UserPromotionDarkFeature implements IDarkFeature {
@Override
public boolean enabled() {
return true;
}

@Override
public boolean dark(long darkTarget) {
return false;
}

@Override
public boolean dark(String darkTarget) {
return false;
}
}

测试

1
2
3
4
5
6
7
8
@Test
void addProgrammedDarkFeature() {
String userPromotionKey = "user_promotion";
darkLaunch.addProgrammedDarkFeature(userPromotionKey, new UserPromotionDarkFeature());
IDarkFeature darkFeature = darkLaunch.getDarkFeature(userPromotionKey);
boolean dark = darkFeature.dark(893);
assertThat(dark).isFalse();
}