赞
踩
日常开发中,对于一些重要的业务版本,如某用户产品的全新功能上线、功能全新升级,一般来说需要逐步发布,防止一些隐藏问题直接投向线上的全量用户,后果不堪设想,只能跑路;不仅限于业务版本,一些技术优化版本,如大表拆分数据迁移、数据脱敏加解密、DUBBO接口升级等,涉及重要的业务如订单、用户资产时,也不能一下梭哈上线,因为如果有前期未考虑到的漏洞时,发生异常带来的业务损失将是致命的;因此,灰度策略是必要的!
我们在手机升级系统时,往往有稳定包和Beta内测包可选,内测包就是用来检测问题,统一在稳定包中迭代修复的;对于客户端来说,APK安装包的灰度是必须的,因为它涉及用户操作,出现异常的概率性更大;对于服务端来说,大功能发布的发布往往也有类似的过程:
(1)内部点检试用
(2)灰度发布观察
(3)线上逐渐梯度
(4)线上全量生效
服务端逐渐灰度方案的实现方式一般有:
第一种方式可以在运维侧实现,缺点是在灰度策略不变即比例不变的情况下,用户可能一段时间命中灰度策略,一段时间又未命中灰度策略;
第二种方式更加简单粗暴,直接根据部署服务版本来实现灰度,缺点与第一种一样,出现问题时难以排查,尤其是新策略涉及写操作且不能回退的情况,并且新功能散落在各个接口时,用户的一次业务流程可能触发多个功能接口,而这些接口可能并没有都命中灰度策略(灰度机器);
因此,我们推荐第三种方案;本篇介绍基于该方案的服务端灰度工具的代码示例;
灰度策略的配置(10%比例):
- [
- {
- "grayStrategyId":"strategy_A",
- "graySwitch":"1",
- "openidWhiteList":[
- "userId-aaa",
- "userId-bbb"
- ],
- "imeiWhiteList":[
- "deviceId-aaa",
- "deviceId-bbb"
- ],
- "ratio":10,
- "modulo":100
- }
- ]
灰度工具代码:
- import com.alibaba.fastjson.JSON;
- import lombok.Data;
- import lombok.extern.slf4j.Slf4j;
- import org.apache.commons.collections.CollectionUtils;
- import org.apache.commons.collections.MapUtils;
- import org.apache.commons.lang3.StringUtils;
- import org.springframework.beans.factory.InitializingBean;
- import org.springframework.stereotype.Component;
-
- import java.util.List;
- import java.util.Map;
- import java.util.function.Function;
- import java.util.stream.Collectors;
-
- /**
- * @description 服务端灰度工具
- */
- @Slf4j
- @Component
- public class GrayManager implements InitializingBean {
-
- /**
- * 灰度策略,监听配置中心
- */
- private static Map<String, GrayConfig> grayConfig;
-
- /**
- * 开关:灰度中,部分老逻辑,部分新逻辑
- */
- private final static String SWITCH_ON = "1";
-
- /**
- * 开关:全部开放,全部走新逻辑
- */
- private final static String SWITCH_ALL = "-1";
-
- /**
- * 灰度规则配置key,可配置多条灰度策略存于List
- */
- private final String GRAY_CONFIG_KEY = "gray.rules";
-
- /**
- * 灰度策略配置
- */
- @Data
- private static class GrayConfig {
-
- /**
- * 灰度策略Id,唯一标识灰度策略
- */
- private String grayStrategyId;
-
- /**
- * 灰度开关类型:0-关闭灰度策略, 1-打开灰度策略
- */
- private String graySwitch;
-
- /**
- * openid白名单
- */
- private List<String> openidWhiteList;
-
- /**
- * imei白名单
- */
- private List<String> imeiWhiteList;
-
- /**
- * 取模后比率,只能逐渐增加,不能改小;ratio=0表示灰度关闭
- */
- private Integer ratio;
-
- /**
- * hash模,一旦设置就不能再修改
- */
- private Integer modulo;
-
- /**
- * 是否在用户标识Id白名单,优先使用userId
- */
- private boolean isInWhiteList(String userId, String deviceId) {
- try {
- // (1)用户账号标识存在,且配置了账号Id白名单,优先判断账号标识是否在白名单
- if (StringUtils.isNotBlank(userId) && CollectionUtils.isNotEmpty(this.getOpenidWhiteList())) {
- if (this.getOpenidWhiteList().contains(userId)) {
- return true;
- }
- }
- // (2)如果账号标识不在白名单,其次再判断设备标识是否在白名单
- if (StringUtils.isNotBlank(deviceId) && CollectionUtils.isNotEmpty(this.getImeiWhiteList())) {
- if (this.getImeiWhiteList().contains(deviceId)) {
- return true;
- }
- }
- } catch (Exception e) {
- log.error("isInOpenidWhiteList error![userId={} whiteList={}]", userId, this.getOpenidWhiteList());
- }
- return false;
- }
-
- /**
- * 用户标识账号Id是否在灰度范围中,暂不使用设备标识
- */
- private boolean isUserIdInGrayRatio(String userId) {
- // 先判断用户openid是否命中灰度
- if (StringUtils.isNotBlank(userId) && hitGrayRatio(userId)) {
- return true;
- }
- return false;
- }
-
- /**
- * 用户标识Id是否在灰度范围中
- */
- private boolean hitGrayRatio(String val) {
- long hashCode = unsignedHash(val);
- if (this.getModulo() == null || this.getRatio() == null) {
- log.error("isInGrayRatio_error, modulo_or_ratio_is_null.");
- return false;
- }
- return hashCode % this.getModulo() <= this.getRatio();
- }
-
- }
-
- /**
- * 监听配置中心变更,动态刷新灰度策略
- */
- @Override
- public void afterPropertiesSet() {
- // 初始化
- refreshGrayConfig();
- if (GrayManager.grayConfig == null) {
- log.error("GrayConfigs_init_fail_null!");
- }
- // 加监听器动态修改配置
- ConfigManager.addListener((item, type) -> {
- if (StringUtils.equals(GRAY_CONFIG_KEY, item.getName())) {
- GrayManager.this.refreshGrayConfig();
- }
- });
- log.warn("GrayConfigs_listener_init_suc.");
- }
-
- /**
- * 刷新灰度策略 [warning]先配置再启动
- */
- private synchronized void refreshGrayConfig() {
- String config = ConfigManager.getString(GRAY_CONFIG_KEY);
- if (StringUtils.isNotBlank(config)) {
- try {
- List<GrayConfig> grayConfigs = JSON.parseArray(config, GrayConfig.class);
- if (CollectionUtils.isNotEmpty(grayConfigs)) {
- GrayManager.grayConfig = grayConfigs.stream().collect(Collectors.toMap(GrayConfig::getGrayStrategyId, Function.identity(), (old, newly) -> old));
- log.warn("refreshGrayConfig_suc.[grayConfig={}]", JSON.toJSONString(grayConfig));
- }
- } catch (Exception e) {
- log.error("refreshGrayConfig_error! [config={}]", config);
- }
- }
- }
-
- /**/
- /**
- * 是否满足灰度策略,优先级:灰度策略开关->用户标识/设备标识 白名单 todo
- *
- * @param userId 用户账号标识
- * @param deviceId 用户设备标识
- * @param strategyId 某一套配置的key
- * @return
- */
- public static boolean isInGrayStrategy(String userId, String deviceId, String strategyId) {
- // 策略为空,认定未命中策略
- if (StringUtils.isBlank(strategyId) || MapUtils.isEmpty(grayConfig) || grayConfig.get(strategyId) == null) {
- return false;
- }
- // 用户标识/设备标识为空,认定未命中策略
- if (StringUtils.isBlank(userId) && StringUtils.isBlank(deviceId)) {
- return false;
- }
- // 当前灰度策略
- final GrayConfig grayConfigById = GrayManager.grayConfig.get(strategyId);
- try {
- // 全局开关打开,所有用户认为命中当前灰度策略
- if (StringUtils.equals(SWITCH_ALL, grayConfigById.getGraySwitch())) {
- return true;
- }
- // 全局开关未打开
- // (1)在用户白名单,认为命中灰度逻辑
- if (grayConfigById.isInWhiteList(userId, deviceId)) {
- return true;
- }
- // (2)用户不在白名单,则判断通过hash后[用户账号标识]是否认为命中灰度逻辑
- return grayConfigById.isUserIdInGrayRatio(userId);
- } catch (Exception e) {
- log.error("isInGrayStrategy_error![userId={} deviceId={}]", userId, deviceId, e);
- }
- // 异常时,认为未命中灰度策略
- return false;
- }
-
- /**
- * 对String取hash
- */
- private static long unsignedHash(String val) {
- if (null == val) {
- return 0L;
- }
- int code = val.hashCode();
- if (code < 0) {
- return 0L - code;
- } else {
- return code;
- }
- }
- }
灰度策略配置主要包括:策略Id、用户id白名单、用户设备报名单;策略为用户标识的hash取模落在比例中;在新功能的接口可以加上此工具的GrayManager .isInGrayStrategy方法,判断是否进入新的业务逻辑;
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。