【服务治理】基于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,尽量与正常服务实例区分开,保证消费者的正常消费


  
  

  


  

posted @ 2023-05-24 16:40  听风是雨  阅读(409)  评论(0编辑  收藏  举报
/* 看板娘 */