轻量级的灰度&配置平台|得物技术
目录
一、前言
二、总体架构设计
三、灰度服务关键设计
1. 灰度数据类型设计
2. 灰度规则设计
3. 简单灰度触发器模型
4. 灰度触发器模型和规则实现
5. 客户端版本号比较算法
6. 灰度服务SDK接口设计
四、配置服务关键设计
1. 配置可视化设计集成
2. 灰度服务的接口抽象
五、稳定的百分比流量调控
六、灰度分组设计
七、白名单设计
八、其他非功能设计
九、展望和总结
一
前言
二
总体架构设计
整体架构模块的概览
主要由如下模块组成:
灰度运营平台:为用户提供增删查改的灰度发布管理和UI界面; 灰度服务端:为灰度运营平台提供标准的增删查改功能、权限控制、灰度场景管理和应用接入命名空间; Nacos&Ark: 提供高性能的灰度配置读取和存储服务; 灰度SDK: 为研发提供轻量级高性能的灰度判断API和配置服务。
系统灰度开关可以在后台运营页面上进行可视化管理; 配置类型可以支持多种开关、灰度、业务配置; 配置的值可以采用富文本形式来进行编辑; 能够支持自定义维度白名单的方式进行灰度; 能够支持自定义多个分组的方式进行灰度; 能够支持自定义分组实验的方式进行灰度; 能够支持按照指定维度以一定比例进行灰度; 能够支持自定义维度进行灰度配置; 能够支持历史配置一键回滚和追溯配置; 能够方便灰度配置信息进行生命周期管理。
三
灰度服务关键设计
灰度数据类型设计
/*** 版本号*/VERSION("version"),/*** 字符串类型*/STRING("string"),/*** 集合类型*/SEGMENT("segment"),/*** 数字类型*/NUMBER("number"),/*** 非法规则*/NONE("none"),;
灰度规则设计
IS_IN("in"),IS_NOT_IN("notIn"),REGEX("regex"),NREGEX("nregex"),EQ("eq"),NEQ("neq"),EQUAL_TO("="),NOT_EQUAL_TO("!="),GREATER_THAN(">"),GREATER_OR_EQUAL(">="),LESS_THAN("<"),LESS_OR_EQUAL("<="),NONE("none"),
简单灰度触发器模型
@Data@NoArgsConstructorpublic class Toggle implements Serializable {/*** 默认不配置该字段,如果启用该字段fullGray=1,则全量开关不进行后续的版本判断*/private Integer fullGray = 0;/*** 是否生效;1:生效;0:不生效*/private Integer enabled = 1;/*** 规则列表,每一项都是或的关系*/private List<Rule> rules;/*** 规则列表* 每一项是或的关系*/@Datapublic static class Rule implements Serializable {/*** 规则字段条件,每一项是且的关系*/private List<Condition> conditions;}/*** 条件列表*/@Datapublic static class Condition implements Serializable {/*** 规则解析类型** @see ConditionType*/private String type;/*** 规则解析字段** @see GrayFields*/private String subject;/*** 条件判断** @see PredicateType*/private String predicate;/*** 条件判断内容*/private List<Object> objects;}}
灰度触发器模型和规则实现
public interface Matcher {ConditionType getConditionType();Map<PredicateType, PredicateMatcher> getPredicateMatcher();}
数字类型灰度规则匹配
/*** 数字类型匹配*/@Componentpublic class NumberMatch implements Matcher {@Overridepublic ConditionType getConditionType() {return ConditionType.NUMBER;}@Overridepublic Map<PredicateType, PredicateMatcher> getPredicateMatcher() {EnumMap<PredicateType, PredicateMatcher> matcherEnumMap = new EnumMap<>(PredicateType.class);matcherEnumMap.put(PredicateType.EQUAL_TO, ((target, objects) -> objects.stream().filter(Objects::nonNull).map(Objects::toString).filter(StringUtils::isNotBlank).map(Long::valueOf).anyMatch(t -> Objects.equals(Long.valueOf(String.valueOf(target)), t))));// ...... 此处后续的代码省略,设计和上面的实现雷同....return matcherEnumMap;}}
字符串类型灰度规则匹配
/*** 字符串匹配*/@Componentpublic class StringMatch implements Matcher {@Overridepublic ConditionType getConditionType() {return ConditionType.STRING;}@Overridepublic Map<PredicateType, PredicateMatcher> getPredicateMatcher() {EnumMap<PredicateType, PredicateMatcher> matcherEnumMap = new EnumMap<>(PredicateType.class);matcherEnumMap.put(PredicateType.EQ, ((target, objects) -> objects.stream().filter(Objects::nonNull).map(Objects::toString).filter(StringUtils::isNotBlank).anyMatch(t -> String.valueOf(target).equalsIgnoreCase(t))));// ...... 此处后续的代码省略,设计和上面的实现雷同....return matcherEnumMap;}}
客户端版本号灰度规则类型匹配
/*** @author feel*/@Componentpublic class VersionMatch implements Matcher {@Overridepublic ConditionType getConditionType() {return ConditionType.VERSION;}@Overridepublic Map<PredicateType, PredicateMatcher> getPredicateMatcher() {EnumMap<PredicateType, PredicateMatcher> matcherEnumMap = new EnumMap<>(PredicateType.class);matcherEnumMap.put(PredicateType.EQUAL_TO, ((target, objects) -> objects.stream().filter(Objects::nonNull).map(Objects::toString).filter(StringUtils::isNotBlank).anyMatch(t -> VersionUtils.compareMajorVersion(String.valueOf(target), t) == 0)));// ...... 此处后续的代码省略,设计和上面的实现雷同....return matcherEnumMap;}}
复杂集合类型灰度规则匹配
/*** 集合列表匹配*/@Componentpublic class SegmentMatch implements Matcher {@Overridepublic ConditionType getConditionType() {return ConditionType.SEGMENT;}@Overridepublic Map<PredicateType, PredicateMatcher> getPredicateMatcher() {EnumMap<PredicateType, PredicateMatcher> matcherEnumMap = new EnumMap<>(PredicateType.class);matcherEnumMap.put(PredicateType.IS_IN, ((target, objects) -> objects.stream().filter(Objects::nonNull).map(Objects::toString).filter(StringUtils::isNotBlank).anyMatch(t -> {if (target instanceof List || target instanceof Set) {return ((Collection<Object>) target).stream().filter(Objects::nonNull).map(Objects::toString).anyMatch(v -> String.valueOf(v).equalsIgnoreCase(t));}return String.valueOf(target).equalsIgnoreCase(t);})));// ...... 此处后续的代码省略,设计和上面的实现雷同....return matcherEnumMap;}}
客户端版本号比较算法
/*** 通用版本号比较* 时间复杂度O(n+m)* 空间复杂度O(n+m)** @param v1 版本号v1* @param v2 版本号v2* @param limit 限制截取长度* @return*/public static int compareVersion(String v1, String v2, Integer limit) {if (v1 == null || v2 == null) {return 0;}try {// 处理非法字符和老版本iphone,appversion:"5.16.1(100.0421)" 格式问题String[] majorV1 = parserPinkAppVersion(v1).split("\\.");String[] majorV2 = parserPinkAppVersion(v2).split("\\.");int len = Math.min(Optional.ofNullable(limit).orElse(Integer.MAX_VALUE), Math.max(majorV1.length, majorV2.length));// 一位一位比较,注意:中间不能直接跳出for (int i = 0; i < len; ++i) {int x = 0, y = 0;if (i < majorV1.length) {x = Integer.parseInt(majorV1[i]);}if (i < majorV2.length) {y = Integer.parseInt(majorV2[i]);}if (x > y) {return 1;}if (x < y) {return -1;}}} catch (NumberFormatException e) {//ignore}return 0;}
灰度服务SDK接口设计
/*** 灰度服务*/public interface GrayService {/*** 根据灰度场景判断当前请求是否命中灰度** @param sceneKey 灰度场景key* @param attrs 灰度参数值* @return*/boolean hitGray(String sceneKey, Map<String, Object> attrs);/*** 根据规则配置命中灰度** @param toggle* @param attrs* @return*/boolean hitGray(Toggle toggle, Map<String, Object> attrs);/*** 根据灰度场景key判断当前请求命中的的实验分组** @param sceneKey 灰度场景key* @param attrs 灰度参数值* @return* @see MapValueUtils 配置值的获取可以使用该工具方法*/HitResult hitExperimentGroup(String sceneKey, Map<String, Object> attrs);/*** 根据灰度场景key指定实验分组key,如果命中则返回命中分组都对应的配置;如果没有命中,则配置重置为空字符** @param sceneKey 灰度场景key* @param experimentGroupKey 实验分组KEY* @param attrs 灰度参数值* @return* @see MapValueUtils 配置值的获取可以使用该工具方法*/HitResult hitExperimentGroup(String sceneKey, String experimentGroupKey, Map<String, Object> attrs);}
四
配置服务关键设计
配置可视化设计集成
灰度服务的接口抽象
/*** 配置服务*/public interface GrayConfigService {/*** 获取配置JSON VALUE** @param sceneKey* @return*/String getConfigValue(String sceneKey);/*** 根据场景key获取String类型值** @param sceneKey 配置开关配置key* @param pathKey 字段key,支持路径path查找;字段是”$."前缀表示,则优先使用JSONPATH解析<a href="https://gotest.hz.netease.com/doc/jie-kou-ce-shi/xin-zeng-yong-li/can-shu-xiao-yan/jsonpi-pei/jsonpathyu-fa.html">jsonpath使用用例</a>* @param defaultValue 默认值* @return*/String getStringValue(String sceneKey, String pathKey, String defaultValue);/*** 根据场景key获取boolean类型值** @param sceneKey 配置开关配置key* @param pathKey 字段key,支持路径path查找;字段是”$."前缀表示,则优先使用JSONPATH解析<a href="https://gotest.hz.netease.com/doc/jie-kou-ce-shi/xin-zeng-yong-li/can-shu-xiao-yan/jsonpi-pei/jsonpathyu-fa.html">jsonpath使用用例</a>* @param defaultValue 默认值* @return*/boolean getBooleanValue(String sceneKey, String pathKey, boolean defaultValue);/*** 根据场景key获取Integer类型值** @param sceneKey 配置开关配置key* @param pathKey 字段key,支持路径path查找;字段是”$."前缀表示,则优先使用JSONPATH解析<a href="https://gotest.hz.netease.com/doc/jie-kou-ce-shi/xin-zeng-yong-li/can-shu-xiao-yan/jsonpi-pei/jsonpathyu-fa.html">jsonpath使用用例</a>* @param defaultValue 默认值* @return*/Integer getIntegerValue(String sceneKey, String pathKey, Integer defaultValue);/*** 根据场景key获取Long类型值** @param sceneKey 配置开关配置key* @param pathKey 字段key,支持路径path查找;字段是”$."前缀表示,则优先使用JSONPATH解析<a href="https://gotest.hz.netease.com/doc/jie-kou-ce-shi/xin-zeng-yong-li/can-shu-xiao-yan/jsonpi-pei/jsonpathyu-fa.html">jsonpath使用用例</a>* @param defaultValue 默认值* @return*/Long getLongValue(String sceneKey, String pathKey, Long defaultValue);/*** 根据场景key获取String类型值** @param sceneKey 配置开关配置key* @param pathKey 字段key,支持路径path查找;字段是”$."前缀表示,则优先使用JSONPATH解析<a href="https://gotest.hz.netease.com/doc/jie-kou-ce-shi/xin-zeng-yong-li/can-shu-xiao-yan/jsonpi-pei/jsonpathyu-fa.html">jsonpath使用用例</a>* @param defaultValue 默认值* @return*/Double getDoubleValue(String sceneKey, String pathKey, Double defaultValue);/*** 根据场景key获取BigDecimal类型值** @param sceneKey 配置开关配置key* @param pathKey 字段key,支持路径path查找;字段是”$."前缀表示,则优先使用JSONPATH解析<a href="https://gotest.hz.netease.com/doc/jie-kou-ce-shi/xin-zeng-yong-li/can-shu-xiao-yan/jsonpi-pei/jsonpathyu-fa.html">jsonpath使用用例</a>* @param scale 精度* @return*/BigDecimal getBigDecimalValue(String sceneKey, String pathKey, int scale);/*** 根据场景key获取String类型值** @param sceneKey 配置开关配置key* @param pathKey 字段key,支持路径path查找;字段是”$."前缀表示,则优先使用JSONPATH解析<a href="https://gotest.hz.netease.com/doc/jie-kou-ce-shi/xin-zeng-yong-li/can-shu-xiao-yan/jsonpi-pei/jsonpathyu-fa.html">jsonpath使用用例</a>* @return*/JSONArray getJSONArrayValue(String sceneKey, String pathKey);/*** 获取配置JSONObject** @param sceneKey* @return*/JSONObject getConfigJSONObject(String sceneKey);/*** 获取配置反序列化对象** @param sceneKey* @param targetClass* @param <T>* @return*/<T> T getConfigObject(String sceneKey, Class<T> targetClass);}
五
稳定的百分比流量调控
/*** 高性能的hash分桶算法*/private static final HashFunction murmur3 = Hashing.murmur3_32();/*** 100_00个分桶*/private static final int bucket = 100_00;
/*** 稳定的流量分桶算法* 用来调控灰度流量比例** @param attrsContext* @param bucketKey* @param rate* @return*/public boolean hitBucket(Map<String, Object> attrsContext, String bucketKey, Integer rate) {// 全量if (Objects.equals(Optional.ofNullable(rate).orElse(bucket), bucket)) {return true;}// 灰度流量比例为0if (Objects.equals(Optional.ofNullable(rate).orElse(bucket), 0)) {return false;}String bucketValue = Optional.ofNullable(attrsContext).map(v -> v.get(bucketKey)).filter(Objects::nonNull).map(String::valueOf).filter(StringUtils::isNotBlank).orElse(String.valueOf(System.currentTimeMillis()));// [0,10000)int hash = Hashing.consistentHash(murmur3.hashString(bucketValue, Charsets.UTF_8), bucket);int rateLimit = bucket;if (rate >= 0 && rate <= bucket) {rateLimit = rate;}return hash <= rateLimit;}
六
灰度分组设计
public class Toggle implements Serializable {/*** 规则列表,每一项都是或的关系*/private List<Rule> rules;/*** 规则列表* 每一项是或的关系*/@Datapublic static class Rule implements Serializable {..............}/*** 条件列表*/@Datapublic static class Condition implements Serializable {.......}}
七
白名单设计
public class Toggle implements Serializable {/*** 灰度白名单列表*/private List<WhiteList> whiteLists;/*** 白名单* 每一项是或的关系*/@Datapublic static class WhiteList implements Serializable {/*** 白名单解析字段** @see GrayFields*/private String subject;/*** 白名单内容*/private List<Object> values;}}
八
其他非功能设计
灰度数据大数据埋点服务集成,提供可视化的数据分析能力 灵活的权限控制 集成发布平台管控能力 全生命周期灰度场景下线机制 集成完整的ABTest能力
九
展望和总结
往期回顾
1. Apache Flink类型及序列化研读&生产应用|得物技术
2. 前端打包工具Mako架构解析|得物技术
3. 基于Rspack实现大仓应用构建提效实践|得物技术
4. 星愿森林的互动玩法揭秘|得物技术
5. StarRocks跨集群迁移最佳实践|得物技术
文 / feel
关注得物技术,每周一、三、五更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。
“
扫码添加小助手微信
如有任何疑问,或想要了解更多技术资讯,请添加小助手微信: