当前位置:   article > 正文

编码技巧——灰度工具_java 灰度开关

java 灰度开关

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

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

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

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

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

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

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

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

灰度策略的配置(10%比例):

  1. [
  2. {
  3. "grayStrategyId":"strategy_A",
  4. "graySwitch":"1",
  5. "openidWhiteList":[
  6. "userId-aaa",
  7. "userId-bbb"
  8. ],
  9. "imeiWhiteList":[
  10. "deviceId-aaa",
  11. "deviceId-bbb"
  12. ],
  13. "ratio":10,
  14. "modulo":100
  15. }
  16. ]

灰度工具代码:

  1. import com.alibaba.fastjson.JSON;
  2. import lombok.Data;
  3. import lombok.extern.slf4j.Slf4j;
  4. import org.apache.commons.collections.CollectionUtils;
  5. import org.apache.commons.collections.MapUtils;
  6. import org.apache.commons.lang3.StringUtils;
  7. import org.springframework.beans.factory.InitializingBean;
  8. import org.springframework.stereotype.Component;
  9. import java.util.List;
  10. import java.util.Map;
  11. import java.util.function.Function;
  12. import java.util.stream.Collectors;
  13. /**
  14. * @description 服务端灰度工具
  15. */
  16. @Slf4j
  17. @Component
  18. public class GrayManager implements InitializingBean {
  19. /**
  20. * 灰度策略,监听配置中心
  21. */
  22. private static Map<String, GrayConfig> grayConfig;
  23. /**
  24. * 开关:灰度中,部分老逻辑,部分新逻辑
  25. */
  26. private final static String SWITCH_ON = "1";
  27. /**
  28. * 开关:全部开放,全部走新逻辑
  29. */
  30. private final static String SWITCH_ALL = "-1";
  31. /**
  32. * 灰度规则配置key,可配置多条灰度策略存于List
  33. */
  34. private final String GRAY_CONFIG_KEY = "gray.rules";
  35. /**
  36. * 灰度策略配置
  37. */
  38. @Data
  39. private static class GrayConfig {
  40. /**
  41. * 灰度策略Id,唯一标识灰度策略
  42. */
  43. private String grayStrategyId;
  44. /**
  45. * 灰度开关类型:0-关闭灰度策略, 1-打开灰度策略
  46. */
  47. private String graySwitch;
  48. /**
  49. * openid白名单
  50. */
  51. private List<String> openidWhiteList;
  52. /**
  53. * imei白名单
  54. */
  55. private List<String> imeiWhiteList;
  56. /**
  57. * 取模后比率,只能逐渐增加,不能改小;ratio=0表示灰度关闭
  58. */
  59. private Integer ratio;
  60. /**
  61. * hash模,一旦设置就不能再修改
  62. */
  63. private Integer modulo;
  64. /**
  65. * 是否在用户标识Id白名单,优先使用userId
  66. */
  67. private boolean isInWhiteList(String userId, String deviceId) {
  68. try {
  69. // (1)用户账号标识存在,且配置了账号Id白名单,优先判断账号标识是否在白名单
  70. if (StringUtils.isNotBlank(userId) && CollectionUtils.isNotEmpty(this.getOpenidWhiteList())) {
  71. if (this.getOpenidWhiteList().contains(userId)) {
  72. return true;
  73. }
  74. }
  75. // (2)如果账号标识不在白名单,其次再判断设备标识是否在白名单
  76. if (StringUtils.isNotBlank(deviceId) && CollectionUtils.isNotEmpty(this.getImeiWhiteList())) {
  77. if (this.getImeiWhiteList().contains(deviceId)) {
  78. return true;
  79. }
  80. }
  81. } catch (Exception e) {
  82. log.error("isInOpenidWhiteList error![userId={} whiteList={}]", userId, this.getOpenidWhiteList());
  83. }
  84. return false;
  85. }
  86. /**
  87. * 用户标识账号Id是否在灰度范围中,暂不使用设备标识
  88. */
  89. private boolean isUserIdInGrayRatio(String userId) {
  90. // 先判断用户openid是否命中灰度
  91. if (StringUtils.isNotBlank(userId) && hitGrayRatio(userId)) {
  92. return true;
  93. }
  94. return false;
  95. }
  96. /**
  97. * 用户标识Id是否在灰度范围中
  98. */
  99. private boolean hitGrayRatio(String val) {
  100. long hashCode = unsignedHash(val);
  101. if (this.getModulo() == null || this.getRatio() == null) {
  102. log.error("isInGrayRatio_error, modulo_or_ratio_is_null.");
  103. return false;
  104. }
  105. return hashCode % this.getModulo() <= this.getRatio();
  106. }
  107. }
  108. /**
  109. * 监听配置中心变更,动态刷新灰度策略
  110. */
  111. @Override
  112. public void afterPropertiesSet() {
  113. // 初始化
  114. refreshGrayConfig();
  115. if (GrayManager.grayConfig == null) {
  116. log.error("GrayConfigs_init_fail_null!");
  117. }
  118. // 加监听器动态修改配置
  119. ConfigManager.addListener((item, type) -> {
  120. if (StringUtils.equals(GRAY_CONFIG_KEY, item.getName())) {
  121. GrayManager.this.refreshGrayConfig();
  122. }
  123. });
  124. log.warn("GrayConfigs_listener_init_suc.");
  125. }
  126. /**
  127. * 刷新灰度策略 [warning]先配置再启动
  128. */
  129. private synchronized void refreshGrayConfig() {
  130. String config = ConfigManager.getString(GRAY_CONFIG_KEY);
  131. if (StringUtils.isNotBlank(config)) {
  132. try {
  133. List<GrayConfig> grayConfigs = JSON.parseArray(config, GrayConfig.class);
  134. if (CollectionUtils.isNotEmpty(grayConfigs)) {
  135. GrayManager.grayConfig = grayConfigs.stream().collect(Collectors.toMap(GrayConfig::getGrayStrategyId, Function.identity(), (old, newly) -> old));
  136. log.warn("refreshGrayConfig_suc.[grayConfig={}]", JSON.toJSONString(grayConfig));
  137. }
  138. } catch (Exception e) {
  139. log.error("refreshGrayConfig_error! [config={}]", config);
  140. }
  141. }
  142. }
  143. /**/
  144. /**
  145. * 是否满足灰度策略,优先级:灰度策略开关->用户标识/设备标识 白名单 todo
  146. *
  147. * @param userId 用户账号标识
  148. * @param deviceId 用户设备标识
  149. * @param strategyId 某一套配置的key
  150. * @return
  151. */
  152. public static boolean isInGrayStrategy(String userId, String deviceId, String strategyId) {
  153. // 策略为空,认定未命中策略
  154. if (StringUtils.isBlank(strategyId) || MapUtils.isEmpty(grayConfig) || grayConfig.get(strategyId) == null) {
  155. return false;
  156. }
  157. // 用户标识/设备标识为空,认定未命中策略
  158. if (StringUtils.isBlank(userId) && StringUtils.isBlank(deviceId)) {
  159. return false;
  160. }
  161. // 当前灰度策略
  162. final GrayConfig grayConfigById = GrayManager.grayConfig.get(strategyId);
  163. try {
  164. // 全局开关打开,所有用户认为命中当前灰度策略
  165. if (StringUtils.equals(SWITCH_ALL, grayConfigById.getGraySwitch())) {
  166. return true;
  167. }
  168. // 全局开关未打开
  169. // (1)在用户白名单,认为命中灰度逻辑
  170. if (grayConfigById.isInWhiteList(userId, deviceId)) {
  171. return true;
  172. }
  173. // (2)用户不在白名单,则判断通过hash后[用户账号标识]是否认为命中灰度逻辑
  174. return grayConfigById.isUserIdInGrayRatio(userId);
  175. } catch (Exception e) {
  176. log.error("isInGrayStrategy_error![userId={} deviceId={}]", userId, deviceId, e);
  177. }
  178. // 异常时,认为未命中灰度策略
  179. return false;
  180. }
  181. /**
  182. * 对String取hash
  183. */
  184. private static long unsignedHash(String val) {
  185. if (null == val) {
  186. return 0L;
  187. }
  188. int code = val.hashCode();
  189. if (code < 0) {
  190. return 0L - code;
  191. } else {
  192. return code;
  193. }
  194. }
  195. }

灰度策略配置主要包括:策略Id、用户id白名单、用户设备报名单;策略为用户标识的hash取模落在比例中;在新功能的接口可以加上此工具的GrayManager .isInGrayStrategy方法,判断是否进入新的业务逻辑;

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/323578
推荐阅读
相关标签
  

闽ICP备14008679号