【微服务七】Ribbon负载均衡策略之BestAvailableRule源码深度剖析

一、前言

前置Ribbon相关文章:

  1. 【云原生&微服务一】SpringCloud之Ribbon实现负载均衡详细案例(集成Eureka、Ribbon)
  2. 【云原生&微服务二】SpringCloud之Ribbon自定义负载均衡策略(含Ribbon核心API)
  3. 【云原生&微服务三】SpringCloud之Ribbon是这样实现负载均衡的(源码剖析@LoadBalanced原理)
  4. 【云原生&微服务四】SpringCloud之Ribbon和Erueka集成的细节全在这了(源码剖析)
  5. 【微服务五】Ribbon随机负载均衡算法如何实现的
  6. 【微服务六】Ribbon负载均衡策略之轮询(RoundRobinRule)、重试(RetryRule)

我们聊了以下问题:

  1. 为什么给RestTemplate类上加上了@LoadBalanced注解就可以使用Ribbon的负载均衡?
  2. SpringCloud是如何集成Ribbon的?
  3. Ribbon如何作用到RestTemplate上的?
  4. 如何获取到Ribbon的ILoadBalancer?
  5. ZoneAwareLoadBalancer(属于ribbon)如何与eureka整合,通过eureka client获取到对应注册表?
  6. ZoneAwareLoadBalancer如何持续从Eureka中获取最新的注册表信息?
  7. 如何根据负载均衡器ILoadBalancer从Eureka Client获取到的List<Server>中选出一个Server?
  8. Ribbon如何发送网络HTTP请求?
  9. Ribbon如何用IPing机制动态检查服务实例是否存活?
  10. Ribbon负载均衡策略之随机(RandomRule)、轮询(RoundRobinRule)、重试(RetryRule)实现方式;

本文继续讨论 最佳可用规则(BestAvailableRule)是如何实现的?

PS:Ribbon依赖Spring Cloud版本信息如下:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.3.7.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--整合spring cloud-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.SR8</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--整合spring cloud alibaba-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2.2.5.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

二、BestAvailableRule

BestAvailableRule会逐个考察Server,如果Server被tripped了,则跳过;最终选择一个并发请求量最小的Server。

1、负载规则

我们知道Ribbon负载均衡算法体现在IRule的choose(Object key)方法中,所以看BestAvailableRule的choose(Object key)方法:

在这里插入图片描述

详细代码注释如下:

public class BestAvailableRule extends ClientConfigEnabledRoundRobinRule {

    // 维护了服务实例的一些状态信息
    private LoadBalancerStats loadBalancerStats;
    
    @Override
    public Server choose(Object key) {
       // 如果服务实例状态信息为空,则直接使用父类的choose()方法,采用RoundRobin算法
        if (loadBalancerStats == null) {
            return super.choose(key);
        }
        // 获取所有的服务实例
        List<Server> serverList = getLoadBalancer().getAllServers();
        // 最小并发连接数
        int minimalConcurrentConnections = Integer.MAX_VALUE;
        // 当前时间
        long currentTime = System.currentTimeMillis();
        Server chosen = null;
        // 遍历每个实例
        for (Server server: serverList) {
            ServerStats serverStats = loadBalancerStats.getSingleServerStat(server);
            // 如果服务实例被tripped了,则直接跳过当前服务实例
            if (!serverStats.isCircuitBreakerTripped(currentTime)) {
                // 获取实例的并发数(当且仅当 当前时间与上次有效更改连接数的时间间隔在指定范围之内(默认10分钟))
                int concurrentConnections = serverStats.getActiveRequestsCount(currentTime);
                // 找到并发连接最小的那个服务实例
                if (concurrentConnections < minimalConcurrentConnections) {
                    minimalConcurrentConnections = concurrentConnections;
                    chosen = server;
                }
            }
        }
        // 如果遍历完所有的服务实例之后,还没有找到server,则调用父类的choose()方法,用RoundRobin算法进行选择。
        if (chosen == null) {
            return super.choose(key);
        } else {
            return chosen;
        }
    }

    @Override
    public void setLoadBalancer(ILoadBalancer lb) {
        super.setLoadBalancer(lb);
        if (lb instanceof AbstractLoadBalancer) {
            loadBalancerStats = ((AbstractLoadBalancer) lb).getLoadBalancerStats();            
        }
    }
}

方法的核心逻辑:

  1. 首先判断如果服务实例状态信息为空,则直接使用父类的choose()方法,采用RoundRobin算法。
  2. 否则:从BestAvailableRule所属的ILoadBalancer中获取服务的所有实例,记录当前时间;
  3. 遍历服务的每个实例,获取实例的ServerStats,如果实例被tripped了,则直接跳过当前服务实例;否则,获取实例的并发数(这里当且仅当 当前时间与上次有效更改连接数的时间间隔在指定范围之内(默认10分钟)),如果超过了时间范围则返回0。循环结束后返回并发数最小的第一个实例。
  4. 最后,如果遍历完所有的服务实例之后,还没有得到Server,则调用其父类的choose()方法,使用RoundRobin算法选择一个实例。

下面我们接着看几个细节点:如何判断服务实例被tripped?如何获取服务实例的并发数?

2、如何判断服务实例被tripped?

在这里插入图片描述
逻辑体现在ServerStatsisCircuitBreakerTripped(long currentTime)方法中:

public boolean isCircuitBreakerTripped(long currentTime) {
    // 获取断路器超时时间点
    long circuitBreakerTimeout = getCircuitBreakerTimeout();
    // 如果断路器超时时间点 <= 0,则直接返回false。
    if (circuitBreakerTimeout <= 0) {
        return false;
    }
    // 如果断路器超时时间点 > 当前时间,则返回true,表示服务实例被tripped了;否则返回false
    return circuitBreakerTimeout > currentTime;
}

方法核心逻辑:

  1. 判断断路器超时时间点是否大于当前时间,如果大于,则表示当前服务实例被tripped了,也就不会再被选择;否者,正常选择。

3、如何获取服务实例的并发数?

在这里插入图片描述
逻辑体现在ServerStatsgetActiveRequestsCount(long currentTime)方法中:

public int getActiveRequestsCount(long currentTime) {
  // 获取实例当前的并发连接数
    int count = activeRequestsCount.get();
    // 连接数为0,则直接返回0
    if (count == 0) {
        return 0;
    // 如果当前时间与上次有效更改连接数的时间间隔不在指定范围之内(默认10分钟),则并发连接数设置为0,并返回0
    } else if (currentTime - lastActiveRequestsCountChangeTimestamp > activeRequestsCountTimeout.get() * 1000 || count < 0) {
        activeRequestsCount.set(0);
        return 0;            
    } else {
    // 正常场景下返回并发连接数
        return count;
    }
}

AtomicInteger activeRequestsCount = new AtomicInteger(0);

private static final DynamicIntProperty activeRequestsCountTimeout = 
        DynamicPropertyFactory.getInstance().getIntProperty("niws.loadbalancer.serverStats.activeRequestsCount.effectiveWindowSeconds", 60 * 10);

关键点在于实例的并发数是如何维护的?下面我就接着看。

4、实例并发数的维护:

1)增加实例的并发数

在开始执行一个Rest请求时会通过ServerStats#incrementActiveRequestsCount()方法新增一个连接数(activeRequestsCount);
在这里插入图片描述

虽然是在new一个RibbonStatsRecorder时新增的实例并发数,但是RibbonStatsRecorder内部组合的ServerStats来源于Ribbo的上下文RibbonLoadBalancerContext,所以每次new RibbonStatsRecorder时,ServerStats数据是共享的;

在这里插入图片描述

2)减少实例的并发数

当Rest请求调用外部服务执行完毕之后,会通过ServerStats#decrementActiveRequestsCount()方法减少一个连接数(activeRequestsCount):
在这里插入图片描述

RibbonStatsRecorder#recordStats(Object entity) 方法如下:

在这里插入图片描述

三、后续文章

下一篇文章我们接着分析Ribbon负载均衡策略之WeightedResponseTimeRule。

### 启用 Spring Cloud Ribbon 进行负载均衡的配置 在微服务架构中,Ribbon 是一种常用的客户端负载均衡工具。通过它,可以实现对多个服务实例之间的请求分发。以下是关于如何在微服务项目中启用并配置 Ribbon 的详细说明。 #### 1. 添加依赖项 为了使用 Ribbon,在项目的 `pom.xml` 文件中需要引入必要的依赖项: ```xml <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency> ``` 此依赖会自动集成 Ribbon 和其他 Spring Cloud 组件的功能[^2]。 #### 2. 创建带有负载均衡能力的 RestTemplate 实例 要让 `RestTemplate` 支持负载均衡功能,需为其添加 `@LoadBalanced` 注解。具体代码如下所示: ```java import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @Configuration public class AppConfig { @Bean @LoadBalanced public RestTemplate createRestTemplate() { return new RestTemplate(); } } ``` 上述代码片段定义了一个具有负载均衡特性的 `RestTemplate` Bean 对象[^4]。 #### 3. 使用 Eureka Server 注册服务实例 假设存在两个提供者服务实例分别运行于不同的端口上(如 `http://localhost:8081` 和 `http://localhost:8082`),这些实例均已在 Eureka Server 中完成注册操作[^3]。 #### 4. 编写控制器类调用远程服务 下面展示的是一个简单的控制层方法,用于向名为 “provider-service” 的服务发起 HTTP 请求: ```java @RestController @RequestMapping("/consumer") public class ConsumerController { private final RestTemplate restTemplate; public ConsumerController(RestTemplate restTemplate) { this.restTemplate = restTemplate; } @GetMapping("/greeting") public String getGreetingFromProviderService() { return restTemplate.getForObject("http://provider-service/greeting", String.class); } } ``` 当访问 `/consumer/greeting` 接口时,该应用将会随机选取其中一个已注册的服务节点来执行实际的数据获取任务。 --- ### 总结 以上便是基于 Spring Cloud 微服务体系下利用 Ribbon 来达成客户端层面负载均衡效果的整体流程介绍。从 Maven 工程构建起步至最终业务逻辑编码完毕为止涵盖了全部必要环节。
评论 49
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秃秃爱健身

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值