【灰度发布(三)】服务端灰度流量负载均衡规则实现

一. 服务间灰度路由的规则

核心原则参考如下图

1.灰度服务优先调度灰度服务

2.非灰度服务优先调度非灰度服务

3.下游目标灰度服务或非灰度服务不存在时,需要有兜底逻辑调用

image

 

二. Ribbon负载均衡规则

public class LoadBalancerRule extends AbstractLoadBalancerRule {
 
    @Autowired
    private NacosDiscoveryProperties nacosDiscoveryProperties;
    /**
     * 灰度标签  标记灰度示例
     */
    public static final String GRAY_TAG = "grayTag";
    /**
     * 灰度调用优先级 特殊服务需要优先调用灰度服务
     */
    public static final String GRAY_PRIORITY = "grayPriorityTag";
 
 
    @Override
    public Server choose(Object key) {
        List<Instance> allInstances = null;
        try {
            DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer();
            if (log.isDebugEnabled()) {
                log.debug("开始执行负载均衡服务调用,当前服务名称:{},目标服务名称:{}", nacosDiscoveryProperties.getService(), loadBalancer.getName());
            }
            allInstances = loadBalancer.getReachableServers().stream()
                    .map(server -> ((NacosServer) server).getInstance())
                    .collect(Collectors.toList());
            if (log.isDebugEnabled()) {
                log.debug("目标服务实例信息列表:{}", JSON.toJSONString(allInstances));
            }
            //校验目标实例是否存在
            if (CollectionUtils.isEmpty(allInstances)) {
                log.warn("目标服务实例不存在:{}", loadBalancer.getName());
                return null;
            }
            //获取当前实例元数据信息
            Map<String, String> currentInstanceMetadataMap = nacosDiscoveryProperties.getMetadata();
            if (log.isDebugEnabled()) {
                log.debug("当前实例元数据信息:{}", currentInstanceMetadataMap);
            }
            boolean grayPriority = false;
            String grayPriorityTag = currentInstanceMetadataMap.getOrDefault(GRAY_PRIORITY, GrayPriorityeEnum.DEFAULT.getCode());
            String currentInstanceGrayTag = currentInstanceMetadataMap.getOrDefault(GRAY_TAG, "");
            //扩展用于处理特殊实例, 校验是否是特殊实例,特殊实例优先执行灰度实例,
            if (GrayPriorityeEnum.HIGH.getCode().equals(grayPriorityTag)) {
                grayPriority = true;
            }
 
            //分类灰度实例和非灰度实例
            List<Instance> grayInstances = new ArrayList<>();
            List<Instance> normalInstances = new ArrayList<>();
            for (Instance instance : allInstances) {
                Map<String, String> targetMetadataMap = instance.getMetadata();
                String targetInstanceGrayTag = targetMetadataMap.get(GRAY_TAG);
                if (currentInstanceGrayTag.trim().equalsIgnoreCase(targetInstanceGrayTag.trim())) {
                    grayInstances.add(instance);
                } else {
                    normalInstances.add(instance);
                }
            }
            //灰度调用逻辑判断
            Instance toBeChooseInstance = null;
            if (grayPriority) {
                if (!CollectionUtils.isEmpty(grayInstances)) {
                    toBeChooseInstance = ExtendBalancer.getHostByRandomWeight2(grayInstances);
                } else {
                    toBeChooseInstance = ExtendBalancer.getHostByRandomWeight2(normalInstances);
                }
            } else {
                List<Instance> waitChooseGrayInstances = allInstances.stream()
                        .filter(instance -> StringUtils.isNotBlank(instance.getMetadata().get(GRAY_TAG))
                                && currentInstanceGrayTag.equals(instance.getMetadata().get(GRAY_TAG)))
                        .collect(Collectors.toList());
                if (!CollectionUtils.isEmpty(waitChooseGrayInstances)) {
                    toBeChooseInstance = ExtendBalancer.getHostByRandomWeight2(waitChooseGrayInstances);
                    if (log.isDebugEnabled()) {
                        log.debug("执行灰度实例调用,灰度实例信息:{}", JSON.toJSONString(toBeChooseInstance));
                    }
                } else {
                    toBeChooseInstance = ExtendBalancer.getHostByRandomWeight2(normalInstances);
                }
            }
 
            if (ObjectUtils.isEmpty(toBeChooseInstance)) {
                log.warn("灰度请求服务实例不存在,兜底从所有实例中默认选择一个实例");
                toBeChooseInstance = ExtendBalancer.getHostByRandomWeight2(allInstances);
            }
            return new NacosServer(toBeChooseInstance);
        } catch (Exception e) {
            log.warn("NacosRule error:{}", e.getMessage(), e);
            if (!CollectionUtils.isEmpty(allInstances)) {
                return new NacosServer(ExtendBalancer.getHostByRandomWeight2(allInstances));
            }
            return null;
        }
    }
 
    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {
 
    }

三. 关于 grayPriorityTag 属性的解释

由于部分服务中存在如下调用链,因此可以通过grayPriorityTag 当前服务优先进行目标灰度服务的调用

image

 

 

因此需要实现灰度调用优先级标签(grayPriorityTag) 自动刷新,由于Nacos discovery service 和 config service 逻辑上互相隔离,需要重写Nacos 配置变更监听器,实现服务实例 metadata信息变更同步到 Nacos 注册中心

 

public abstract class YamlListener extends AbstractListener

监听器实现

@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 GRAY_PRIORITY = "grayPriorityTag";
 
    @PostConstruct
    public void init() {
        try {
            //服务启动 初始化版本号到本地缓存
            Map<String, String> registryMetadataMap = nacosDiscoveryProperties.getMetadata();
            localMap.put(GRAY_PRIORITY, registryMetadataMap.getOrDefault(GRAY_PRIORITY, ""));
 
            //注册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 serverGrayTag) {
                    Object localGrayTag = localMap.getOrDefault(GRAY_PRIORITY, "");
                    if (log.isDebugEnabled()) {
                        log.debug("本地缓存灰度优先级标签 localGrayTag:{},配置中心实时灰度优先级标签 serverGrayTag:{}", localGrayTag, serverGrayTag);
                    }
                    if (StringUtils.isEmpty(localGrayTag) && StringUtils.isEmpty(serverGrayTag)) {
                        if (log.isDebugEnabled()) {
                            log.debug("本地缓存灰度优先级标签 与 配置中心实时灰度优先级标签,无需更新实例");
                        }
                        return;
                    }
                    if (localGrayTag.equals(serverGrayTag)) {
                        log.debug("本地缓存灰度优先级标签 与 配置中心实时灰度优先级标签 一致,无需更新");
                        return;
                    }
                    localMap.put(GRAY_PRIORITY, serverGrayTag);
                    log.info("开始更新服务实例灰度优先级标签元数据信息:{}",serverGrayTag);
                    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(serverGrayTag)) {
                            instance.getMetadata().remove(GRAY_PRIORITY);
                        } else {
                            instance.getMetadata().put(GRAY_PRIORITY, serverGrayTag);
                        }
                        nacosDiscoveryProperties.namingMaintainServiceInstance().updateInstance(service, instance);
                    } catch (Exception e) {
                        log.error("更新服务实例元数据信息失败:{}", e);
                    }
                }
            });
        } catch (Exception e) {
            log.error("初始化服务实例更新任务失败:{}",e.getMessage(), e);
        }
    }
 
    /**
     * 从注册中心获取实例信息
     *
     * @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);
    }
}
public class GrayEnvCondition implements Condition {

    @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("gray.config.auto-refresh");
            if (!ObjectUtils.isEmpty(property)) {
                return true;
            }
        }
        return false;
    }
}

 

posted @ 2025-08-22 20:36  听风是雨  阅读(16)  评论(0)    收藏  举报
/* 看板娘 */