我们已经说了灰度组件的需求和设计思路,今天要进行相对应的实现工作

关于灰度规则框架

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

我们希望支持不同格式 JSON YAML XML.不同存储方式的 Redis Zookeeper,自研的配置中心等灰度规则的配置方法

2.灰度规则的格式

我们支持多种灰度规则语法格式:具体值 比如891,区间值,比如 1102-1120,比例值 比如%30

除此之外,对于更加复杂的灰度规则,我们通过暴露接口的方式来实现

3.灰度规则的内存组织方式

我们将灰度的规则组织成快速查找的数据结构,方便能够快速的判定某个灰度的对象能够落在灰度规则的范围内

4.灰度规则热更新

修改了灰度规则之后,我们不希望部署或者重新系统,新的灰度规则就能生效,我们希望能够支持热更新

在v1版本,对于第一点灰度规则的格式或者存储方式,我们只需要YAML格式本地文件的配置存储方式,对于剩下的几点,我们在v1版本一同实现了

首先实现灰度组件的基本功能呢

我们先基于YAML格式的本地文件的灰度规则配置方式,以及灰度规则的热更新

我们将这个功能的基本开发需求,用代码实现出来,目录结构如下

// 代码目录结构

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}

对于业务系统来说,灰度组件的两个直接使用的类是DarkLauch类和DarkFeature类

首先是顶级的上帝类

DarkLauch类,是整个灰度组件的最顶层入口类,组装其他类的对象,串联整个操作的流程,提供外部调用的接口

DarkLauch先去读取了配置规则的文件,映射为了内存之中的JAVA对象

再将这个中间结构,构建成一个支持快速查找辨识的数据结构DarkRule,定期更新灰度规则,前面提到的灰度规则热更新

对于这个上帝类的实现

首先是关于热更新

最好是使用了CAS的设计思想,先构建,完成后进行设置

或者是使用读写锁,读是无所谓,写时上写锁

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;

}

}

读取配置文件,进行定期的热更新,进行获取规则,这些一应俱全

然后看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方法

}

}

里面嵌套了一个内部类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}

在DarkRule,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);

}

}

最后是DarkFeature类,DarkFeature类要表示每个灰度的业务功能的灰度规则,DarkFeature将配置文件中的灰度规则,解析成一定的结构,方便快速判定某个灰度对象是否符合灰度规则

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);

}

}

优化我们的灰度组件功能

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

第二步实现的代码,进行一些改造,改造后的代码目录结构如下所示,其中DarkFeature,DarkRuleConfig的基本代码不变,新增了IDarkFeature接口,DarkLaunch,DarkRule的代码进行了改动,支持编程来实现灰度

// 第一步的代码目录结构

com.xzg.darklaunch

–DarkLaunch(框架的最顶层入口类)

–DarkFeature(每个feature的灰度规则)

–DarkRule(灰度规则)

–DarkRuleConfig(用来映射配置到内存中)

// 第二步的代码目录结构

com.xzg.darklaunch

–DarkLaunch(框架的最顶层入口类,代码有改动)

–IDarkFeature(抽象接口)

–DarkFeature(实现IDarkFeature接口,基于配置文件的灰度规则,代码不变)

–DarkRule(灰度规则,代码有改动)

–DarkRuleConfig(用来映射配置到内存中,代码不变)

在IDarkFeature接口,用来抽象从配置文件中得到的灰度规则,以及编程实现的灰度规则

支持的接口如下

public interface IDarkFeature {

boolean enabled();

boolean dark(long darkTarget);

boolean dark(String darkTarget);

}

基于抽象的接口,业务系统可以自己编程实现复杂的灰度规则,然后添加到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);

}

}

因为DarkRule的代码有所修改,对应的DarkLauch的代码也需要进行修改

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;

}

}

最后,看一个Demo,如何使用现在实现的灰度组件

// 灰度规则配置(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));

}

}

添加的自定义编程接口手段还是有点搓的

本章重点

我们分析了限流,幂等,灰度三个实战项目,从需求分析,系统设计,代码实现三个环节,学习了如何进行功能性,非功能性的需求分析,通过合理的设计,完成功能性需求的同时,满足非功能性的需求

实际上,项目本身的分析,设计实现,不重要,主要是对思考的思路,开发的套路进行思考

课后思考

在DarkFeature类中,灰度规则的解析代码设计的不够优雅,如何办呢?

在此类场景下,我们可以简单的使用工厂类去封装规则的解析,

但是我个人觉着,应该以配置文件中配置的规则为主,所以,第二版需要在配置文件中写上实现接口的全限定类名,反射获取实例,同样支持更新,这样配置文件的Map就可以移除了,而且可以将简单的原生三种解析规则也抽象为接口,利用策略类进行区分调用

发表评论

邮箱地址不会被公开。 必填项已用*标注