前言

ribbon 作为一个负载均衡组件,个人认为,其核心的功能就是提供多种负载均衡策略。

暂停思考一下,如果让你自己写负载均衡组件,要如何做?

  1. 获取有多少合适的服务可供负载
  2. 服务有时候可能会不可用,那么需要更新维护服务,及时将不合理的服务剔除
  3. 提供策略判断服务是否可用
  4. 实现多种负载算法,供用户选择
  5. 负载失败后的重试

实际上, ribbon 的核心功能,也大概就是上面我说的这些。

理论

1、获取有多少合适的服务可供负载

当今的应用,大多数部署在云上,并向全国各地提供服务。假设服务 A 在福建、北京均有部署。当在北京的服务器请求了服务 A,最合理的情况,应该要由北京的服务器响应。

其实,这里引入了 ribbon 的一个核心概念。区域亲和性
说人话就是,优先选择同一个区域的服务(除非同一区域的服务都挂了)

ribbon 区域亲和性.png
(注:eureka 可以配置 region 和 zone )

关于 eureka 分区配置,请参考文章 eureka分区的深入讲解

2、服务维护

解决了第一个问题。接下来,要考虑如何维护服务列表?
最简单的方式,其实就是直接写个定时器,每隔 30s。重新拉取服务列表,维护最新的可用服务列表。

ribbon 的实现就是这样。 ribbon 中,由 PollingServerListUpdater 每隔 30s 更新服务列表。

ribbon 维护服务列表流程.png

3、服务是否可用判断

有了服务列表,还需要,判断服务可不可用,及时剔除不可用的服务。那么最简单的方式,就是直接 ping 该服务,看是否正常返回。但是,要考虑一个问题,如果你的服务非常多,每次检查的时候,都需要 ping 服务,那么这样会不会太损耗性能了?

ribbon 中,有 ping 的抽象:IPing ,而且还有 ping 策略的抽象:IPingStrategy

在 SerialPingStrategy(默认的 IPingStrategy 实现)实现中, ribbon 就考虑到了,我们上述说的问题。因此,实际上, ribbon 在判断服务是否可用,并非真正的 ping 服务,而是仅用服务的 up 的状态判断服务是否可用。

4、负载均衡算法

服务列表维护好了,接下来就是实现负载均衡算法了。我们自己实现负载均衡算法时,算法肯定是会有非常多的。那么肯定要先抽象出一个 IRule 接口, 定义如何选择一个服务

ribbon中,将负载均衡算法抽象为 IRule。 有以下算法

  1. RoundRobinRule
    轮询策略。Ribbon 默认采用的策略。若经过一轮轮询没有找到可用的 provider,其最多 轮询 10 轮。若最终还没有找到,则返回 null。
  2. RandomRule
    随机策略,从所有可用的 provider 中随机选择一个。
  3. RetryRule
    重试策略。先按照 RoundRobinRule 策略获取 provider,若获取失败,则在指定的时限内 重试。默认的时限为 500 毫秒
  4. BestAvailableRule
    最可用策略。选择并发量最小的 provider,即连接的消费者数量最少的 provider。
  5. AvailabilityFilteringRule
    可用过滤算法。该算法规则是:过滤掉处于服务熔断状态的 provider,或已经超过连接 极限的 provider,对剩余 provider 采用轮询策略。
  6. ZoneAvoidanceRule
    区域回避策略。综合 provider 所在区域的性能及 provider 的可用性,对服务器进行选择。
  7. WeightedResponseTimeRule
    “权重响应时间”策略。根据每个 provider 的平均响应时间计算其权重,响应时间越快 权重越大,被选中的机率就越高(并非是权重最大的一定被选定)。在刚启动时采用轮询策 略。后面就会根据权重进行选择了。

ribbon 默认使用的算法为 RoundRobinRule (实际上是多种 IRule 一起协调工作,过程较复杂)

流程详情参考文章 ribbon 负载均衡

配置负载均衡算法

clientAppName.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.WeightedResponseTimeRule

5、失败重试

负载均衡算法实现完毕了,但是调用过程中,难免接口会出现错误。接口出错,有的接口可能就需要重试。
接口要重试,需要考虑一个关键的事情:接口支持幂等吗?
GET 天生支持幂等操作。因此,毫无疑问,Get 请求出错,正常情况,都是允许重试的。
那么允许一个服务最多重试几次,允许在几个服务之间重试?这都是在实现时需要考虑的。

ribbon 提供的失败重试机制,功能较弱。默认是 GET 请求超时,才会触发重试机制。ribbon 默认不进行重试。

# 在这个服务的最大重试次数,不包含第一次调用 Max number of retries on the same server (excluding the first try)
ribbon.MaxAutoRetries=1

# 最大在几个服务之间重试,不包含第一次 Max number of next servers to retry (excluding the first server)
ribbon.MaxAutoRetriesNextServer=0

注:如果要使用重试功能,建议使用更强大的 spring-retry

理论总结

  1. ribbon 具有区域亲和性
  2. 使用 PollingServerListUpdater 每隔 30s 定时更新服务列表
  3. IPing 判断服务是否可用,默认使用 SerialPingStrategy 实现 ping 策略。该策略在 ‘ping’ 时,仅判断服务的 up 状态
  4. IRule 负载均衡算法
  5. ribbon 功能较弱的重试机制(建议用 spring-retry)

更多配置项,请参考类 DefaultClientConfigImpl 或者参考 ribbon 文档

实际运用

Q:如何单独配置某个服务提供者的 ribbon
A:

clientAppName.ribbon.ReadTimeout=3000
clientAppName.ribbon.ConnectTimeout=3000

Q:为什么有人在单独使用 ribbon 的时候,设置 ReadTimeout, ConnectTimeout 无效。正确的使用方式是什么?
A:
需要配置

ribbon.http.client.enabled=true

才会使得 ribbon 的ReadTimeout、ConnectTimeout 生效。需要注意的是,以上配置,springcloud 不推荐,已被标识为过时的。

个人认为比较合理的做法如下:

@Bean
@LoadBalanced
public RestTemplate ribbonTest() {
    HttpComponentsClientHttpRequestFactory httpRequestFactory = new HttpComponentsClientHttpRequestFactory();
    httpRequestFactory.setConnectTimeout(500);
    httpRequestFactory.setReadTimeout(500);
    return new RestTemplate(httpRequestFactory);
}

Q:ribbon 有提供失败重试,为什么配置了,并没有用?
A:ribbon 的重试配置,大概如下

# 在这个服务的最大重试次数,不包含第一次调用 Max number of retries on the same server (excluding the first try)
ribbon.MaxAutoRetries=1

# 最大在几个服务之间重试,不包含第一次 Max number of next servers to retry (excluding the first server)
ribbon.MaxAutoRetriesNextServer=0
  1. ribbon 提供的重试机制,一般是在 GET 请求连接超时,或者读超时,会触发重试机制。其他的异常并不会触发异常机制。
  2. 正常使用时,不会使用 ribbon 提供的重试机制,而是会引入spring-retry,使用它提供的重试机制。

因此,最正确的做法是引入 spring-retry,使用更强大的重试机制。

 <dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

参考文档

  1. ribbon 官方介绍
  2. eureka分区的深入讲解
  3. ribbon 负载均衡器
  4. ribbon 状态统计

心无私天地宽
513 声望22 粉丝