前言
Spring Cloud Ribbon是一个基于 HTTP 和 TCP 的客户端负载均衡工具。通过 Spring Cloud 封装,我们可以将面向服务的REST目标请求自动转换成客户端负载均衡的服务调用。IRule 是Ribbon 中负载均衡器服务选择策略要实现的接口,我们可以看一下它的类图:
上图涵盖了 Ribbon 所有的负载均衡策略,我们看一下IRule的源码:
public interface IRule{
public Server choose(Object key);
public void setLoadBalancer(ILoadBalancer lb);
public ILoadBalancer getLoadBalancer();
}
上述方法中最核心的便是choose方法,子类重写该方法以实现不同的负载均衡策略。再看一下AbstractLoadBalancerRule类,此类为抽象类,类中定义了负载均衡器对象,负载均衡策略都是以此负载均衡器中维护的信息为依据。
public abstract class AbstractLoadBalancerRule implements IRule, IClientConfigAware {
private ILoadBalancer lb;
@Override
public void setLoadBalancer(ILoadBalancer lb){
this.lb = lb;
}
@Override
public ILoadBalancer getLoadBalancer(){
return lb;
}
}
RandomRule
RandomRule策略实现从服务实例清单中随机选择一个服务实例的功能,重点看一下该类的choose方法,
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
}
Server server = null;
while (server == null) {
if (Thread.interrupted()) {
return null;
}
List<Server> upList = lb.getReachableServers();
List<Server> allList = lb.getAllServers();
int serverCount = allList.size();
if (serverCount == 0) {
return null;
}
int index = rand.nextInt(serverCount);
server = upList.get(index);
if (server == null) {
Thread.yield();
continue;
}
if (server.isAlive()) {
return (server);
}
server = null;
Thread.yield();
}
return server;
}
该策略使用负载均衡器来获取可用实例列表 upList 和全量实例列表 allList,通过生成一个不大于服务实例总数量的随机值,并将其作为索引返回具体实例。
RoundRobinRule
RoundRobinRule按照线性轮询的方式依次选择每个服务实例,看一下choose方法,
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
}
Server server = null;
int count = 0;
while (server == null && count++ < 10) {
List<Server> reachableServers = lb.getReachableServers();
List<Server> allServers = lb.getAllServers();
int upCount = reachableServers.size();
int serverCount = allServers.size();
if ((upCount == 0) || (serverCount == 0)) {
log.warn("No up servers available from load balancer: " + lb);
return null;
}
int nextServerIndex = incrementAndGetModulo(serverCount);
server = allServers.get(nextServerIndex);
if (server == null) {
/* Transient. */
Thread.yield();
continue;
}
if (server.isAlive() && (server.isReadyToServe())) {
return (server);
}
server = null;
}
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: "
+ lb);
}
return server;
}
可以看到,RoundRobinRule的具体实现跟RandomRule类似,在循环条件中增加了一个名为count的计数器,如果选择不到server超过10次,便会结束尝试并打印告警信息。
WeightedResponseTimeRule
WeightedResponseTimeRule继承自RoundRobinRule,在选择服务实例的时候把权重因素也考虑进去。WeightedResponseTimeRule在初始化的时候会启动一个定时任务用来计算每个服务实例的权重,默认间隔为30秒。
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
}
Server server = null;
while (server == null) {
// get hold of the current reference in case it is changed from the other thread
List<Double> currentWeights = accumulatedWeights;
if (Thread.interrupted()) {
return null;
}
List<Server> allList = lb.getAllServers();
int serverCount = allList.size();
if (serverCount == 0) {
return null;
}
int serverIndex = 0;
// last one in the list is the sum of all weights
double maxTotalWeight = currentWeights.size() == 0 ? 0 : currentWeights.get(currentWeights.size() - 1);
// No server has been hit yet and total weight is not initialized
// fallback to use round robin
if (maxTotalWeight < 0.001d) {
server = super.choose(getLoadBalancer(), key);
if(server == null) {
return server;
}
} else {
// generate a random weight between 0 (inclusive) to maxTotalWeight (exclusive)
double randomWeight = random.nextDouble() * maxTotalWeight;
// pick the server index based on the randomIndex
int n = 0;
for (Double d : currentWeights) {
if (d >= randomWeight) {
serverIndex = n;
break;
} else {
n++;
}
}
server = allList.get(serverIndex);
}
if (server == null) {
/* Transient. */
Thread.yield();
continue;
}
if (server.isAlive()) {
return (server);
}
// Next.
server = null;
}
return server;
}
该策略累加每个服务的权重值获得权重之和,并生成一个不大于权重之和的随机值,根据随机值所属的权重区间索引,返回相应的服务实例。RoundRobinRule、WeightedResponseTimeRule两种策略与Dubbo中的随机策略相似,读者可以自行比较一番。
RetryRule
顾名思义,RetryRule实现了一个具备重试机制的实例选择功能,看一下具体实现,
public class RetryRule extends AbstractLoadBalancerRule {
IRule subRule = new RoundRobinRule();
long maxRetryMillis = 500;
……
public Server choose(ILoadBalancer lb, Object key) {
long requestTime = System.currentTimeMillis();
long deadline = requestTime + maxRetryMillis;
Server answer = null;
answer = subRule.choose(key);
if (((answer == null) || (!answer.isAlive()))
&& (System.currentTimeMillis() < deadline)) {
InterruptTask task = new InterruptTask(deadline
- System.currentTimeMillis());
while (!Thread.interrupted()) {
answer = subRule.choose(key);
if (((answer == null) || (!answer.isAlive()))
&& (System.currentTimeMillis() < deadline)) {
/* pause and retry hoping it's transient */
Thread.yield();
} else {
break;
}
}
task.cancel();
}
if ((answer == null) || (!answer.isAlive())) {
return null;
} else {
return answer;
}
}
……
}
RetryRule内部定义了一个IRule对象,默认使用RoundRobinRule实例,并且在choose方法内部反复尝试内部定义的策略。如果在设置的尝试结束时间阈值内选择不到服务实例就返回null。
ClientConfigEnabledRoundRobinRule
ClientConfigEnabledRoundRobinRule 策略比较特殊,一般不直接用它。看一下它的源码,
public class ClientConfigEnabledRoundRobinRule extends AbstractLoadBalancerRule {
RoundRobinRule roundRobinRule = new RoundRobinRule();
……
@Override
public Server choose(Object key) {
if (roundRobinRule != null) {
return roundRobinRule.choose(key);
} else {
throw new IllegalArgumentException(
"This class has not been initialized with the RoundRobinRule class");
}
}
}
ClientConfigEnabledRoundRobinRule 内部定义了一个RoundRobinRule 策略,而choose方法也是直接使用RoundRobinRule的线性轮询机制,这是为何呢?
其实从文章开头的类图中可以看到,有些策略继承自ClientConfigEnabledRoundRobinRule,父类的choose方法可以在子类无法获取服务实例的时候作为一种备选方案。后文介绍的策略均为ClientConfigEnabledRoundRobinRule的扩展。
BestAvailableRule
顾名思义,此策略返回的是“”最可用“”的服务实例。如何定义“最可用”呢?其实就是当前并发请求数量最少的可用服务实例,换句话说,也就是“最闲的”实例。
public class BestAvailableRule extends ClientConfigEnabledRoundRobinRule {
private LoadBalancerStats loadBalancerStats;
@Override
public Server choose(Object key) {
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);
if (!serverStats.isCircuitBreakerTripped(currentTime)) {
int concurrentConnections = serverStats.getActiveRequestsCount(currentTime);
if (concurrentConnections < minimalConcurrentConnections) {
minimalConcurrentConnections = concurrentConnections;
chosen = server;
}
}
}
if (chosen == null) {
return super.choose(key);
} else {
return chosen;
}
}
……
}
BestAvailableRule 通过遍历负载均衡器中维护的所有服务实例,过滤掉故障的实例,找出并发请求最小的一个并返回。如果因为某些因素无法找到,则采用父类的线性轮询策略。
PredicateBasedRule
这是一个基于Predicate实现的抽象策略。Predicate 是 Google Guava Collection 工具对集合进行过滤的条件接口。
public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule {
public abstract AbstractServerPredicate getPredicate();
@Override
public Server choose(Object key) {
ILoadBalancer lb = getLoadBalancer();
Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
if (server.isPresent()) {
return server.get();
} else {
return null;
}
}
}
接下来的两个策略是基于此抽象策略实现的,只是他们使用了不同Predicate 实现来完成过滤逻辑达到不同的实例选择效果。
AvailabilityFilteringRule
AvailabilityFilteringRule 继承自 PredicateBasedRule,基本处理逻辑也是“先过滤清单、再轮询选择”。
public class AvailabilityFilteringRule extends PredicateBasedRule {
private AbstractServerPredicate predicate;
@Override
public Server choose(Object key) {
int count = 0;
Server server = roundRobinRule.choose(key);
while (count++ <= 10) {
if (predicate.apply(new PredicateKey(server))) {
return server;
}
server = roundRobinRule.choose(key);
}
return super.choose(key);
}
}
过滤条件使用了 AvailabilityPredicate,看一下 AvailabilityPredicate 的apply方法,
public boolean apply(@Nullable PredicateKey input) {
LoadBalancerStats stats = getLBStats();
if (stats == null) {
return true;
}
return !shouldSkipServer(stats.getSingleServerStat(input.getServer()));
}
private boolean shouldSkipServer(ServerStats stats) {
if ((CIRCUIT_BREAKER_FILTERING.get() && stats.isCircuitBreakerTripped())
|| stats.getActiveRequestsCount() >= activeConnectionsLimit.get()) {
return true;
}
return false;
}
源码中过滤逻辑位于shouldSkipServer方法中,条件为是否故障和实例的并发请求数大于阈值,二者满足其一便返回true。
ZoneAvoidanceRule
ZoneAvoidanceRule 采用了CompositePredicate来进行服务实例清单的过滤,这是一个组合条件,以 ZoneAvoidancePredicate 为主过滤条件,AvailabilityPredicate 为次过滤条件。
public List<Server> getEligibleServers(List<Server> servers, Object loadBalancerKey) {
List<Server> result = super.getEligibleServers(servers, loadBalancerKey);
Iterator<AbstractServerPredicate> i = fallbacks.iterator();
while (!(result.size() >= minimalFilteredServers && result.size() > (int) (servers.size() * minimalFilteredPercentage))
&& i.hasNext()) {
AbstractServerPredicate predicate = i.next();
result = predicate.getEligibleServers(servers, loadBalancerKey);
}
return result;
}
过滤逻辑需要判断两个条件:过滤后的实例总数不小于最小过滤实例数、过滤后的实例比例大于最小过滤百分比,任何一个条件不满足,则将当前结果返回以供线性轮询算法选择。