SpringCloudAlibaba-扩展Ribbon实现同集群优先调用

303 阅读4分钟

🌈往期回顾

第一期:Spring Cloud Alibaba前景如何?

第二期:Spring Cloud Alibaba Nacos服务治理

第三期:Spring Cloud Alibaba Sentinel - 分布式系统的流量防卫兵

第四期:Spring Cloud Alibaba Ribbon - 负载均衡

第五期:Spring Cloud Alibaba Gateway - 服务网关

第六期:SpringCloudAlibaba - 扩展Ribbon支持Nacos权重

在扩展Ribbon实现同一集群服务优先调用之前我们先来回顾一下Nacos服务发现领域模型,下面是我画的一个领域模型图:

Nacos服务发现领域模型.jpg

领域模型

数据模型

Nacos 数据模型 Key 由三元组唯一确定, Namespace默认是空串,公共命名空间(public),分组默认是 DEFAULT_GROUP。

nacos_data_model

服务领域模型

nacos_naming_data_model

配置领域模型

围绕配置,主要有两个关联的实体,一个是配置变更历史,一个是服务标签(用于打标分类,方便索引),由 ID 关联。

nacos_config_er

了解完Nacos领域模型时候,下面开始实现通过扩展Ribbon实现同集群按按权重优先调用,首先自定义NacosSameWeigthedRule,通过继承AbstractLoadBalancerRule实现重写调用的规则策略,⌨实现的思路是:

  1. 先找到指定服务的所有实例instancesA
  2. 过滤出相同集群的所有实例 instancesB
  3. 如果发现同集群的instancesB为空,才跨集群调用instancesA
  4. 基于权重的负载均衡,返回1个可调用的健康实例

代码实现

package com.jacklin.mamba.contentcenter.post.config;

import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.NacosServiceManager;
import com.alibaba.cloud.nacos.ribbon.NacosServer;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.naming.NamingService;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.alibaba.nacos.client.naming.core.Balancer;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.Server;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * 扩展Ribbon - 同一集群下的优先调用
 * <p>
 * 北京机房的内容中心优先调用北京机房的用户中心实例,如果找不到北京机房的用户实例,才调用南京机房的用户实例
 *
 * @author: jacklin
 * @date: 2022/6/9 22:31
 */
@Slf4j
public class NacosSameClusterWeightedRule extends AbstractLoadBalancerRule {

    @Autowired
    private NacosDiscoveryProperties nacosDiscoveryProperties;
    @Autowired
    private NacosServiceManager nacosServiceManager;

    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {

    }

    @Override
    public Server choose(Object key) {

        try {
            //拿到配置文件中的集群名称
            String clusterName = nacosDiscoveryProperties.getClusterName();

            BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
            //获取想要调用的服务名称
            String serviceName = loadBalancer.getName();
            NamingService namingService = nacosServiceManager.getNamingService(nacosDiscoveryProperties.getNacosProperties());

            //1.找到指定服务的所有实例A
            List<Instance> instances = namingService.selectInstances(serviceName, true);
            //2.过滤出相同集群的所有实例B
            List<Instance> sameClusterInstances = instances.stream().filter(instance -> Objects.equals(instance.getClusterName(), clusterName)).collect(Collectors.toList());
            //3.如果B是空,就调用A
            List<Instance> instancesToBeChosen = new ArrayList<>();
            if (CollectionUtil.isEmpty(sameClusterInstances)) {
                //如果同一个集群下没有找到实例
                instancesToBeChosen = instances;
                log.warn("发生跨集群调用serviceName = {},clusterName = {},instances = {}", serviceName, clusterName, instances);
            } else {
                instancesToBeChosen = sameClusterInstances;
            }
            //4.基于权重的负载均衡算法,返回1个可调用实例
            Instance chooseInstance = ExtendBalancer.getHostByRandomWeightExtend(instancesToBeChosen);
            log.info("选择的实例是 instance = {},port = {}", chooseInstance, chooseInstance.getPort());

            return new NacosServer(chooseInstance);

        } catch (NacosException e) {
            log.error("同一集群下的优先调用异常,{}", e);
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 这里扩展Balancer的getHostByRandomWeightExtend,由于getHostByRandomWeightExtend是protect修饰不能被修改,可以通过继承的方式来调用Balancer.getHostByRandomWeightExtend();
     *
     * @author: jacklin
     * @since 2022/6/9 22:58
     **/
    static class ExtendBalancer extends Balancer {
        public static Instance getHostByRandomWeightExtend(List<Instance> hosts) {
            return Balancer.getHostByRandomWeight(hosts);
        }
    }
}

全局配置

/**
 * Ribbon负载均衡规则配置
 *
 * @author jacklin
 * @Date: 2022-01-06 22:28
 */
@Configuration
public class RibbonConfiguration {

    @Bean
    public IRule ribbonRule(){
        return new NacosSameClusterWeightedRule();
    }

    //@Bean
    //public IPing ping(){
    //    return new PingUrl();
    //}
}

现在我们通过内容中心调用用户中心,假设内容中心集群部署在北京机房,用户中心分布部署在北京机房和广州机房,配置文件信息如下:

content-center.application.yml (北京机房实例,端口9081)

server:
  port: 9081
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/content_center?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=convertToNull
    username: root
    password: admin
    driver-class-name: com.mysql.cj.jdbc.Driver
  application:
    name: content-center
  cloud:
    nacos:
      discovery:
        #指定nacos server地址
        server-addr: localhost:8848
        #服务名称
        service: content-center
        #集群部署在北京机房(命名为:BJ)
        cluster-name: BJ

# 开启Ribbon饥饿加载(Ribbon默认是懒加载模式,会导致首次情况RT过长)
ribbon:
  eager-load:
    enabled: true
    # 指定Ribbon请求user-center服务的时候启饥饿加载,多个可以用","分隔
    clients: user-center

user-center.application.yml (北京机房实例1,端口8080)

server:
  port: 8080
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/user_center?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=convertToNull
    username: root
    password: admin
    driver-class-name: com.mysql.cj.jdbc.Driver
  application:
    name: user-center
  cloud:
    nacos:
      discovery:
        #指定nacos server地址
        server-addr: localhost:8848
        #服务名称
        service: user-center
        # 指定namespace
        # namespace: 82758abc-a175-4b25-be4e-af74ee82697d
        # 指定集群名称(北京集群-BJ)
        cluster-name: BJ
        metadata:
          instance: a
          version: v1.0.0

user-center.application.yml (广州机房实例2,端口8081)

server:
  port: 8080
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/user_center?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=convertToNull
    username: root
    password: admin
    driver-class-name: com.mysql.cj.jdbc.Driver
  application:
    name: user-center
  cloud:
    nacos:
      discovery:
        #指定nacos server地址
        server-addr: localhost:8848
        #服务名称
        service: user-center
        # 指定namespace
        # namespace: 82758abc-a175-4b25-be4e-af74ee82697d
        # 指定集群名称(广州集群-GZ)
        cluster-name: GZ
        metadata:
          instance: a
          version: v1.0.0

现在分别都将内容中心(北京9081)、用户中心(北京8080&广州8081)启动起来,通过内容中心调用用户中心相关接口观察调用日志情况:

image.png

用户实例信息:

image.png

内容中心通过RestTemplate调用用户中心实例(10次),发现请求都打到了北京机房(同集群)的用户中心实例上: image.png

image.png

当把北京机房集群的用户实例下线之后,再次访问发现: image.png

这时候发现请求达到了广州机房的用户中心,发生了跨集群调用,至此我们就已经扩展实现Ribbon同集群优先调用了。