【服务治理】基于SpringCloudAlibaba微服务组件的灰度发布设计(二)
一.背景
在上文中,灰度发布遇到了些问题,例如:
1.多个终端,例如移动端(IOS和Android),PC端,Web端对应的版本号不同,但又需要访问同一个后台微服务,网关灰度路由怎么配置
2.动态修改Nacos配置中心的元数据信息,如何同步到Nacos注册中心对应服务的列表中
3.管理后台业务调用其它服务灰度实例时的路由规则
4.SpringCloudGateway基于请求头Romote断言时,由于客户端多级代理IP的问题,导致获取到的IP不准确的问题
5.服务实例中RocketMQ的灰度方案
二.解决方案
针对以上问题的解决措施
问题1: 多个终端,例如移动端(IOS和Android),PC端,Web端对应的版本号不同,但又需要访问同一个后台微服务,网关灰度路由怎么配置
例如:客户端版本号:10000 ,移动端版本号:20000, 那么对应的微服务中Nacos灰度配置的版本号如何处理
解决方案:
首先在灰度路由中配置自定义请求头的断言,如基于请求头版本号字段的断言,然后在网关后续的过滤器中,将版本号字段指定统一的版本号:例如8888
意味着将所有满足灰度路由断言的请求都会携带上版本号:8888这个标识,以便后续的负载均衡器中执行灰度路由策略
#表示自定义请求头version字段 满足10000或者20000的值,都会替换为version=8888
- id: u-nnpc
uri: grayLb://client-platform
predicates:
- Path=/u-nnpc/**
- Header=version, (10000)|(20000)
filters:
- StripPrefix=1
- AddRequestHeader=version, 8888
问题2: 动态修改Nacos配置中心的元数据信息,如何同步到Nacos注册中心对应服务的列表中
Nacos作为微服务的注册中心和配置中心,但在配置中心修改注册中心相关的元数据信息时,注册中心服务列表里面的实例信息的元数据不会主动更新,这主要原因是注册中心和配置中心是两个独立运行的模块
通常我们有需求,例如将配置中心里面灰度配置文件里面元数据的版本号由8888修改为4444,此时注册中心是不会主动拉取元数据信息的,实例元数据信息在服务启动时,就会将元数据信息通过注册的方式同步到注册中心了,后续的心跳也不会同步元数据信息,此时可以依靠配置中心配置变更的监听器来监听配置的变化,过滤出元数据信息,然后主动向注册中心更新当前实例信息
/** * 增加条件组件,目的是只针对灰度服务实例做配置监听和变更,减小服务端压力 * @author Sam.yang * @since 2023/4/20 16:01 */ public class GrayEnvCondition implements Condition { /** * 获取灰度环境 */ public List<String> grayEnvs = ImmutableList.of("gray", "testgray"); @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata annotatedTypeMetadata) { Environment environment = context.getEnvironment(); if (environment instanceof StandardEnvironment) { MutablePropertySources mutablePropertySources = ((StandardEnvironment) environment).getPropertySources(); return matchProp(mutablePropertySources); } return false; } private boolean matchProp(MutablePropertySources mutablePropertySources) { Iterator<PropertySource<?>> iterator = mutablePropertySources.iterator(); for (Iterator<PropertySource<?>> it = iterator; it.hasNext(); ) { Object property = it.next().getProperty("spring.profiles.active"); if (!ObjectUtils.isEmpty(property) && grayEnvs.contains(property.toString())) { return true; } } return false; } }
增加Yaml监听器
/** * @author Sam.yang * @since 2023/4/21 20:55 */ @Slf4j public abstract class YamlListener extends AbstractListener { /** * 接受配置变更信息 * * @param configInfo 配置信息 */ public void receiveConfigInfo(String configInfo) { if (StringUtils.isEmpty(configInfo)) { return; } try { Yaml yaml = new Yaml(); Map<String, Object> yamlMap = yaml.load(configInfo); JSON json = (JSON) JSON.toJSON(yamlMap); Object versionValueObj = JSONPath.extract(json.toString(), "$..metadata.version[0]"); this.innerReceive(versionValueObj == null ? null : versionValueObj.toString()); } catch (Exception e) { log.error("解析Nacos配置变更数据异常:{}", e); this.innerReceive(""); } } public abstract void innerReceive(String serverVersionVal); }
增加灰度配置监听Bean整合上述监听器
@Slf4j @Component @Conditional(value = GrayEnvCondition.class) public class NacosGayMetadataComponent { @Autowired private NacosConfigProperties nacosConfigProperties; @Autowired private NacosDiscoveryProperties nacosDiscoveryProperties; private Map<String, Object> localMap = new ConcurrentHashMap<>(); public static final String VERSION = "version"; @PostConstruct public void init() { try { //服务启动 初始化版本号到本地缓存 Map<String, String> registryMetadataMap = nacosDiscoveryProperties.getMetadata(); localMap.put(VERSION, registryMetadataMap.getOrDefault(VERSION, "")); //注册Nacos配置变化监听器 String service = nacosDiscoveryProperties.getService(); ConfigService configService = nacosConfigProperties.configServiceInstance(); String group = nacosConfigProperties.getGroup(); configService.addListener(service + "-" + nacosConfigProperties.getEnvironment().getActiveProfiles()[0] + "." + nacosConfigProperties.getFileExtension(), group, new YamlListener() { @Override public void innerReceive(String serverVersionVal) { Object localVersionVal = localMap.getOrDefault(VERSION, ""); log.debug("本地缓存版本号 localVersion:{},配置中心实时版本号 serverVersion:{}", localVersionVal, serverVersionVal); if (StringUtils.isEmpty(localVersionVal) && StringUtils.isEmpty(serverVersionVal)) { log.debug("本地缓存版本号与远端配置版本号均为空,无需更新实例"); return; } if (localVersionVal.equals(serverVersionVal)) { log.debug("本地缓存版本号与远端配置版本号一致,无需更新"); return; } localMap.put(VERSION, serverVersionVal); log.debug("开始更新服务实例元数据信息"); String ip = nacosDiscoveryProperties.getIp(); Integer port = nacosDiscoveryProperties.getPort(); String service = nacosDiscoveryProperties.getService(); Instance instance = null; try { instance = getInstanceFromRegistry(nacosDiscoveryProperties, service, ip, port); if (instance == null) { log.warn("当前实例与注册中心服务列表中的实例均不匹配"); return; } if (StringUtils.isEmpty(serverVersionVal)) { instance.getMetadata().remove(VERSION); } else { instance.getMetadata().put(VERSION, serverVersionVal); }
//主动发起更新实例 nacosDiscoveryProperties.namingMaintainServiceInstance().updateInstance(service, instance); } catch (Exception e) { log.error("更新服务实例元数据信息失败:{}", e); } } }); } catch (Exception e) { log.error("初始化服务实例更新任务失败"); } } /** * 从注册中心获取实例信息 * * @param nacosDiscoveryProperties {@link NacosDiscoveryProperties} * @param service 当前实例服务名 * @param ip 当前实例IP * @param port 当前实例端口 * @return 实例信息 {@link Instance} * @throws NacosException */ private Instance getInstanceFromRegistry(NacosDiscoveryProperties nacosDiscoveryProperties, String service, String ip, Integer port) throws NacosException { NamingService namingService = nacosDiscoveryProperties.namingServiceInstance(); List<Instance> allInstances = namingService.selectInstances(service, true); if (CollectionUtils.isEmpty(allInstances)) { log.warn("注册中心服务列表实例为空:{}", service); return null; } return allInstances.stream().filter(instance -> ip.equals(instance.getIp()) && port.equals(instance.getPort())) .findFirst().orElse(null); }
问题3.管理后台业务调用其它服务灰度实例时的路由规则
业务中,通常需要灰度的API服务提供业务接口FeignClient给到管理后台服务调用,然而管理后台服务又是全量发布,需要灰度的API服务的业务代码比比线上的正常的API服务功能要齐全,意味着通过OpenFeign调用时,在原有的负载均衡器的基础上,需要对管理后台服务进行OpenFeign调用时特殊处理,即当被调用方服务实例同时存在灰度实例和正常实例时,优先调度到灰度实例上
//通过判断当前调用方实例的实例名称是否是管理后台服务实例 log.info("校验微服务之间的流量调用,优先执行灰度服务实例调用,调用方服务名称:{},被调用方服务名称:{}", serviceName, name); List<Instance> waitChooseGrayInstances = allInstances.stream().filter(instance -> instance.getMetadata() .containsKey(VERSION)).collect(Collectors.toList()); if (!CollectionUtils.isEmpty(waitChooseGrayInstances)) { toBeChooseInstance = ExtendBalancer.getHostByRandomWeight2(waitChooseGrayInstances); log.debug("执行灰度实例调用,灰度实例信息:{}", JSON.toJSONString(toBeChooseInstance)); return new NacosServer(toBeChooseInstance); }
问题4.SpringCloudGateway基于请求头Romote断言时,由于客户端多级代理IP的问题,导致获取到的IP不准确的问题
此时需要在网关中增加一个配置,表示只信任第一个IP
参考链接:http://hk.javashuo.com/article/p-qdwyxgyg-be.html
/** * @author Sam.yang * @since 2023/5/12 18:18 */ @Configuration public class GatewayConfig { @Bean public RemoteAddressResolver customRemoteAddressResolver() { return XForwardedRemoteAddressResolver.maxTrustedIndex(1); } }
问题5.服务实例中RocketMQ的灰度方案
在灰度服务实例时,负载均衡器对OpenFeign请求做了处理,保证能按我们的灰度配置标识(例如版本号)去进行调度,但是在使用中间件的场景下,需要我们对中间件的配置特殊处理,避免上下游服务全量和灰度之间的兼容性问题
如:正常实例的RokcetMQ Topic配置为TopicA,且是在代码中写死
那么灰度实例的RocketMQ Topic配置可以这是为Gray-TopicA,尽量与正常服务实例区分开,保证消费者的正常消费