乐趣区

关于后端:我试图给你分享一种自适应的负载均衡有点打脑壳但是确实也有点厉害

你好呀,我是歪歪。

这篇文章带大家来盘一个有点意思的负载平衡算法:

https://cn.dubbo.apache.org/zh-cn/overview/core-features/load…

自适应负载平衡,尽管这个算法我是在 Dubbo 的源码外面看到的。然而这并不算是 Dubbo 的专属,而是一种算法思维,只不过你能够在 Dubbo 外面找到其对应的 Java 实现。

同样的,在 go-zero 外面,你也能够找到其对应的 Go 语言的实现。

对于这几种负载平衡策略,官网给了两个示意图。

https://cn.dubbo.apache.org/zh-cn/overview/reference/proposal…

当服务提供端的机器配置比拟平衡时,既每台机器的解决能力都差不多,甚至时一样的时候,能够看到 p2c 算法的吞吐量,遥遥领先:

而当服务提供端的机器配置参差不齐的时候,也就是有的机器解决能力牛逼,有的又很拉跨的时候,adaptive 策略就比拟杰出了:

本文次要就带大家盘一下 adaptive 策略。

Demo

首先,还是不要偷懒,咱们搞个 Demo 进去。

在 Dubbo 的官网上,不晓得什么时候冒出来一个 Initializer 模块:

我体验了一下,利用这个搭建 Demo,和以前本人和 SpringBoot 搞交融的形式比起来,就一个字:十分的快!

https://start.dubbo.apache.org/bootstrap.html

对于这个 Initializer 模块的具体介绍,如果感兴趣能够本人去玩玩,我这里就不扩大了。

留神 Dubbo 版本抉择 3.2.0 就行,因为咱们要钻研的自适应负载平衡策略是在这个版本中才开始反对的。

我创立的是一个 Single Module 我的项目,代码下载下来之后,构造是这样的:

一个提供者,一个消费者。消费者继承了 CommandLineRunner 接口,在服务启动实现之后会主动去触发一次服务调用:

所以在我的项目关上,依赖拉取结束之后,啥也不必管,咱们先把我的项目启动起来,看看啥状况:

控制台输入了 Consumer 类中的内容,这样就算是实现了一次 Dubbo 调用,就这么简略。

如果你想要理解 Dubbo 服务的调用过程,那么你基本上就能够用这个 Demo 去进行调试了。

在服务提供者的实现类中打上断点,拿到调用栈,玩去吧:

然而,你有没有感觉到一丝丝奇怪?

咱们甚至都没有启动一个注册核心,就实现了一次 Dubbo 调用?

因为在 Demo 外面应用的是 injvm 协定,也就是本地调用:

https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/a…

所以,咱们还须要对这个 Demo 进行一点革新,本次要调试的局部是负载平衡策略,须要应用近程调用才行。

因而须要在服务援用的中央,加上 scope=”remote” 配置:

而后在配置文件中配置注册核心:

同时在本人本地启动一个 zookeeper。

通过 ZooInspector 工具,能够看到在 20880 端口有一个服务提供者了:

相似的,咱们在 20881 和 20882 再搞一个服务提供者,一共三个。

有同学可能就要问了:为什么至多要三个呢?

如果只有一个服务提供者,也不须要进行负载平衡。

如果只有两个服务提供者,也用不上自适应负载平衡。

为什么?

咱们再看一下对于它的形容。

自适应负载平衡:在 P2C 算法根底上,抉择二者中 load 最小的那个节点

P2C 算法,是要随机抉择两个节点。

如果你只有两个节点,还选个啥啊。

所以,如果咱们要盘一下自适应负载平衡,服务提供方节点当然是越多越好,然而至多也须要 3 个节点。

源码

如果要启用自适应负载平衡算法,须要在服务援用的中央进行指定:

而后在对应的地位打上断点:

org.apache.dubbo.rpc.cluster.loadbalance.AdaptiveLoadBalance#doSelect

进入断点处,既 doSelect 办法的第一行的办法,就是叫做 selectByP2C,主打的就是一个单刀直入。

能够看到这个办法的入参 invokers 的大小就是 3,它就是代表咱们在 20880、20881、20882 端口启动的三个服务提供方。

进入 doSelect 办法,外围逻辑就两局部:

第一局部是这样的:

int pos1 = ThreadLocalRandom.current().nextInt(length);
int pos2 = ThreadLocalRandom.current().nextInt(length - 1);
if (pos2 >= pos1) {pos2 = pos2 + 1;}

你说这是在干啥?

尽管只有简略的四行代码,然而我还是给你缕一缕,因为我总感觉这个中央有 BUG。

首先,在咱们的 Demo 中 length 就是 invokers 的大小,既为 3。

而后 pos1、pos2 代表的是 invokers 这个 List 的下标。

所以,

第一次随机,pos1 就是从 [0,3) 之间的负数。

第二次随机,pos2 就是从 [0,2) 之间的负数。

没问题对吧?

那么问题就来了:如果第一次没有随机出 2,即最初一个下标。那么第二次随机的时候因为执行了减一操作,所以最初一个下标基本就不可能被随机到。

所以,我认为这个随机算法对于 invokers 汇合中的最初一个元素是不偏心的,因为它少了一次参加随机的机会。

而后,它还有这样的两行代码:

if (pos2 >= pos1) {pos2 = pos2 + 1;}

目标是为了解决当 pos2 和 pos1 随机出一样的值的时候,把 pos2 进行加一解决。

首先,pos2 和 pos1 随机出一样的值,这个是齐全有可能的。

其次,为什么不是 pos1 + 1 呢?或者说能不能是 pos1 + 1 呢?

不能。

因为,假如 pos1 是最初一个下标,再加一的话那么就越界了呀。

只能是 pos2 进行加一,因为 pos2 的最大值也只能是倒数第二个元素。

整个算法从逻辑上来说,齐全是没有问题的,能够实现随机抉择两个元素进去的逻辑。

然而,我总感觉对于最初一个元素,即 List 中最初一个服务提供者来说,的确是不太敌对。它被选中的概率比其余的元素少了一半。

万一最初一个服务提供者,又恰好是一个性能牛逼的服务器呢?

这个中央不晓得是不是我想错了,反正我之前写 P2C 的时候,是这样写的:

Object invoker1 = invokerList.remove(ThreadLocalRandom.current().nextInt(invokerList.size()-1));
Object invoker2 = invokerList.remove(ThreadLocalRandom.current().nextInt(invokerList.size()-1));

对于 Dubbo 源码这里为什么是这样的写法,我也不太明确。我看了 go-zero 对应局部的源码,也是和 Dubbo 一样的写法。不晓得是不是我把本人给绕进去了。

算了,问题不大:

后面讲的代码,就是 P2C 思维的实现,是不是非常简单?

在咱们的 Demo 中,选出来的就是 2,1 这两个 invokers:

当初咱们曾经随机抉择出了两个 invoker 了,那么应该由哪个 invoker 来执行这次的申请呢?

逻辑就来到了 chooseLowLoadInvoker 办法外面。从办法名能够晓得,是要抉择负载比拟低的那个。

在这个办法外面,有两个要害变量,load1 和 load2:

它们是通过某个公式计算出来的。

咱们先看简略的状况,当 load1 和 load2 一样的时候,也就是说这两个 invoker 都能够应用,则依照权重抉择一个。

当 load1 和 load2 不一样的时候,则选 load 值比拟小的。

所以接下来的问题就变成了:如何计算 load 的值?

源码都在这个办法外面:

org.apache.dubbo.rpc.AdaptiveMetrics#getLoad

首先 getStatus 办法是获取一个 AdaptiveMetrics 对象。这个对象外面有一个 ConcurrentHashMap,内容是这样的:

以“ip: 端口: 办法”为 key,value 外面放了很多计算负载相干的字段:

比方其中的 pickTime 字段,在计算负载的时候,第一个用到的就是它:

以后工夫减去 pickTime 工夫,如果差值超过超时工夫的两倍,则间接选中它。

假如超时工夫是 5s,那么当这个服务端间隔上次被选中的工夫超过 10s,则返回 0,既示意无负载。

那么这个 pickTime 是什么时候设置的呢?

就是在 doSelect 办法外面通过 selectByP2C 办法抉择出一个服务端之后,在后面提到的 ConcurrentHashMap 中保护了 pickTime:

同时,在这里还在上下文中保护了一个 startTime,示意这个申请开始执行的工夫:

它是在什么时候用的呢?

这个时候就要把眼光放到 AdaptiveLoadBalanceFilter 这个类上了:

org.apache.dubbo.rpc.filter.AdaptiveLoadBalanceFilter#onResponse

在 AdaptiveLoadBalanceFilter 外面的 onResponse 办法外面,当收到服务端的响应之后,在这里取出了 startTime 用来计算 rt 值。

同时在这里还保护了 AdaptiveMetrics 的 consumerSuccess(申请胜利)、errorReq(申请失败)这两个属性:

而后,咱们还能够看到有这样的一部分代码:

这部分代码是在对 ADAPTIVE_LOADBALANCE_ATTACHMENT_KEY 这个字段进行解决。

那么这是个啥玩意呢?

我也不晓得,然而我晓得它也是在 AdaptiveLoadBalance 的 doSelect 办法外面进行了一次保护:

塞进去的值是:

private String attachmentKey = “mem,load”;

看样子是要对内存和负载这两个维度进行统计。

首先,我问你,统计是站在谁的维度统计?

是不是要统计服务端的 mem 和 load?

所以,这里的含意是客户端通知服务端:我这边须要 mem,load 这两个维度,你一会给我送回来。

那么服务端是怎么感知到的呢?

那咱们就要把眼光切换到 ProfilerServerFilter 这个 Filter 了:

org.apache.dubbo.rpc.filter.ProfilerServerFilter#onResponse

在这里咱们能够看到,它从上下文中取出了 ADAPTIVE_LOADBALANCE_ATTACHMENT_KEY 变量,判断是否为空。

如果不为空则搞点事件,而后再从新给这个变量赋值。

那么问题就来了:这个中央并没有对所谓的 mem,load 进行任何解决,只是进行了非空判断,而后就本人 new 了一个 StringBuilder,拼接了 curTime 和 load 属性。

和 mem 没有任何关系?

是的,没有任何关系。

甚至和 load 都没有任何关系,只有 ADAPTIVE_LOADBALANCE_ATTACHMENT_KEY 不是空就行。

然而你不能说这是 BUG,这算是个 features 吧。

好,通过后面的剖析,咱们回到这个中央:

org.apache.dubbo.rpc.filter.AdaptiveLoadBalanceFilter#onResponse

来,你通知我,这个 metricsMap 外面装得是什么?

是不是只有 curTime、load、rt 这三个属性。

巧了,在 AdaptiveMetrics 的 setProviderMetrics 办法中,也只是用(写)到(死)了这三个值,用于给其余字段赋值:

org.apache.dubbo.rpc.AdaptiveMetrics#setProviderMetrics

而后你留神看最初两行:

Vt = β Vt-1 + (1 – β) θt

这是什么货色?

公式?我当然晓得这是一个公式了。

我是问你这是一个什么公式?

第一次看到的时候我也是懵的,我也不晓得,所以我查了一下,这是指数加权均匀 (exponentially weighted moving average),简称 EWMA,能够用来预计变量的部分均值,使得变量的更新与一段时间内的历史取值无关。

我试图去了解它,我也大略晓得它是什么货色,然而你让我给你说进去,道歉,超纲了。

好,最初回到计算 load 值的这个中央:

org.apache.dubbo.rpc.AdaptiveMetrics#getLoad

最初一行代码是这样的:

return metrics.providerCPULoad (Math.sqrt(metrics.ewma) + 1) (inflight + 1) / ((((double) metrics.consumerSuccess.get() / (double) (metrics.consumerReq.get() + 1)) * weight) + 1);

尽管一眼望去眼睛疼,然而我还是给你解释一下每个变量的含意:

  • providerCPULoad:是在 ProfilerServerFilter 的 onResponse 办法中通过计算失去的 cpu load。
  • ewma:是在 setProviderMetrics 办法外面保护的,其中 lastLatency 是在 ProfilerServerFilter 的 onResponse 办法中通过计算失去的 rt 值。
  • inflight:是以后服务提供方正在解决中的申请个数。
  • consumerSuccess:是在每次调用胜利后在 AdaptiveLoadBalanceFilter 的 onResponse 办法中保护的值。
  • consumerReq:是总的调用次数。
  • weight:是服务提供方配置的权重。

以上这些变量带入到下面的公式中,就能获取到一个 load 值。

每个服务提供方通过下面的计算都会失去一个 load 值,其值越低代表越其负载越低。申请就应该发到负载低的机器下来。

因为这个 load 值,是实时计算出来的,反馈的是以后服务器的解决能力。

而负载平衡策略在抉择的时候,通过 load 值来决策是否应该选中这个服务提供方。

所以,这就是自适应负载平衡。

有的同学就会问了:既然能够实时计算 load 值,那么为什么不把所有的服务提供者的 load 都计算出来,而后抉择最小的呢?

很简略,因为随机抉择两个进去比拟对应的工夫是可控的,在常数工夫内。然而如果你要把所有的服务提供者都计算一遍,那么耗时就和服务提供者的数量成正比了。

P2C,稳当的,释怀。

自适应限流

后面聊了自适应负载平衡,然而还在站在服务调用方的角度来说的。

服务调用方来决定本次由哪个服务提供方来执行这次申请。

那么问题就来了:你服务调用方凭什么说啥就是啥?我服务提供方不服气。

假如当初所有的服务提供方的 load 值都很高了。我 P2C 进去两个,一个负载是 100,一个负载是 99。

按理来说,这个时候不应该把申请再给到服务方了。然而调用方可不论这些事件,一个劲的给就行了,边给边说:成长,肯定是随同着苦楚的 …

所以,出于爱护本人的准则,服务端应该有权在本人快 hold 不住的时候,回绝调用端发来的申请。

然而到底应该在什么时候去拒绝请求呢?

这个不好说,应该是一个随着服务能力变动而变动的一个货色。

基于此,Dubbo 也提出了自适应限流的措施:

https://cn.dubbo.apache.org/zh-cn/overview/reference/proposal…

从实践上讲,服务端机器的解决能力是存在下限的,对于一台服务端机器,当短时间内呈现大量的申请调用时,会导致解决不及时的申请积压,使机器过载。在这种状况下可能导致两个问题:

  • 因为申请积压,最终所有的申请都必须期待较长时间能力被解决,从而使整个服务瘫痪。
  • 服务端机器长时间的过载可能有宕机的危险。

这玩意就更简单了,我看了一下对应的 pr,目前还处于 open 状态:

https://github.com/apache/dubbo/pull/10642

看得我脑壳疼,在这里指个路,有趣味的能够本人去钻研一下。

另外,能够联合着这个赛题看。

https://tianchi.aliyun.com/competition/entrance/531923/introd…

能看明确更好,看不明确的话,学到一个看起来很牛逼的词也不错的:柔性集群调度。

退出移动版