前言
ribbon 作为一个负载均衡组件,个人认为,其核心的功能就是提供多种负载均衡策略。
暂停思考一下,如果让你自己写负载均衡组件,要如何做?
- 获取有多少合适的服务可供负载
- 服务有时候可能会不可用,那么需要更新维护服务,及时将不合理的服务剔除
- 提供策略判断服务是否可用
- 实现多种负载算法,供用户选择
- 负载失败后的重试
实际上, ribbon 的核心功能,也大概就是上面我说的这些。
理论
1、获取有多少合适的服务可供负载
当今的应用,大多数部署在云上,并向全国各地提供服务。假设服务 A 在福建、北京均有部署。当在北京的服务器请求了服务 A,最合理的情况,应该要由北京的服务器响应。
其实,这里引入了 ribbon 的一个核心概念。区域亲和性
说人话就是,优先选择同一个区域的服务(除非同一区域的服务都挂了)
(注:eureka 可以配置 region 和 zone)
关于 eureka 分区配置,请参考文章 eureka 分区的深入讲解
2、服务维护
解决了第一个问题。接下来,要考虑如何维护服务列表?
最简单的方式,其实就是直接写个定时器,每隔 30s。重新拉取服务列表,维护最新的可用服务列表。
ribbon 的实现就是这样。ribbon 中,由 PollingServerListUpdater 每隔 30s 更新服务列表。
3、服务是否可用判断
有了服务列表,还需要,判断服务可不可用,及时剔除不可用的服务。那么最简单的方式,就是直接 ping 该服务,看是否正常返回。但是,要考虑一个问题,如果你的服务非常多,每次检查的时候,都需要 ping 服务,那么这样会不会太损耗性能了?
ribbon 中,有 ping 的抽象:IPing,而且还有 ping 策略的抽象:IPingStrategy。
在 SerialPingStrategy(默认的 IPingStrategy 实现)实现中,ribbon 就考虑到了,我们上述说的问题。因此,实际上, ribbon 在判断服务是否可用,并非真正的 ping 服务,而是仅用服务的 up 的状态判断服务是否可用。
4、负载均衡算法
服务列表维护好了,接下来就是实现负载均衡算法了。我们自己实现负载均衡算法时,算法肯定是会有非常多的。那么肯定要先抽象出一个 IRule 接口,定义如何选择一个服务。
ribbon 中,将负载均衡算法抽象为 IRule。有以下算法
- RoundRobinRule
轮询策略。Ribbon 默认采用的策略。若经过一轮轮询没有找到可用的 provider,其最多 轮询 10 轮。若最终还没有找到,则返回 null。 - RandomRule
随机策略,从所有可用的 provider 中随机选择一个。 - RetryRule
重试策略。先按照 RoundRobinRule 策略获取 provider,若获取失败,则在指定的时限内 重试。默认的时限为 500 毫秒 - BestAvailableRule
最可用策略。选择并发量最小的 provider,即连接的消费者数量最少的 provider。 - AvailabilityFilteringRule
可用过滤算法。该算法规则是: 过滤掉处于服务熔断状态的 provider,或已经超过连接 极限的 provider,对剩余 provider 采用轮询策略。 - ZoneAvoidanceRule
区域回避策略。综合 provider 所在区域的性能及 provider 的可用性,对服务器进行选择。 - 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
理论总结
- ribbon 具有区域亲和性
- 使用 PollingServerListUpdater 每隔 30s 定时更新服务列表
- IPing 判断服务是否可用,默认使用 SerialPingStrategy 实现 ping 策略。该策略在‘ping’时,仅判断服务的 up 状态
- IRule 负载均衡算法
- 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
- ribbon 提供的重试机制,一般是在 GET 请求连接超时,或者读超时,会触发重试机制。其他的异常并不会触发异常机制。
- 正常使用时,不会使用 ribbon 提供的重试机制,而是会引入 spring-retry,使用它提供的重试机制。
因此,最正确的做法是引入 spring-retry,使用更强大的重试机制。
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
参考文档
- ribbon 官方介绍
- eureka 分区的深入讲解
- ribbon 负载均衡器
- ribbon 状态统计