设计实现一个支持自定义规则的灰度发布组件
# 设计实现一个支持自定义规则的灰度发布组件
来源:极客时间《设计模式之美》 (opens new window)专栏笔记
# 1. 需求背景
公共服务平台提供的是基于某个开源 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接口的代码逻辑
}
2
3
4
5
6
7
不过更改 callRestfulApi 的值需要修改代码,而修改代码就要重新部署,通常把这个值放到配置文件或者配置中心就可以了。为了更加保险,不只是使用功能开关做新老接口调用方式的切换,还希望调用方在替换某个接口的时候,先让小部分接口请求,调用新的 RESTful 接口,剩下的大部分接口请求,还是调用老的 RPC 接口,验证没有问题之后,再逐步加大调用新接口的请求比例,最终将所有的接口请求,都替换成调用新的接口。这就是所谓的“灰度”。
这个灰度功能又该如何实现呢?
首先要决定使用什么来做灰度,即灰度对象。可以针对请求携带的时间戳信息、业务 ID 等信息,按照区间、比例或者具体的值来做灰度。
假设要灰度的是根据用户 ID 查询用户信息接口。接口请求会携带用户 ID 信息,所以就可以把用户 ID 作为灰度的对象。为了实现逐渐放量,先配置用户 ID 是 918、879、123(具体的值)的查询请求调用新接口,验证没有问题之后再扩大范围,让用户 ID 在 1020~1120(区间值)之间的查询请求调用新接口。
如果验证之后还是没有问题再继续扩大范围,让 10% 比例(比例值)的查询请求调用新接口(对应用户 ID 跟 10 取模求余小于 1 的请求)。以此类推,灰度范围逐步扩大到 20%、30%、50% 直到 100%。当灰度比例达到 100%,并且运行一段时间没有问题之后,调用方就可以把老的代码逻辑删除掉了。
实际上类似的灰度需求场景还有很多。比如在金融产品的清结算系统中修改了清结算的算法。为了安全起见可以灰度替换新的算法,把贷款 ID 作为灰度对象,先对某几个贷款应用新的算法,如果没有问题,再继续按照区间或者比例,扩大灰度范围。
除此之外,为了保证代码万无一失,提前做好预案,添加或者修改一些复杂功能、核心功能,即便不做灰度,也建议通过功能开关,灵活控制这些功能的上下线。在不需要重新部署和重启系统的情况,做到快速回滚或新老代码逻辑的切换。
# 2. 需求分析
从实现的角度来讲,调用方只需要把灰度规则和功能开关,按照某种事先约定好的格式,存储到配置文件或者配置中心,在系统启动的时候,从中读取配置到内存中,之后看灰度对象是否落在灰度范围内,以此来判定是否执行新的代码逻辑。但为了避免每个调用方都重复开发,把功能开关和灰度相关的代码,抽象封装为一个灰度组件,提供给各个调用方来复用。
这里的灰度是代码级别的灰度,目的是保证项目质量,规避重大代码修改带来的不确定性风险。实际上平时经常讲的灰度,一般都是产品层面或者系统层面的灰度。所谓产品层面,有点类似 A/B Testing,让不同的用户看到不同的功能,对比两组用户的使用体验,收集数据,改进产品。所谓系统层面的灰度,往往不在代码层面上实现,一般是通过配置负载均衡或者 API-Gateway,来实现分配流量到不同版本的系统上。系统层面的灰度也是为了平滑上线功能,但比起代码层面的灰度就没有那么细粒度了,开发和运维成本也相对要高些。
# 2.1 功能性需求
从使用的角度来分析。组件使用者需要设置一个 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)的金额来做灰度
2
3
4
5
6
7
8
9
10
灰度组件在业务系统启动的时候,会将这个灰度配置,按照事先定义的语法,解析并加载到内存对象中,业务系统直接使用组件提供的灰度判定接口,给业务系统使用,来判定某个灰度对象是否灰度执行新的代码逻辑。配置的加载解析、灰度判定逻辑这部分代码,都不需要业务系统来从零开发。
public interface DarkFeature {
boolean enabled();
boolean dark(String darkTarget); //darkTarget是灰度对象,比如前面提到的用户ID、手机号码、金额等
}
2
3
4
总结一下的话,灰度组件跟限流框架很类似,它也主要包含两部分功能:灰度规则配置解析和提供编程接口(DarkFeature)判定是否灰度。而非功能性需求也类似。
# 2.2 非功能性需求
主要从易用性、扩展性、灵活性、性能、容错性这几个方面,来分析它的非功能性需求。
易用性
框架需要集成到业务系统中使用,通常希望它尽可能低侵入,与业务代码松耦合,替换、移除起来更容易些。因为接口的限流和幂等跟具体的业务是无关的,可以把限流和幂等相关的逻辑跟业务代码解耦,统一放到公共的地方来处理(比如 Spring AOP 切面中)。
但对于灰度来说,实现的灰度功能是代码级别的细粒度的灰度,而替代掉原来的 if-else 逻辑,是针对一个业务一个业务来做的,跟业务强相关,要做到跟业务代码完全解耦是不现实的。所以在侵入性这一点上,灰度组件只能做妥协,容忍一定程度的侵入。
除此之外,在灰度的过程中要不停地修改灰度规则,在测试没有出现问题的情况下逐渐放量。从运维的角度来说,如果每次修改灰度规则都要重启系统,显然是比较麻烦的。所以希望支持灰度规则的热更新,也就是说,当在配置文件中修改了灰度规则之后,系统在不重启的情况下会自动加载、更新灰度规则。
扩展性、灵活性
跟限流框架一样希望支持不同格式(JSON、YAML、XML 等)、不同存储方式(本地配置文件、Redis、Zookeeper、或者自研配置中心等)的灰度规则配置方式。除此之外,对于灰度规则本身,之前定义了三种灰度规则语法格式:具体值(比如 893)、区间值(比如 1020-1120)、比例值(比如 %30)。不过这只能处理比较简单的灰度规则。如果要支持更加复杂的灰度规则,比如只对 30 天内购买过某某商品并且退货次数少于 10 次的用户进行灰度,现在的灰度规则语法就无法支持了。所以如何支持更加灵活的、复杂的灰度规则,也是设计实现的重点和难点。
性能
在性能方面,灰度组件的处理难度并不像限流框架那么高。在限流框架中对于分布式限流模式,接口请求访问计数存储在中心存储器中,比如 Redis。而 Redis 本身的读写性能以及限流框架与 Redis 的通信延迟,都会很大地影响到限流本身的性能,进而影响到接口响应时间。所以对于分布式限流来说,低延迟高性能是设计实现的难点和重点。
但对于灰度组件来说,灰度的判断逻辑非常简单,而且不涉及访问外部存储,所以性能一般不会有太大问题。不过仍然需要把灰度规则组织成快速查找的数据结构,能够支持快速判定某个灰度对象(darkTarget,比如用户 ID)是否落在灰度规则设定的区间内。
容错性
在限流框架中要求高度容错,不能因为框架本身的异常,导致接口响应异常。从业务上来讲一般能容忍限流框架的暂时、小规模的失效,所以限流框架对于异常的处理原则是,尽可能捕获所有异常,并且内部“消化”掉,不要往上层业务代码中抛出。
对于幂等框架来说不能容忍框架暂时、小规模的失效,因为这种失效会导致业务有可能多次被执行,发生业务数据的错误。所以幂等框架对于异常的处理原则是,按照 fail-fast 原则,如果异常导致幂等逻辑无法正常执行,让业务代码也中止。因为业务执行失败,比业务执行出错,修复的成本更低。
对于灰度组件来说,上面的两种对异常的处理思路都是可以接受的。在灰度组件出现异常时,既可以选择中止业务,也可以选择让业务继续执行。如果让业务继续执行,本不应该被灰度到的业务对象,就有可能被执行。这是否能接受,还是要看具体的业务。不过更倾向于采用类似幂等框架的处理思路,在出现异常时中止业务。
# 3. 框架设计
主要包括这样两点:支持更灵活、更复杂的灰度规则和支持灰度规则热更新。
如何支持更灵活、更复杂的灰度规则
灰度规则的配置也是跟业务强相关的。业务方需要根据要灰度的业务特点,找到灰度对象( darkTarget,比如用户 ID),然后按照给出的灰度规则语法格式,配置相应的灰度规则。对于那种复杂的灰度规则(只对 30 天内购买过某某商品并且退货次数少于 10 次的用户进行灰度),通过定义语法规则来支持是很难实现的。可以有两种方案,其中一种是使用规则引擎,比如 Drools,可以在配置文件中调用 Java 代码。另一种是支持编程实现灰度规则,这样做灵活性更高。缺点是更新灰度规则需要更新代码,重新部署。
对于大部分业务的灰度使用前面定义的最基本的语法规则(具体值、区间值、比例值)就能满足了。对于极个别复杂的灰度规则借鉴 Spring 的编程式配置,由业务方编程实现,这样既兼顾了易用性,又兼顾了灵活性。之所以选择第二种实现方式,而不是使用 Drools 规则引擎,主要是出于不想为了不常用的功能,引入复杂的第三方框架,提高开发成本和灰度框架本身的学习成本。
如何实现灰度规则热更新
灰度规则的热更新实现起来并不难。创建一个定时器每隔固定时间(比如 1 分钟),从配置文件中,读取灰度规则配置信息,并且解析加载到内存中,替换掉老的灰度规则。需要特别强调的是,更新灰度规则,涉及读取配置、解析、构建等一系列操作,会花费比较长的时间,不能因为更新规则就暂停了灰度服务。所以在设计和实现灰度规则更新的时候,要支持更新和查询并发执行。
# 4. 框架实现
# 4.1 V1 版本功能需求
灰度规则的格式和存储方式
支持不同格式(JSON、YAML、XML 等)、不同存储方式(本地配置文件、Redis、Zookeeper、或者自研配置中心等)的灰度规则配置方式。
灰度规则的语法格式
支持三种灰度规则语法格式:具体值(比如 893)、区间值(比如 1020-1120)、比例值(比如 %30)。除此之外,对于更加复杂的灰度规则,比如只对 30 天内购买过某某商品并且退货次数少于 10 次的用户进行灰度,通过编程的方式来实现。
灰度规则的内存组织方式
需要把灰度规则组织成支持快速查找的数据结构,能够快速判定某个灰度对象(darkTarget,比如用户 ID),是否落在灰度规则设定的范围内。
灰度规则热更新
希望不重新部署和重启系统,新的灰度规则就能生效,所以需要支持灰度规则热更新。
# 4.2 实现灰度组件基本功能
基本功能的代码由 4 个类组成。
// 代码目录结构
com.xzg.darklaunch
--DarkLaunch(框架的最顶层入口类)
--DarkFeature(每个feature的灰度规则)
--DarkRule(灰度规则)
--DarkRuleConfig(用来映射配置到内存中)
// 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));
}
}
// 灰度规则配置(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}
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
对于业务系统来说,灰度组件的两个直接使用的类是 DarkLaunch 类和 DarkFeature 类。
DarkLaunch 类 这个类是灰度组件的最顶层入口类。它用来组装其他类对象,串联整个操作流程,提供外部调用的接口。DarkLaunch 类先读取灰度规则配置文件,映射为内存中的 Java 对象(DarkRuleConfig),然后再将这个中间结构,构建成一个支持快速查询的数据结构(DarkRule)。除此之外,它还负责定期更新灰度规则,也就是前面提到的灰度规则热更新。
为了避免更新规则和查询规则的并发执行冲突,在更新灰度规则的时候,并非直接操作老的 DarkRule,而是先创建一个新的 DarkRule,然后等新的 DarkRule 都构建好之后,再“瞬间”赋值给老的 DarkRule。
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 DarkRule rule;
private ScheduledExecutorService executor;
public DarkLaunch(int ruleUpdateTimeInterval) {
loadRule();
this.executor = Executors.newSingleThreadScheduledExecutor();
this.executor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
loadRule();
}
}, ruleUpdateTimeInterval, ruleUpdateTimeInterval, TimeUnit.SECONDS);
}
public DarkLaunch() {
this(DEFAULT_RULE_UPDATE_TIME_INTERVAL);
}
private void loadRule() {
// 将灰度规则配置文件dark-rule.yaml中的内容读取DarkRuleConfig中
InputStream in = null;
DarkRuleConfig ruleConfig = null;
try {
in = this.getClass().getResourceAsStream("/dark-rule.yaml");
if (in != null) {
Yaml yaml = new Yaml();
ruleConfig = yaml.loadAs(in, DarkRuleConfig.class);
}
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
log.error("close file error:{}", e);
}
}
}
if (ruleConfig == null) {
throw new RuntimeException("Can not load dark rule.");
}
// 更新规则并非直接在this.rule上进行,
// 而是通过创建一个新的DarkRule,然后赋值给this.rule,
// 来避免更新规则和规则查询的并发冲突问题
DarkRule newRule = new DarkRule(ruleConfig);
this.rule = newRule;
}
public DarkFeature getDarkFeature(String featureKey) {
DarkFeature darkFeature = this.rule.getDarkFeature(featureKey);
return darkFeature;
}
}
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
DarkRuleConfig 类的功能非常简单,只是用来将灰度规则映射到内存中。
public class DarkRuleConfig {
private List<DarkFeatureConfig> features;
public List<DarkFeatureConfig> getFeatures() {
return this.features;
}
public void setFeatures(List<DarkFeatureConfig> features) {
this.features = features;
}
public static class DarkFeatureConfig {
private String key;
private boolean enabled;
private String rule;
// 省略getter、setter方法
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DarkRuleConfig 类嵌套了一个内部类 DarkFeatureConfig。这两个类跟配置文件的两层嵌套结构完全对应。
<!--对应DarkRuleConfig-->
features:
- key: call_newapi_getUserById <!--对应DarkFeatureConfig-->
enabled: true
rule: {893,342,1020-1120,%30}
- key: call_newapi_registerUser <!--对应DarkFeatureConfig-->
enabled: true
rule: {1391198723, %10}
- key: newalgo_loan <!--对应DarkFeatureConfig-->
enabled: true
rule: {0-1000}
2
3
4
5
6
7
8
9
10
11
DarkRule 类包含所有要灰度的业务功能的灰度规则。它用来支持根据业务功能标识(feature key),快速查询灰度规则(DarkFeature)。
public class DarkRule {
private Map<String, DarkFeature> darkFeatures = new HashMap<>();
public DarkRule(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);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
DarkFeature 类表示每个要灰度的业务功能的灰度规则。DarkFeature 将配置文件中灰度规则,解析成一定的结构(比如 RangeSet),方便快速判定某个灰度对象是否落在灰度规则范围内。具体的代码如下所示:
public class DarkFeature {
private String key;
private boolean enabled;
private int percentage;
private RangeSet<Long> rangeSet = TreeRangeSet.create();
public DarkFeature(DarkRuleConfig.DarkFeatureConfig darkFeatureConfig) {
this.key = darkFeatureConfig.getKey();
this.enabled = darkFeatureConfig.getEnabled();
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 enabled() {
return this.enabled;
}
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;
}
public boolean dark(String darkTarget) {
long target = Long.parseLong(darkTarget);
return dark(target);
}
}
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
# 4.3 添加、优化灰度组件功能
实现基于编程的灰度规则配置方式,用来支持更加复杂、更加灵活的灰度规则。改造之后的代码目录结构如下所示。其中,DarkFeature、DarkRuleConfig 的基本代码不变,新增了 IDarkFeature 接口,DarkLaunch、DarkRule 的代码有所改动,用来支持编程实现灰度规则。
// 第一步的代码目录结构
com.xzg.darklaunch
--DarkLaunch(框架的最顶层入口类)
--DarkFeature(每个feature的灰度规则)
--DarkRule(灰度规则)
--DarkRuleConfig(用来映射配置到内存中)
// 第二步的代码目录结构
com.xzg.darklaunch
--DarkLaunch(框架的最顶层入口类,代码有改动)
--IDarkFeature(抽象接口)
--DarkFeature(实现IDarkFeature接口,基于配置文件的灰度规则,代码不变)
--DarkRule(灰度规则,代码有改动)
--DarkRuleConfig(用来映射配置到内存中,代码不变)
2
3
4
5
6
7
8
9
10
11
12
13
14
IDarkFeature 接口用来抽象从配置文件中得到的灰度规则,以及编程实现的灰度规则。具体代码如下所示:
public interface IDarkFeature {
boolean enabled();
boolean dark(long darkTarget);
boolean dark(String darkTarget);
}
2
3
4
5
基于这个抽象接口,业务系统可以自己编程实现复杂的灰度规则,然后添加到 DarkRule 中。为了避免配置文件中的灰度规则热更新时,覆盖掉编程实现的灰度规则,在 DarkRule 中对从配置文件中加载的灰度规则和编程实现的灰度规则分开存储。重构之后的代码如下所示:
public class DarkRule {
// 从配置文件中加载的灰度规则
private Map<String, IDarkFeature> darkFeatures = new HashMap<>();
// 编程实现的灰度规则
private ConcurrentHashMap<String, IDarkFeature> programmedDarkFeatures = new ConcurrentHashMap<>();
public void addProgrammedDarkFeature(String featureKey, IDarkFeature darkFeature) {
programmedDarkFeatures.put(featureKey, darkFeature);
}
public void setDarkFeatures(Map<String, IDarkFeature> newDarkFeatures) {
this.darkFeatures = newDarkFeatures;
}
public IDarkFeature getDarkFeature(String featureKey) {
IDarkFeature darkFeature = programmedDarkFeatures.get(featureKey);
if (darkFeature != null) {
return darkFeature;
}
return darkFeatures.get(featureKey);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
因为 DarkRule 代码有所修改,对应地 DarkLaunch 的代码也需要做少许改动,主要有一处修改和一处新增代码,具体如下所示:
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 DarkRule rule = new DarkRule();
private ScheduledExecutorService executor;
public DarkLaunch(int ruleUpdateTimeInterval) {
loadRule();
this.executor = Executors.newSingleThreadScheduledExecutor();
this.executor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
loadRule();
}
}, ruleUpdateTimeInterval, ruleUpdateTimeInterval, TimeUnit.SECONDS);
}
public DarkLaunch() {
this(DEFAULT_RULE_UPDATE_TIME_INTERVAL);
}
private void loadRule() {
InputStream in = null;
DarkRuleConfig ruleConfig = null;
try {
in = this.getClass().getResourceAsStream("/dark-rule.yaml");
if (in != null) {
Yaml yaml = new Yaml();
ruleConfig = yaml.loadAs(in, DarkRuleConfig.class);
}
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
log.error("close file error:{}", e);
}
}
}
if (ruleConfig == null) {
throw new RuntimeException("Can not load dark rule.");
}
// 修改:单独更新从配置文件中得到的灰度规则,不覆盖编程实现的灰度规则
Map<String, IDarkFeature> darkFeatures = new HashMap<>();
List<DarkRuleConfig.DarkFeatureConfig> darkFeatureConfigs = ruleConfig.getFeatures();
for (DarkRuleConfig.DarkFeatureConfig darkFeatureConfig : darkFeatureConfigs) {
darkFeatures.put(darkFeatureConfig.getKey(), new DarkFeature(darkFeatureConfig));
}
this.rule.setDarkFeatures(darkFeatures);
}
// 新增:添加编程实现的灰度规则的接口
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;
}
}
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
灰度组件使用方式如下:
// 灰度规则配置(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-100}
// 编程实现的灰度规则
public class UserPromotionDarkRule implements IDarkFeature {
@Override
public boolean enabled() {
return true;
}
@Override
public boolean dark(long darkTarget) {
// 灰度规则自己想怎么写就怎么写
return false;
}
@Override
public boolean dark(String darkTarget) {
// 灰度规则自己想怎么写就怎么写
return false;
}
}
// Demo
public class Demo {
public static void main(String[] args) {
DarkLaunch darkLaunch = new DarkLaunch(); // 默认加载classpath下dark-rule.yaml文件中的灰度规则
darkLaunch.addProgrammedDarkFeature("user_promotion", new UserPromotionDarkRule()); // 添加编程实现的灰度规则
IDarkFeature darkFeature = darkLaunch.getDarkFeature("user_promotion");
System.out.println(darkFeature.enabled());
System.out.println(darkFeature.dark(893));
}
}
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