编码技巧——灰度工具

日常开发中,对于一些重要的业务版本,如某用户产品的全新功能上线、功能全新升级,一般来说需要逐步发布,防止一些隐藏问题直接投向线上的全量用户,后果不堪设想,只能跑路;不仅限于业务版本,一些技术优化版本,如大表拆分数据迁移、数据脱敏加解密、DUBBO接口升级等,涉及重要的业务如订单、用户资产时,也不能一下梭哈上线,因为如果有前期未考虑到的漏洞时,发生异常带来的业务损失将是致命的;因此,灰度策略是必要的!

我们在手机升级系统时,往往有稳定包和Beta内测包可选,内测包就是用来检测问题,统一在稳定包中迭代修复的;对于客户端来说,APK安装包的灰度是必须的,因为它涉及用户操作,出现异常的概率性更大;对于服务端来说,大功能发布的发布往往也有类似的过程:

(1)内部点检试用
(2)灰度发布观察
(3)线上逐渐梯度
(4)线上全量生效

服务端逐渐灰度方案的实现方式一般有:

  • 按照请求特征随机灰度,如请求时间、IP,可以借助NGINX配置策略;
  • 按照机器灰度,如100台机器,灰度10%比例也就是随机10台机器部署新的代码观察;
  • 按照用户特征灰度,如用户userId、deviceId;

第一种方式可以在运维侧实现,缺点是在灰度策略不变即比例不变的情况下,用户可能一段时间命中灰度策略,一段时间又未命中灰度策略;

第二种方式更加简单粗暴,直接根据部署服务版本来实现灰度,缺点与第一种一样,出现问题时难以排查,尤其是新策略涉及写操作且不能回退的情况,并且新功能散落在各个接口时,用户的一次业务流程可能触发多个功能接口,而这些接口可能并没有都命中灰度策略(灰度机器);

因此,我们推荐第三种方案;本篇介绍基于该方案的服务端灰度工具的代码示例;

灰度策略的配置(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方法,判断是否进入新的业务逻辑;

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值