基于用户和ip的灰度发布方案 为了能够更好的解决系统新版本上线无法验证的风险,我们通常需要在升级的时候进行灰度发布,下面调研了一个上线灰度发布的流程。
下面先看一张图,然后再用一段文字描述整个发布的逻辑。
在正常情况下,用户的流量是可以随意打到A,A1,A2服务上的,A到B的流量,也是可以随意打的。 当灰度发布的开关打开以后,如图上所示,例如A3,B2,C4的链路为灰度路由的配置。此时将切断正常用户对A3,B2,C4的服务实例的访问,并且正常流量到A1以后,也不会再打到B2上。 当指定的测试账号访问系统的时候,所有的测试流量都会打到A3,B2,C4的链路服务实例上,第一步流量到A3以后,也不会将A调用B的流量打到B1的服务实例上。 当测试将这一组验证完毕后,即可将对配置进行修改,进而发布下一组。 当所有的实例都发布完成后,再将灰度发布开关关闭,此时,发布完成。 所有的这些操作,都可以在Apollo配置中心一键完成。
链路配置格式:
1 2 3 4 { "sevice-demo-a" : "172.16.101.90:5000" , "sevice-demo-b" : "172.16.101.90:5020" }
实现原理 本方案基于Nepxion Discovery开发
Nepxion Discovery is an enhancement for Spring Cloud Discovery on Eureka + Consul + Zookeeper + Nacos with Nacos + Apollo config for gray release, router and isolation 灰度发布、服务隔离、服务路由、服务权重、黑/白名单过滤 http://www.nepxion.com
要了解灰度发布的原理,首先需要知道SpringCloud是基于Ribbon实现负载均衡算法的,以及是如何从注册中心拉取的服务列表。
灰度路由除了可以根据请求头来路由请求,也可以跟服务实例本身的版本号来决定路由。下面给出一个逻辑图,以及两个个源码解析的UML图。
nepxion本来就提供了良好的服务过滤扩展,所以我的代码就是核心的一个过滤策略。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 @Log4j2 public class GrayIpDiscoveryEnabledStrategy implements DiscoveryEnabledStrategy { private static final String IS_GRAY_USER = "isGrayUser" ; private static final String SERVICE_IP = "serviceIp" ; private static final String GRAY_SWITCH = "graySwitch" ; @Autowired private ServiceStrategyContextHolder serviceStrategyContextHolder; private Map<String, String> serviceIpMap; private boolean graySwitch; @Value("${apollo.plugin.namespace}") private String namespace; @Autowired private ObjectMapper objectMapper; @PostConstruct public void init () { Config config = ConfigService.getConfig(namespace); serviceIpMap = toMap(config.getProperty(SERVICE_IP, "{}" )); graySwitch = config.getBooleanProperty(SWITCH, Boolean.FALSE); config.addChangeListener(changeEvent -> { if (changeEvent.isChanged(SERVICE_IP)) { final String newValue = changeEvent.getChange(SERVICE_IP).getNewValue(); final String oldValue = changeEvent.getChange(SERVICE_IP).getOldValue(); log.info("service ip map changed, oldValue is {}, newValue is {}" , () -> oldValue, () -> newValue); serviceIpMap = toMap(newValue); } if (changeEvent.isChanged(GRAY_SWITCH)) { final String newValue = changeEvent.getChange(GRAY_SWITCH).getNewValue(); final String oldValue = changeEvent.getChange(GRAY_SWITCH).getOldValue(); log.info("graySwitch changed, oldValue is {}, newValue is {}" , () -> oldValue, () -> newValue); graySwitch = Boolean.valueOf(changeEvent.getChange(GRAY_SWITCH).getNewValue()); } }, Sets.newHashSet(SERVICE_IP, GRAY_SWITCH)); } @Override public boolean apply (Server server, Map<String, String> metadata) { boolean enabled = applyIpFilter(server, metadata); if (!enabled) { return false ; } return true ; } private boolean applyIpFilter (Server server, Map<String, String> metadata) { if (!graySwitch) { return true ; } String appName = server.getMetaInfo().getAppName(); String ip = server.getHostPort(); final String ipHost = serviceIpMap.get(appName); if (ipHost != null ) { final boolean ipMatch = ip.equals(ipHost); ServletRequestAttributes restAttributes = serviceStrategyContextHolder.getRestAttributes(); if (restAttributes == null ) { log.info("The ServletRequestAttributes object is null, ignore to do gray ip filter for service={}" , appName); return !ipMatch; } final String isGrayUser = restAttributes.getRequest().getHeader(IS_GRAY_USER); if (StringUtils.isEmpty(isGrayUser)) { log.info("The isGrayUser header is null, ignore to do gray ip filter for service={}" , appName); return !ipMatch; } if (Boolean.valueOf(isGrayUser)) { if (ipMatch) { log.info("found gray user request, service {} route to ip {}" , appName, ipHost); } return ipMatch; } else { return !ipMatch; } } return true ; } @SneakyThrows public Map<String, String> toMap (String str) { JavaType javaType = getParametricTypeJavaType(String.class, String.class); return objectMapper.readValue(str, javaType); } private JavaType getParametricTypeJavaType (Class... clazz) { return objectMapper.getTypeFactory().constructParametricType(HashMap.class, clazz); } }
nepxion discovery框架的作者是个很用心的作者,我在开发的时候也遇到了一些问题,作者都耐心的和我一起分析,一一帮我解决了,推荐大家去Star,去学习。
https://github.com/Nepxion/Discovery