前言
通过上篇文章对于SpringCloud Ribbon 负载均衡器的解读,我们已经对 Ribbon 实现负载均衡器以及其中包含的服务实例过滤器、服务实例信息存储对象、区域的信息快照等有了深入的认识和理解,接下来我们来看下负载均衡的几个策略实现。
正文
Ribbon中的负载均衡选择策略通过实现IRule
接口来实现。具体关系如下图:
下面我们来看下各种负载均衡策略。
AbstractLoadBalancerRule
负载均衡策略的抽象类,在该抽象类中定义了负载均衡器ILoadBalancer
对象,该对象能够在具体实现选择服务策略时,获取到一些负载均衡中维护的信息来作为分配依据,并以此设计一些算法来实现针对特定场景的高效策略。
1 | public abstract class AbstractLoadBalancerRule implements IRule, IClientConfigAware { |
RandomRule
该策略实现了从服务实例清单中随机选择一个服务实例的功能。其具体实现如下:
主要由choose
函数完成,委托给函数choose(ILoadBalancer lb, Object key)
来实现。
- 获取可用实例列表
upList
和所有实例列表allList
; - 获取一个随机数,通过
chooseRandomInt(serverCount)
函数; - 将该随机数作为
upList
的索引值来返回具体实例; - 选择逻辑处于一个循环中,正常情况下,每次都应该选出一个具体实例,如果出现死循环获取不到服务实例的情况,则可能出现一些问题。
1 | public Server choose(ILoadBalancer lb, Object key) { |
RoundRobinRule
该策略实现了按照线性轮询的方式一次选择每个服务实例的功能。其具体实现逻辑如下:
- 获取可用实例列表
reachableServers
和所有实例列表allServers
,并记录它们的数量upCount
、serverCount
; - 获取下一个可用服务的索引,主要通过
incrementAndGetModulo
函数实现; - 选择逻辑处于循环中,与
RandomRule
不同的是,如果一直选不到server
超过10次,该循环终止,打印警告日志并返回null。
1 | public Server choose(ILoadBalancer lb, Object key) { |
RetryRule
该策略实现了一个具备重试机制的实例选择功能。其具体实现逻辑如下:
- 内部定义一个
IRule
对象,默认使用RoundRobinRule
实例; choose
函数中实现了对内部策略进行反复尝试的策略;- 若期间能够选择到具体实例就返回,若选择不到就根据设置的尝试结束时间为阈值
maxRetryMillis
,当超过阈值后就返回null。
1 | public class RetryRule extends AbstractLoadBalancerRule { |
WeightedResponseTimeRule
该策略是对RoundRobinRule
的扩展,增加了根据实例的运行情况来计算权重,并根据权重来挑选实例,以达到更优的分配效果。
其主要构成如下:
定时任务
WeightedResponseTimeRule
策略在初始化的时候会通过serverWeightTimer.schedule
启动一个定时任务,用来为每个服务实例计算权重,该任务默认30s执行一次。
1 | //.... |
权重计算
在源码中我们可以找到用于存储权重的对象 List
,该List
中每个权重值所处的位置对应了负载均衡器维护的实例清单中所有实例所在清单中的位置。
维护实例权重的计算过程通过maintainWeights
函数实现,其代码如下:
1 | public void maintainWeights() { |
该函数实现内容如下:
- 根据
LoadBalancerStats
中记录的每个实例的统计信息,累加所有实例的平均响应时间,得到总平均响应时间totalResponseTime
,该值会用于后续计算。 - 为负载均衡器中维护的实例清单逐个计算权重(从第一个开始),计算规则为
weightSoFar+totalResponseTime-实例平均响应时间
,其中weightSoFar
初始化为0,并且每计算好一个权重需要累加到weightSoFar
上供下次计算使用。
如下例子:
假设4个实例A、B、C、D,它们平均响应时间为10、40、80、100,所以总响应时间为10+40+80+100=230,根据上面,可以计算出实例A、B、C、D的权重:
- A:230 - 10 = 220
- B:220 + (230 - 40) = 410
- C:410 + (230 - 80) = 560
- D:560 + (230 - 100) = 690
需要注意的是,这里的权重值只是表示了各实例权重区间上限,并非实例优先级。实例A、B、C、D的权重区间如下:
- A:[0,220]
- B:(220,410]
- C:(410,560]
- D:(560,690)
可以看到,每个区间的宽度就是:总平均响应时间-实例的平均响应时间,所有实例的平均响应时间越短、权重区间的宽度越大,宽度越大被选中的概率就越高。
我们再来看下区间边界的开闭是如何确定的。
实例选择
我们来看下该策略的实例选择算法相关代码。
1 | public Server choose(ILoadBalancer lb, Object key) { |
其代码逻辑主要如下:
- 生成一个 [0,最大权重值) 区间内的随机数。
- 遍历权重列表,比较权重值与随机数的大小,如果权重值大于等于随机数,就拿当前权重列表的索引值去服务实例列表中获取具体的实例。因此每个权重区间为 (x,y] 的形式,由于随机数的最小值可以为0,所以第一个实例的下限是闭区间,随机数最大值取不到权重最大值,所以最后一个实例上限是开区间。
按照上面的例子,如果随机数为230,则该值位于第二区间,所以此时就会选择实例B进行请求。
ClientConfigEnabledRoundRobinRule
该策略较为特殊,我们一般不直接使用它。因为它本身没有实现任何特殊的处理逻辑,如代码所示,在它内部定义了一个RoundRobinRule
策略,而choose
函数的实现也正是使用了RoundRobinRule
的线下轮询机制。
虽然我们不会直接使用该策略,但是通过继承该策略,默认的choose
就实现了线性轮询机制,在子类中做一些高级策略时通常可能存在一些无法实施的情况,那么就可以使用父类的实现作为备选。
1 | public class ClientConfigEnabledRoundRobinRule extends AbstractLoadBalancerRule { |
BestAvailableRule
该策略继承自ClientConfigEnabledRoundRobinRule
,同时在实现中注入了负载均衡器统计对象LoadBalancerStats
,算法通过利用统计对象中保存的实例信息来选择满足要求的实例。
通过代码我们可以看到,它通过遍历负载均衡器中维护的所有实例,会过滤掉故障实例,并找出并发请求数最小的一个,所以该策略的特性是可以选出最空闲的实例。
当LoadBalancerStats
对象为空时,会使用父类的RoundRobinRule
策略。
1 | public class BestAvailableRule extends ClientConfigEnabledRoundRobinRule { |
PredicateBasedRule
这是一个抽象策略,它也继承自ClientConfigEnabledRoundRobinRule
,其基础逻辑如下:
先通过子类中实现的Predicate
逻辑来过滤一部分服务实例,然后再以线性轮询的方式从过滤后的实例清单中选出一个。
1 | public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule { |
它是通过 Google Guava Collection 工具对集合进行过滤的接口Predicate
来实现的。我们来看下AbstractServerPredicate
的部分关键逻辑。
1 | public abstract class AbstractServerPredicate implements Predicate<PredicateKey> { |
这个抽象策略只是提供一个实现过滤清单的模板,具体实现需要其子类去完成(实现Predicate
接口的apply
方法),过滤清单后得到符合条件的实例,轮询选择。
1 |
|
AvailabilityFilteringRule
该策略继承自PredicateBasedRule
,其过滤条件使用了AvailabilityPredicate
。
AvailabilityPredicate
的关键代码如下:
1 | public class AvailabilityPredicate extends AbstractServerPredicate { |
从上面代码,我们可以看到该策略的主要过滤逻辑:
- 是否故障,即断路器是否生效已断开。
- 实例的并发请求数大于阈值,默认
Integer.MAX_VALUE
,该配置可通过参数. .ActiveConnectionsLimit 来修改。
这两项只要满足一个就返回false,代表节点可能故障或者负载过高,不适用处理请求,会被过滤掉,都不满足返回true,表示该节点可被选择用于处理请求。
除了上面的过滤方法,该策略的choose
函数也做了一些改进优化,如下:
1 | public class AvailabilityFilteringRule extends PredicateBasedRule { |
可以看到,choose
函数的实现逻辑并不像父类那样,先遍历所有节点进行过滤,然后在过滤后的集合中选择实例。
而是先以线性的方式选择一个实例,接着用过滤条件判断该实例是否满足要求,满足直接使用该实例,不满足选择下一个实例,并进行检查,如此循环进行,如果这个过程重复了10次还是没有找到符合要求的实例,就采用父类的实现方案。
该策略通过线性抽样的方式直接尝试寻找可用且较空闲的实例来使用,优化了父类每次都要遍历所有实例的开销。
ZoneAvoidanceRule
该策略也是PredicateBasedRule
的实现类。可以看到它使用了CompositePredicate
来进行服务清单过滤。这是一个组合过滤条件,在其构造函数中,它以ZoneAvoidancePredicate
为主过滤条件,AvailabilityPredicate
为次过滤条件来进行过滤。
1 | public class ZoneAvoidanceRule extends PredicateBasedRule { |
ZoneAvoidanceRule
在实现的时候并没有像AvailabilityFilteringRule
那样重写choose
函数来优化,所以它和父类一样,先过滤清单,再轮询选择。
过滤条件就是上面提到的两个组合条件,我们先来看下CompositePredicate
的部分源码。
1 | public class CompositePredicate extends AbstractServerPredicate { |
由上面源码,可以看到CompositePredicate
是可以支持多个过滤条件的,它们存储在fallbacks
的List里。
我们指定传入的过滤条件参数顺序就是过滤条件的优先级,因为它们放入List后是有序的。
我们主要来看下getEligibleServers
的逻辑:
- 使用主过滤条件对所有实例过滤并返回过滤后的实例清单。
- 依次使用次过滤条件列表中的过滤条件对上面的过滤结果进行过滤。
- 每次过滤后(包括主过滤条件和次过滤条件),都需要判断下面两个条件,只要有一个不符合就不再进行过滤,将当前结果返回供线性轮询算法选择:
- 过滤后的实例总数 >= 最小过滤实例数(minimalFilteredServers,默认为1).
- 过滤后的实例比例 > 最小过滤百分比(minimalFilteredPercentage,默认为0).
对于传入的两个过滤条件,AvailabilityPredicate
我们上面有介绍,我们来看下ZoneAvoidancePredicate
。
其主要逻辑部分如下:
1 | public class ZoneAvoidancePredicate extends AbstractServerPredicate { |
代码逻辑:
niws.loadbalancer.zoneAvoidanceRule.enabled
配置参数是否开启,如果为false,该过滤条件直接返回true。- 拿到实例的
Zone
,如果为空,该过滤条件直接返回true。 - 拿到实例的
LoadBalancerStats
,如果为空或者可用Zone
数量小于等于1,该过滤条件直接返回true。 - 通过
ZoneAvoidanceRule.createSnapshot
函数拿到Zone
映射,如果该映射里不包含该实例的Zone
,该过滤条件直接返回true。 - 否则通过
ZoneAvoidanceRule.getAvailableZones
拿到可用Zone
列表,如果列表不为空,返回是否包含该实例的Zone
结果;如果为空,直接返回false。
总结
以上就是关于Spring Cloud Ribbon 负载均衡策略的全部内容,通过了解Ribbon的负载均衡策略,可以使我们更好的了解到Ribbon的一些特性,对Ribbon有更深入的了解。