Spring Cloud源码分析(二)Ribbon(续)

}

WeightedResponseTimeRule

该策略是对RoundRobinRule的扩展,增加了根据实例的运行情况来计算权重,并根据权重来挑选实例,以达到更优的分配效果,它的实现主要有三个核心内容:

定时任务

WeightedResponseTimeRule策略在初始化的时候会通过serverWeightTimer.schedule(new DynamicServerWeightTask(), 0, serverWeightTaskTimerInterval)启动一个定时任务,用来为每个服务实例计算权重,该任务默认30秒执行一次。

class DynamicServerWeightTask extends TimerTask {

public void run() {

ServerWeight serverWeight = new ServerWeight();

try {

serverWeight.maintainWeights();

} catch (Throwable t) {

logger.error("Throwable caught while running DynamicServerWeightTask for " + name, t);

}

}

}

权重计算

在源码中我们可以轻松找到用于存储权重的对象:List<Double> accumulatedWeights = new ArrayList<Double>(),该List中每个权重值所处的位置对应了负载均衡器维护的服务实例清单中所有实例在清单中的位置。

维护实例权重的计算过程通过maintainWeights函数实现,具体如下源码所示:

public void maintainWeights() {

ILoadBalancer lb = getLoadBalancer();

try {

logger.info(“Weight adjusting job started”);

AbstractLoadBalancer nlb = (AbstractLoadBalancer) lb;

LoadBalancerStats stats = nlb.getLoadBalancerStats();

// 计算所有实例的平均响应时间的总和:totalResponseTime

double totalResponseTime = 0;

for (Server server : nlb.getAllServers()) {

// this will automatically load the stats if not in cache

ServerStats ss = stats.getSingleServerStat(server);

totalResponseTime += ss.getResponseTimeAvg();

}

// 逐个计算每个实例的权重:weightSoFar + totalResponseTime - 实例的平均响应时间

Double weightSoFar = 0.0;

List finalWeights = new ArrayList();

for (Server server : nlb.getAllServers()) {

ServerStats ss = stats.getSingleServerStat(server);

double weight = totalResponseTime - ss.getResponseTimeAvg();

weightSoFar += weight;

finalWeights.add(weightSoFar);

}

setWeights(finalWeights);

} catch (Throwable t) {

logger.error(“Exception while dynamically calculating server weights”, t);

} finally {

serverWeightAssignmentInProgress.set(false);

}

}

该函数的实现主要分为两个步骤:

  • 根据LoadBalancerStats中记录的每个实例的统计信息,累加所有实例的平均响应时间,得到总平均响应时间totalResponseTime,该值会用于后续的计算。

  • 为负载均衡器中维护的实例清单逐个计算权重(从第一个开始),计算规则为:weightSoFar + totalResponseTime - 实例的平均响应时间,其中weightSoFar初始化为零,并且每计算好一个权重需要累加到weightSoFar上供下一次计算使用。totalResponseTime则的上计算结果。

举个简单的例子来理解这个计算过程:假设有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

需要注意的是,这里的权重值只是表示了各实例权重区间的上限,并非某个实例的优先级,所以不是数值越大被选中的概率就越大。那么什么是权重区间呢?以上面例子的计算结果为例,它实际上是为这4个实例构建了4个不同的区间,每个实例的区间下限是上一个实例的区间上限,而每个实例的区间上限则是我们上面计算并存储于List accumulatedWeights中的权重值,其中第一个实例的下限默认为零。所以,根据上面示例的权重计算结果,我们可以得到每个实例的权重区间:

  • 实例A:[0, 220]

  • 实例B:(220, 410]

  • 实例C:(410, 560]

  • 实例D:(560,690)

我们不难发现,实际上每个区间的宽度就是:总的平均响应时间 - 实例的平均响应时间,所以实例的平均响应时间越短、权重区间的宽度越大,而权重区间的宽度越大被选中的概率就越高。可能很多读者会问,这些区间边界的开闭是如何确定的呢?为什么不那么规则?下面我们会通过实例选择算法的解读来解释。

实例选择

WeightedResponseTimeRule选择实例的实现与之前介绍的算法结构类似,下面是它主体的算法(省略了循环体和一些判断等处理):

public Server choose(ILoadBalancer lb, Object key) {

List currentWeights = accumulatedWeights;

List allList = lb.getAllServers();

int serverCount = allList.size();

if (serverCount == 0) {

return null;

}

int serverIndex = 0;

// 获取最后一个实例的权重

double maxTotalWeight = currentWeights.size() == 0 ? 0 : currentWeights.get(currentWeights.size() - 1);

if (maxTotalWeight < 0.001d) {

// 如果最后一个实例的权重值小于0.001,则采用父类实现的线性轮询的策略

server = super.choose(getLoadBalancer(), key);

if(server == null) {

return server;

}

} else {

// 如果最后一个实例的权重值大于等于0.001,就产生一个[0, maxTotalWeight)的随机数

double randomWeight = random.nextDouble() * maxTotalWeight;

int n = 0;

for (Double d : currentWeights) { // 遍历维护的权重清单,若权重大于等于随机得到的数值,就选择这个实例

if (d >= randomWeight) {

serverIndex = n;

break;

} else {

n++;

}

}

server = allList.get(serverIndex);

}

return server;

}

从源码中,我们可以看到,选择实例的核心过程就两步:

  • 生产一个[0, 最大权重值)区间内的随机数。

  • 遍历权重列表,比较权重值与随机数的大小,如果权重值大于等于随机数,就拿当前权重列表的索引值去服务实例列表中获取具体实例。这就是在上一节中提到的服务实例会根据权重区间挑选的原理,而权重区间边界的开闭原则根据算法,正常应该每个区间为(x, y]的形式,但是第一个实例和最后一个实例为什么不同呢?由于随机数的最小取值可以为0,所以第一个实例的下限是闭区间,同时随机数的最大值取不到最大权重值,所以最后一个实例的上限是开区间。

若继续以上面的数据为例,进行服务实例的选择,则该方法会从[0, 690)区间中选出一个随机数,比如选出的随机数为230,由于该值位于第二个区间,所以此时就会选择实例B来进行请求。

ClientConfigEnabledRoundRobinRule

该策略较为特殊,我们一般不直接使用它。因为它本身并没有实现什么特殊的处理逻辑,正如下面的源码所示,在它的内部定义了一个RoundRobinRule策略,而choose函数的实现也正是使用了RoundRobinRule的线性轮询机制,所以它实现的功能实际上与RoundRobinRule相同,那么定义它有什么特殊的用处呢?

虽然我们不会直接使用该策略,但是通过继承该策略,那么默认的choose就实现了线性轮询机制,在子类中做一些高级策略时通常都有可能会存在一些无法实施的情况,那么就可以通过父类的实现作为备选。在后文中我们将继续介绍的高级策略均是基于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”);

}

}

}

BestAvailableRule

该策略继承自ClientConfigEnabledRoundRobinRule,在实现中它注入了负载均衡器的统计对象:LoadBalancerStats,同时在具体的choose算法中利用LoadBalancerStats保存的实例统计信息来选择满足要求的实例。从如下源码中我们可以看到,它通过遍历负载均衡器中维护的所有服务实例,会过滤掉故障的实例,并找出并发请求数最小的一个,所以该策略的特性是选出最空闲的实例。

public Server choose(Object key) {

if (loadBalancerStats == null) {

return super.choose(key);

}

List 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;

}

}

同时,由于该算法的核心依据是统计对象loadBalancerStats,当其为空的时候,该策略是无法执行的。所以从源码中我们可以看到,当loadBalancerStats为空的时候,它会采用父类的线性轮询策略,正如我们在介绍ClientConfigEnabledRoundRobinRule时那样,它的子类在无法满足实现高级策略时候,可以使用线性轮询策略的特性。后面将要介绍的策略因为也都继承自ClientConfigEnabledRoundRobinRule,所以他们都会具有这样的特性。

PredicateBasedRule

这是一个抽象策略,它也继承了ClientConfigEnabledRoundRobinRule,从其命名中可以猜出他是一个基于Predicate实现的策略,Predicate是Google Guava Collection工具对集合进行过滤的条件接口。

如下源码所示,它定义了一个抽象函数getPredicate来获取AbstractServerPredicate对象的实现,而在choose函数中,通过AbstractServerPredicatechooseRoundRobinAfterFiltering函数来选出具体的服务实例。从该函数的命名我们也大致能猜出它的基础逻辑:先通过子类中实现的Predicate逻辑来过滤一部分服务实例,然后再以线性轮询的方式从过滤后的实例清单中选出一个。

public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule {

public abstract AbstractServerPredicate getPredicate();

@Override

public Server choose(Object key) {

ILoadBalancer lb = getLoadBalancer();

Optional server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);

if (server.isPresent()) {

return server.get();

} else {

return null;

}

}

}

通过下面AbstractServerPredicate的源码片段,可以证实我们上面所做的猜测。在上面choose函数中调用的chooseRoundRobinAfterFiltering方法先通过内部定义的getEligibleServers函数来获取备选的实例清单(实现了过滤),如果返回的清单为空,则用Optional.absent()来表示不存在,反之则以线性轮询的方式从备选清单中获取一个实例。

public abstract class AbstractServerPredicate implements Predicate {

public Optional chooseRoundRobinAfterFiltering(List servers, Object loadBalancerKey) {

List eligible = getEligibleServers(servers, loadBalancerKey);

if (eligible.size() == 0) {

return Optional.absent();

}

return Optional.of(eligible.get(nextIndex.getAndIncrement() % eligible.size()));

}

public List getEligibleServers(List servers, Object loadBalancerKey) {

if (loadBalancerKey == null) {

return ImmutableList.copyOf(Iterables.filter(servers, this.getServerOnlyPredicate()));

} else {

List results = Lists.newArrayList();

for (Server server: servers) {

if (this.apply(new PredicateKey(loadBalancerKey, server))) {

results.add(server);

}

}

return results;

}

}

}

在了解了整体逻辑之后,我们来详细看看实现过滤功能的getEligibleServers函数。从源码上看,它的实现结构非常简单清晰,通过遍历服务清单,使用this.apply方法来判断实例是否需要保留,是就添加到结果列表中。

可能到这里,不熟悉Google Guava Collections集合工具的读者会比较困惑,这个applyAbstractServerPredicate中并找不到它的定义,那么它是如何实现过滤的呢?实际上,AbstractServerPredicate实现了com.google.common.base.Predicate接口,而apply方法是该接口中的定义,主要用来实现过滤条件的判断逻辑,它输入的参数则是过滤条件需要用到的一些信息(比如源码中的new PredicateKey(loadBalancerKey, server)),它传入了关于实例的统计信息和负载均衡器的选择算法传递过来的key)。既然在AbstractServerPredicate中我们未能找到apply的实现,所以这里的chooseRoundRobinAfterFiltering函数只是定义了一个模板策略:“先过滤清单,再轮询选择”。对于如何过滤,则需要我们在AbstractServerPredicate的子类去实现apply方法来确定具体的过滤策略了。

后面我们将要介绍的两个策略就是基于此抽象策略实现,只是它们使用了不同的Predicate实现来完成过滤逻辑以达到不同的实例选择效果。

Google Guava Collections是一个对Java Collections Framework增强和扩展的一个开源项目。虽然Java Collections Framework已经能够 满足了我们大多数情况下使用集合的要求,但是当遇到一些特殊的情况我们的代码会比较冗长且容易出错。Guava Collections 可以帮助我们的让集合操作代码更为简短精炼并大大增强代码的可读性。

AvailabilityFilteringRule

该策略继承自上面介绍的抽象策略PredicateBasedRule,所以它也继承了“先过滤清单,再轮询选择”的基本处理逻辑,其中过滤条件使用了AvailabilityPredicate

public class AvailabilityPredicate extends AbstractServerPredicate {

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方法中,它主要判断服务实例的两项内容:

  • 是否故障,即断路器是否生效已断开

  • 实例的并发请求数大于阈值,默认值为 2 31 2^{31} 231 - 1,该配置我们可通过参数..ActiveConnectionsLimit来修改

其中只要有一个满足apply就返回false(代表该节点可能存在故障或负载过高),都不满足就返回true。

在该策略中,除了实现了上面的过滤方法之外,对于choose的策略也做了一些改进优化,所以父类的实现对于它来说只是一个备用选项,其具体实现如下:

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);

《MySql面试专题》

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

《MySql性能优化的21个最佳实践》

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

《MySQL高级知识笔记》

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

文中展示的资料包括:**《MySql思维导图》《MySql核心笔记》《MySql调优笔记》《MySql面试专题》《MySql性能优化的21个最佳实践》《MySq高级知识笔记》**如下图

全网火爆MySql 开源笔记,图文并茂易上手,阿里P8都说好

关注我,点赞本文给更多有需要的人
ca1H-1721879961841)]

《MySQL高级知识笔记》

[外链图片转存中…(img-QYLx7j16-1721879961841)]

[外链图片转存中…(img-9XWdv2VD-1721879961842)]

[外链图片转存中…(img-jTKrgEpK-1721879961842)]

[外链图片转存中…(img-TCNS2rgP-1721879961842)]

[外链图片转存中…(img-OojMBTss-1721879961843)]

[外链图片转存中…(img-9ZWccNac-1721879961843)]

[外链图片转存中…(img-2hV25oVU-1721879961843)]

[外链图片转存中…(img-AcdgHxsc-1721879961844)]

[外链图片转存中…(img-hX0r7vgt-1721879961844)]

[外链图片转存中…(img-AZn6UePR-1721879961844)]

文中展示的资料包括:**《MySql思维导图》《MySql核心笔记》《MySql调优笔记》《MySql面试专题》《MySql性能优化的21个最佳实践》《MySq高级知识笔记》**如下图

[外链图片转存中…(img-KgwaU5C5-1721879961844)]

关注我,点赞本文给更多有需要的人

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值