3-Ribbon负载均衡服务调用


一、Ribbon概述

Spring Cloud Ribbon是 Netflix 公司开源的一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现,负载均衡的行为在客户端发生。

通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。Spring Cloud Ribbon虽然只是一个工具类框架,它不像服务注册中心、配置中心、API网关那样需要独立部署,但是它几乎存在于每一个Spring Cloud构建的微服务和基础设施中。因为微服务间的调用,API网关的请求转发等内容,实际上都是通过Ribbon来实现的,包括 Feign,它也是基于Ribbon实现的工具。

文章为了更贴切 Ribbon 主题,所以使用 RestTemplate 充当网络调用工具

RestTemplate 是 Spring Web 下提供访问第三方 RESTFul Http 接口的网络框架

Eureka的jar包中包含Ribbon。

实现:负载均衡+RestTemplate 调用

  • Ribbon工作时有两步
    1. 第一步先选择 EurekaServer,优先选择统一区域负载较少的 server
    2. 第二部再根据用户指定的策略,从server取到的服务注册列表中选择一个地址。其中 Riibon 提供了多种策略(轮询,随机,根据响应时间加权)。

二、RestTemplate的使用

1、配置类ApplicationContextConfig

RestTemplate 默认是没有负载均衡的,所以需要添加 @LoadBalanced

@Configuration
public class ApplicationContextConfig {

    @Bean
    //赋予RestTemplate负载均衡的能力,实现轮询
    @LoadBalanced
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }

}

2、getFor***()

区别:

  1. getForObject()返回对象为响应体中的数据转化成的对象,可以理解为json串(推荐使用)

  2. getForEntity()返回ResponseEntity对象,包含响应中的一些重要信息,包括响应头、状态码、响应体等。

@RestController
@Slf4j
public class OrderController {

    //服务名,用于查找对应的服务集群
    public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";

    //服务消费者直接通过调用被@LoadBalanced注解修饰过的RestTemplate来实现面向服务的接口调用。
    @Resource
    private RestTemplate restTemplate;

    //此处最后要加/,
    @GetMapping("/consumer/payment/get/{id}")
    public CommonResult<Payment> getPayment(@PathVariable("id") Long id){

        return restTemplate.getForObject(PAYMENT_URL+"/payment/get/"+id,CommonResult.class);

    }

    @GetMapping("/consumer/payment/getEntity/{id}")
    public CommonResult<Payment> getPayment2(@PathVariable("id") Long id){

        ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);

        //通过响应状态码判断是否成功访问
        if (entity.getStatusCode().is2xxSuccessful()){
            log.info("200");
            return entity.getBody();
        }else{
            return new CommonResult<>(444,"操作失败");
        }

    }
}

3、@LoadBalanced注意事项

注意:加了@LoadBalanced后,

不能直接访问地址,需要把地址改成你所调用的ur在eureka上注册的application.name

加了注解 @LoadBalanced 之后,我们的restTemplate 会走这个类RibbonLoadBalancerClient,serverid必须是我们访问的服务名称 ,当我们直接输入ip的时候获取的server是null,就会抛出异常

因为ribbon的作用是负载均衡,那么你直接使用ip地址,那么就无法起到负载均衡的作用,因为每次都是调用同一个服务,当你使用的是服务名称的时候,他会根据自己的算法去选择具有该服务名称的服务。

No instances available for localhost

三、核心组件IRule

IRule:根据特定算法从服务列表中选取一个要访问的服务,即改变RestTemplate的负载均衡规则

1、IRule默认自带的负载规则

(1)RoundRobinRule

轮询策略:Ribbon 默认采用的策略。若经过一轮轮询没有找到可用的provider,其最多轮询 10 轮(代码中写死的,不能修改)。若还未找到,则返回 null。

(2)RandomRule

随机策略:从所有可用的 provider 中随机选择一个。

(3)RetryRule

重试策略:先按照 RoundRobinRule 策略获取 server,若获取失败,则在指定的时限内重试。默认的时限为 500 毫秒。

重试策略有个子规则,默认是轮询。先通过子规则得到服务,如果服务不可用,开启一个定时任务,在这个定时任务生效之前不断循环选取服务,直到选择一个合适的服务。

(4)BestAvailableRule

最可用策略:选择并发量最小的 provider,即连接的消费者数量最少的provider。其会遍历服务列表中的每一个server,选择当前连接数量minimalConcurrentConnections 最小的server。

(5)AvailabilityFilteringRule

可用性敏感策略,过滤掉性能差的服务

可用过滤算法:该算法规则是过滤掉处于熔断状态的 server 与已经超过连接极限的server,对剩余 server 采用轮询策略。

过滤掉那些一直连接失败的且被标记为 circuit tripped 的后端 Server,并过滤掉那些高并发的后端 Server 或者使用一个 AvailabilityPredicate 来包含过滤 Server 的逻辑。其实就是检查 Status 里记录的各个 Server 的运行状态。

(6)ZoneAvoidanceRule

区域敏感性策略,综合考虑区域可用性和服务可用性,默认规则

使用 ZoneAvoidancePredicate 和 AvailabilityPredicate 来判断是否选择某个 Server,前一个判断判定一个 Zone 的运行性能是否可用,剔除不可用的 Zone(的所有 Server),AvailabilityPredicate 用于过滤掉连接数过多的 Server。

(7)WeightedResponseTimeRule

权重轮询策略,响应时间越短,越容易被选上

根据响应时间分配一个 Weight(权重),响应时间越长,Weight 越小,被选中的可能性越低。

(8)ResponseTimeWeightedRule

作用同 WeightedResponseTimeRule,ResponseTimeWeightedRule 后来改名为 WeightedResponseTimeRule。

(9)ClientConfigEnabledRoundRobinRule

客户端可配轮询,实际上就是轮询策略

2、自定义规则

优先同与调用者同 IP 的服务,如果没有那就随机

/**
 * nacos dev环境微服务调度规则
 * 优先同IP
 * debugger.local-ip 请配置为本机在nacos上的那个IP
 */
public class DevNacosDebugRule extends AbstractLoadBalancerRule {

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    //@Value("${debugger.local-ip:}")
    private String localIp;

    @Autowired
    private NacosDiscoveryProperties nacosDiscoveryProperties;

    /**
     * 重写choose方法
     */
    @SneakyThrows
    @Override
    public Server choose(Object key) {
        //获取负载均衡器
        BaseLoadBalancer baseLoadBalancer = (BaseLoadBalancer) getLoadBalancer();
        //调用服务的名字
        String invokedServerName = baseLoadBalancer.getName();
        //获取namingServer(包含nacos注册发现相关api)
        NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
        //获取被调用的服务的所有实例
        List<Instance> invokedAllInstanceList = namingService.getAllInstances(invokedServerName);
        if (invokedAllInstanceList.isEmpty()) {
            logger.warn("no instance in service {}", invokedServerName);
            return null;
        }

        String localIp = this.getLocalhostIp();

        for (Instance instance : invokedAllInstanceList) {
            // 同 ip 优先级最高,匹配到直接返回
            if (instance.getIp().equals(localIp)) {
                return new NacosServer(instance);
            }
        }

        Random random = new Random();
        Instance instance = invokedAllInstanceList.get(random.nextInt(invokedAllInstanceList.size()));
        return new NacosServer(instance);
    }


    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {

    }

    /**
     * 获取本地ip
     */
    public String getLocalhostIp() {
        if (!StringUtils.isEmpty(localIp)) {
            return localIp;
        }
        try {
            Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
            while (allNetInterfaces.hasMoreElements()) {
                NetworkInterface netInterface = allNetInterfaces.nextElement();
                Enumeration<InetAddress> addresses = netInterface.getInetAddresses();
                while (addresses.hasMoreElements()) {
                    InetAddress ip = addresses.nextElement();
                    if (ip instanceof Inet4Address
                            && !ip.isLoopbackAddress() //loopback地址即本机地址,IPv4的loopback范围是127.0.0.0 ~ 127.255.255.255
                            && !ip.getHostAddress().contains(":")) {
                        return ip.getHostAddress();
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

3、替换负载规则-原生Ribbon

(1)新建配置类

注意:注意:IRule配置类不能放在@ComponentSan 的包及子包下,因为默认的扫描会变成全局负载均衡都按照这样的规则。而@SpringBootApplication就包含这个注解,所以不能和主启动类放在一个包下。

public class MySelfRule {
        @Bean
        public IRule myRule(){
            return new RandomRule();//定义为随机
        }
}

(2)主启动类中添加注解

// 选择要接收的服务和配置类
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration = MySelfRule.class)

4、替换负载规则-Nacos

(1)全局配置

只需把 IRule 的实现类注入到容器中即可

(2)指定服务

# 针对调用 ribbon-order-service 服务时生效
ribbon-order-service:
  ribbon:
    NFLoadBalancerRuleClassName: com.rewind.config.MyRule1

# 针对调用 ribbon-stock-service 服务时生效
ribbon-stock-service:
  ribbon:
    NFLoadBalancerRuleClassName: com.rewind.config.MyRule2

此种方法自定义的规则类中,可使用 @Autowired 注入 bean

四、Ribbon底层原理

更多原理说明:https://juejin.cn/post/6933767343570944008#heading-13

底层原理实现这一块内容,会先说明使用 Ribbon 负载均衡调用远端请求的全过程,然后着重看一下 RandomRule 负载策略底层是如何实现

  1. 创建 ILoadBalancer 负载均衡客户端,初始化 Ribbon 中所需的 定时器和注册中心上服务实例列表
  2. 从 ILoadBalancer 中,通过 负载均衡选择出健康服务列表中的一个 Server
  3. 将服务名(ribbon-produce)替换为 Server 中的 IP + Port,然后生成 HTTP 请求进行调用并返回数据

上面已经说过,ILoadBalancer 是负责负载均衡路由的,内部会使用 IRule 实现类进行负载调用

public interface ILoadBalancer {
    public Server chooseServer(Object key);
      ...
}

chooseServer 流程中调用的就是 IRule 负载策略中的 choose 方法,在方法内部获取一个健康 Server

public Server choose(ILoadBalancer lb, Object key) {
    ... 
    Server server = null;
    while (server == null) {
        ...
        List<Server> upList = lb.getReachableServers();  // 获取服务列表健康实例
        List<Server> allList = lb.getAllServers();  // 获取服务列表全部实例
        int serverCount = allList.size();  // 全部实例数量

        // 全部实例数量为空,返回 null,相当于错误返回
        if (serverCount == 0) {  
            return null;
        }
        // 考虑到效率问题,使用多线程 ThreadLocalRandom 获取随机数
        int index = chooseRandomInt(serverCount);  
        // 获取健康实例
        server = upList.get(index);  
        if (server == null) {
            // 作者认为出现获取 server 为空,证明服务列表正在调整,但是!这只是暂时的,所以当前释放出了 CPU
            Thread.yield();
            continue;
        }
        if (server.isAlive()) {  // 服务为健康,返回
            return (server);
        }
        ...
    }
    return server;
}

简单说一下随机策略 choose 中流程

  1. 获取到全部服务、健康服务列表,判断全部实例数量是否等于 0,是则返回 null,相当于发生了错误
  2. 从全部服务列表里获取下标索引,然后去 健康实例列表获取 Server
  3. 如果获取到的 Server 为空会放弃 CPU,然后再来一遍上面的流程,相当于一种重试机制
  4. 如果获取到的 Server 不健康,设置 Server 等于空,再歇一会,继续走一遍上面的流程

比较简单,有小伙伴可能就问了,如果健康实例小于全部实例怎么办?这种情况下存在两种可能

  1. 运气比较好,从全部实例数量中随机了比较小的数,刚好健康实例列表有这个数,那么返回 Server
  2. 运气比较背,从全部实例数量中随机了某个数,健康实例列表数量为空或者小于这个数,直接会下标越界异常

为什么不直接从健康实例中选择实例呢

如果直接从健康实例列表选择,就能规避下标越界异常,为什么作者要先从全部实例中获取 Server 下标?


  目录