我们已经说了灰度组件的需求和设计思路,今天要进行相对应的实现工作
关于灰度规则框架
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就可以移除了,而且可以将简单的原生三种解析规则也抽象为接口,利用策略类进行区分调用