Skip to main content

Dubbo如何自定义负载均衡?

作者:程序员马丁

在线博客:https://open8gu.com

note

大话面试,技术同学面试必备的八股文小册,以精彩回答应对深度问题,助力你在面试中拿个offer。

回答话术

1. 为什么自定义负载均衡?

Dubbo 提供了多种内置的负载均衡策略,比如随机、轮询、最少活跃数、一致性 Hash 等,默认的负载均衡策略是随机,已经满足了大多数场景的需求。

在一些特殊的场景下,程序可能需要自定义负载均衡策略,Dubbo 可以通过 SPI 技术根据自己的需求实现自定义的负载均衡策略。

2. 实现自定义 LoadBalance

2.1. 继承抽象 AbstractLoadBalance

package org.apache.dubbo.rpc.cluster.loadbalance;

public class MaLoadBalance extends AbstractLoadBalance {

@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// 从invokers中选择一个返回
}
}

2.2. 引入负载均衡 SPI

  1. resources 新增文件夹 META-INF.dubbo;
  2. 新建文件:org.apache.dubbo.rpc.cluster.LoadBalance;
  3. 文件内容:myLoadBalance=org.apache.dubbo.rpc.cluster.loadbalance.MyLoadBalance。

2.3. 测试使用自定义负载均衡

通过代码编程方式测试 Dubbo 自定义负载均衡。

public static void main(String[] args) {
// 服务对象实例
ReferenceConfig<GreetingService> referenceConfig = new ReferenceConfig<>();

// 应用程序信息
referenceConfig.setApplication(new ApplicationConfig("dubbo-consumer"));

// 服务注册中心
referenceConfig.setRegistry(new RegistryConfig("ZKAddress"));

// 接口和超时时间
referenceConfig.setInterface(GreetingService.class);
referenceConfig.setTimeout(5000);

// 使用自定义负载均衡
referenceConfig.setLoadbalance("myLoadBalance");

// 服务分组与版本
referenceConfig.setVersion("1.0.0");
referenceConfig.setGroup("dubbo");

// 调用服务
GreetingService greetingService = referenceConfig.get();
}

如果在 SpringBoot + Dubbo 架构中,通过配置文件使用自定义负载均衡器。

dubbo:
consumer:
loadbalance: myLoadBalance

问题详解

1. 什么情况下需要自定义 Dubbo 负载均衡器?

以下特殊场景中,需要自定义负载均衡器进行定制化的路由策略。

  • 根据特定的业务规则进行负载均衡,例如根据用户的 IP 地址、请求的参数等。
  • 根据服务的性能状态进行负载均衡,例如根据服务的 CPU 使用率、内存使用率等。
  • 根据服务的调用历史进行负载均衡,例如根据服务的响应时间、成功率等。

2. LoadBalance 接口结构

Dubbo 的负载均衡实现比较简单基本都是继承抽象类进行实现,主要作用就是根据具体的策略在路由之后的服务列表中筛选一个实例进行远程 RPC 调用。

@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {

/**
* 从服务列表中筛选一个
*
* @param invokers 调用者列表
* @param url 调用地址
* @param invocation 调用对象
* @return 选定的调用者
*/
@Adaptive("loadbalance")
<T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;
}

整体类图如下所示:

6-1.png

3. 各个负载均衡器的特点

3.1. Random LoadBalance

按照概率设置权重,相对来说比较均匀,并且可以动态调节提供者的权重。

3.2. RoundRobin LoadBalance

轮询,按公约后的权重设置轮询比率。会存在执行比较慢的服务提供者堆积请求的情况,比如一个机器执行得非常慢,但是机器没有宕机(如果宕机了,那么当前机器会从 ZooKeeper 的服务列表中删除)。

当很多新的请求到达该机器后,由于之前的请求还没处理完,会导致新的请求被堆积,久而久之,消费者调用这台机器上的所有请求都会被阻塞。

3.3. LeastActive LoadBalance

最少活跃调用数:如果每个提供者的活跃数相同,则随机选择一个。

在每个服务提供者里维护着一个活跃数计数器,用来记录当前同时处理请求的个数,也就是并发处理任务的个数。这个值越小,说明当前服务提供者处理的速度越快或者当前机器的负载比较低,所以路由选择时就选择该活跃度最小的机器。

如果一个服务提供者处理速度很慢,同时处理的请求就比较多,请求造成堆积,也就是说活跃调用数较大,处理速度慢。这时,处理速度慢的提供者将收到更少的请求,以均匀流量。

3.4. ConsistentHash LoadBalance

一致性 Hash,可以保证相同参数的请求总是发到同一提供者,当某一台提供者机器宕机时,原本发往该提供者的请求,将基于虚拟节点平摊给其他提供者,这样就不会引起剧烈变动。

4. 实例预热原理

刚启动的应用实例,类似于人刚起床时还不太清醒。它需要一个“预热”的过程,通过慢慢“热身”,各个部件才能正式运转起来。

举个例子,刚启动的实例,数据库连接可能还没完全建立,代码也还是解释执行状态,没有编译成最优化的形式。这个时候,如果突然塞进大量请求,应用来不及处理,很可能就会超时报错。

所以,Dubbo 有一个默认 10 分钟的预热时间。这段时间里,新启动实例接收的流量会慢慢增加,最后跟其他实例一样多。具体做法是,控制新实例的权重逐步提高,第一分钟 10,第二分钟 20,以此类推。这样就可以保证应用有足够时间完全“清醒”过来,然后正常服务。

如图所示:

image.png

随机负载均衡核心代码解析:

// 预热过程权重计算
static int calculateWarmupWeight(int uptime, int warmup, int weight) {
int ww = (int) (uptime / ((float) warmup / weight));
return ww < 1 ? 1 : (Math.min(ww, weight));
}

int getWeight(Invoker<?> invoker, Invocation invocation) {
int weight;
URL url = invoker.getUrl();
// 多注册中心场景下的,注册中心权重获取
if (UrlUtils.isRegistryService(url)) {
weight = url.getParameter(REGISTRY_KEY + "." + WEIGHT_KEYDEFAULT_WEIGHT);
} else {
weight = url.getMethodParameter(invocation.getMethodName()WEIGHT_KEYDEFAULT_WEIGHT);
if (weight > 0) {
// 获取实例启动时间
long timestamp = invoker.getUrl().getParameter(TIMESTAMP_KEY0L);
if (timestamp > 0L) {
long uptime = System.currentTimeMillis() - timestamp;
if (uptime < 0) {
return 1;
}
// 获取预热时间
int warmup = invoker.getUrl().getParameter(WARMUP_KEYDEFAULT_WARMUP);
if (uptime > 0 && uptime < warmup) {
weight = calculateWarmupWeight((int)uptime, warmup, weight);
}
}
}
}
return Math.max(weight, 0);
}

@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// 调用者数量
int length = invokers.size();
// 每个调用者都有相同的权重
boolean sameWeight = true;
// 每个调用者的权重
int[] weights = new int[length];
// 第一个调用者的权重
int firstWeight = getWeight(invokers.get(0), invocation);
weights[0] = firstWeight;
// 权重之和
int totalWeight = firstWeight;
for (int i = 1; i < length; i++) {
int weight = getWeight(invokers.get(i), invocation);
// 保存以备后用
weights[i] = weight;
// 总和
totalWeight += weight;
if (sameWeight && weight != firstWeight) {
sameWeight = false;
}
}
if (totalWeight > 0 && !sameWeight) {
// 如果(不是每个调用者都有相同的权重&至少有一个调用者的权重>0),根据 totalWeight 随机选择
int offset = ThreadLocalRandom.current().nextInt(totalWeight);
// 基于随机值返回调用程序。
for (int i = 0; i < length; i++) {
offset -= weights[i];
if (offset < 0) {
return invokers.get(i);
}
}
}
// 如果所有调用程序都具有相同的权重值或 totalWeight=0,则均匀返回
return invokers.get(ThreadLocalRandom.current().nextInt(length));
}

我们再来解析下代码中的主要逻辑:

  1. 计算每个服务提供者实例的权重:考虑了权重预热逻辑,启动未满指定预热时间的实例,其权重线性递增。权重值取自 URL 参数。
  2. 判断所有实例是否权重相同:相同则随机选择实例,不同则基于权重总和随机选择实例。
  3. 核心关键点:在于动态计算每个实例的权重、支持权重预热以及根据权重分布随机选择实例。