Motan_LoadBalance

本贴最后更新于 1699 天前,其中的信息可能已经时异事殊

Motan 系列文章


LoadBalance 即负载均衡策略(下面简称 LB),Motan 为 LB 提供了多种方案,默认为 ActiveWeight ,并支持自定义扩展(通过 SPI 机制),Motan 是在 Client 端做的负载均衡。目前支持以下几种:

  • ActiveWeight(默认)

低并发度优先: referer 的某时刻的 call 数越小优先级越高。

配置方式:

<motan:protocol ... loadbalance="activeWeight"/>
  • Random

随机选择。在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。

配置方式:

<motan:protocol ... loadbalance="random"/>
  • RoundRobin

轮循选择,调用比较均匀。

配置方式:

<motan:protocol ... loadbalance="roundrobin"/>
  • LocalFirst

本地服务优先获取策略,对 referers 根据 ip 顺序查找本地服务,多存在多个本地服务,获取 Active 最小的本地服务进行服务。

当不存在本地服务,但是存在远程 RPC 服务,则根据 ActivWeight 获取远程 RPC 服务。

当两者都存在,所有本地服务都应优先于远程服务,本地 RPC 服务与远程 RPC 服务内部则根据 ActiveWeight 进行。

配置方式:

<motan:protocol ... loadbalance="localFirst"/>
  • Consistent

一致性 Hash,相同参数的请求总是发到同一提供者。

配置方式:

<motan:protocol ... loadbalance="consistent"/>
  • ConfigurableWeight

权重可配置的负载均衡策略。

配置方式:

<motan:protocol ... loadbalance="configurableWeight"/>

0 工程结构及继承关系

下图展示了 LB 源码在工程中的位置。

MotanLB1.png

下面来看下 LB 的体系结构。

MotanLoadBalance 类图.jpg

LoadBalance 作为顶层接口,定义了 LB 的主要功能。最主要的两个方法为 select()selectToHolder(),用于从 Cluster 中根据规则选择一个 Referer 调用。

AbstractLoadBalance 实现了 LoadBalance 接口,并提供了 select()selectToHolder() 方法的通用实现,同时又定义了 doSelect()doSelectToHolder() 两个抽象方法,select()selectToHolder() 分别调用了这两个方法,这里是一个模板设计模式的实现,doSelect()doSelectToHolder() 会交给具体的实现类实现,定义自己的 Referer 选取逻辑。

最后,6 个具体实现类 extends 了 AbstractLoadBalance,并实现 doSelect()doSelectToHolder(),完成具体的 Referer 选取。

PS:doSelect()doSelectToHolder() 都是选取 Referer 的逻辑,只是调用的地方不同。doSelect()FailfastHaStrategy 调用,doSelectToHolder()FailoverHaStrategy 调用。也就是说,这两个方法分别对应到两个不同的 HA 具体实现上。对于默认 HA 策略的 FailoverHaStrategy 来说,需要实现失效转移功能,所以这里可以暂时理解为,doSelectToHolder 用于支持失效转移,而 doSelect 用于支持快速失败。

1 LoadBalance 策略的选取及设置

Motan 会在 Cluster 初始化阶段设置 LB 策略。LB 策略可通过上述配置来定义,如果不配置,默认使用 ActiveWeight,即低并发度优先策略。

在实现上,LoadBalance 采用插件化开发(即 SPI),每个 LoadBalance 的具体实现都是一个 SPI,其实现类都标注了 @SpiMeta 注解,在 setLoadBalance 时,根据具体的 LB 名称即可找到具体的实现。以 ActiveWeight 为例:

@SpiMeta(name = "activeWeight")
public class ActiveWeightLoadBalance<T> extends AbstractLoadBalance<T> {
    
}

然后在 ClusterSupport 中通过 SPI 获取 LoadBalance 的具体实现,并 setLoadBalance。

private void prepareCluster() {
    String clusterName = url.getParameter(URLParamType.cluster.getName(), URLParamType.cluster.getValue());
    // 获取LoadBalance策略,优选获取用户配置的,如果没有配置,取URLParamType配置的默认值,即 activeWeight
    String loadbalanceName = url.getParameter(URLParamType.loadbalance.getName(), URLParamType.loadbalance.getValue());
    String haStrategyName = url.getParameter(URLParamType.haStrategy.getName(), URLParamType.haStrategy.getValue());

    cluster = ExtensionLoader.getExtensionLoader(Cluster.class).getExtension(clusterName);
    // 通过SPI的方式获取具体LoadBalance实现
    LoadBalance<T> loadBalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(loadbalanceName);
    HaStrategy<T> ha = ExtensionLoader.getExtensionLoader(HaStrategy.class).getExtension(haStrategyName);
    ha.setUrl(url);
    cluster.setLoadBalance(loadBalance); // setLoadBalance
    cluster.setHaStrategy(ha);
    cluster.setUrl(url);
}

2 调用时序

配置好 LB 以后,再来看一下调用时序。以下时序图说明了一个 RPC 请求的调用时序,主要关注 HaStrategy 到 LoadBalance 的调用。

MotanReferer 调用时序图.jpg

至此,本文已说明 LB 的设置以及 LB 的调用时机,下面介绍几个具体的 LB 实现。

3 LoadBalance 的实现

下面介绍三种 LoadBalance 的具体实现。

  • ActiveWeightLoadBalance:低并发度优先策略
  • RandomLoadBalance:随机策略
  • RoundRobinLoadBalance:轮询策略

3.1 ActiveWeightLoadBalance

ActiveWeightLoadBalance 是默认的 LB 策略,其 Referer 的选取策略是低并发度优先,即某一时刻,选取正在处理请求数最少的那个 Referer。

先来看 ActiveWeightLoadBalance 的 doSelectToHolder 方法,他的作用是在集群中选出最多 10 个可用的节点,并将这些节点按照并发数升序排序。这样一来,第一个就是并发度最低的那个。

PS:为啥要选出多个可用节点,而不是选出一个就行?
因为 LB 的调用上游是 HA,默认情况下,HA 策略是失效转移,即如果一个节点不可用了,要再次对其他的节点尝试调用。所以这里要选出多个,用于满足 HA 的失效转移策略。

protected void doSelectToHolder(Request request, List<Referer<T>> refersHolder) {
    // 获取集群的所有节点
    List<Referer<T>> referers = getReferers();

    int refererSize = referers.size();
    // 随机一个起始索引出来(不从0开始索引),目的是,当集群规模较大时,保证所有节点都有被选取的机会
    int startIndex = ThreadLocalRandom.current().nextInt(refererSize);
    int currentCursor = 0;
    int currentAvailableCursor = 0;
    // MAX_REFERER_COUNT的值是10,意味着最多选出10个有效节点作为备选调用
    while (currentAvailableCursor < MAX_REFERER_COUNT && currentCursor < refererSize) {
        Referer<T> temp = referers.get((startIndex + currentCursor) % refererSize);
        currentCursor++;

        if (!temp.isAvailable()) {
            continue;
        }

        currentAvailableCursor++;

        refersHolder.add(temp);
    }
    // 按并发数升序排序
    Collections.sort(refersHolder, new LowActivePriorityComparator<T>());
}

这里最大规模集群做了一个优化,例如集群有 100 个节点,并且都是可用状态,那么,startIndex 是 100 以内的一个随机值,例如 95,由于最多选取 10 个,所以最终选择的节点是:[95, 96, 97, 98, 99, 0, 1, 2, 3, 4]。

如果 startIndex 每次都从 0 开始,后面的节点就没有机会入选了。

最后将选取结果按并发度升序排序,得到最终结果。

并发度如何计算?

由上述代码可知,排序依据是 LowActivePriorityComparator 这个比较器。来看下这个:

static class LowActivePriorityComparator<T> implements Comparator<Referer<T>> {
    @Override
    public int compare(Referer<T> referer1, Referer<T> referer2) {
        return referer1.activeRefererCount() - referer2.activeRefererCount();
    }
}

这里涉及到了 Referer 的 activeRefererCount,这个字段的定义在 AbstractReferer 中,参考如下源码:

protected AtomicInteger activeRefererCount = new AtomicInteger(0);

public Response call(Request request) {
    if (!isAvailable()) {
        throw new MotanFrameworkException(this.getClass().getSimpleName() + " call Error: node is not available, url=" + url.getUri()
                + " " + MotanFrameworkUtil.toString(request));
    }
    // activeRefererCount + 1
    incrActiveCount(request);
    Response response = null;
    try {
        response = doCall(request);

        return response;
    } finally {
        // activeRefererCount - 1
        decrActiveCount(request, response);
    }
}

可以得出结论,这个值,其实是在某一 Referer 的 RPC 调用前 +1,调用结束后-1,这样就得到了某一 Referer 的并发数。

3.2 RandomLoadBalance

RandomLoadBalance,即从集群中随机选取一个,如果选取的是不可用的节点,就继续随机,直到找到可用的为止。

protected void doSelectToHolder(Request request, List<Referer<T>> refersHolder) {
    List<Referer<T>> referers = getReferers();

    int idx = (int) (ThreadLocalRandom.current().nextDouble() * referers.size());
    for (int i = 0; i < referers.size(); i++) {
        Referer<T> referer = referers.get((i + idx) % referers.size());
        if (referer.isAvailable()) {
            refersHolder.add(referer);
        }
    }
}

3.3 RoundRobinLoadBalance

即轮询策略。在大规模集群下,仍然是最多选取 10 个可用的节点。

private AtomicInteger idx = new AtomicInteger(0);

protected void doSelectToHolder(Request request, List<Referer<T>> refersHolder) {
    List<Referer<T>> referers = getReferers();

    int index = getNextNonNegative();
    for (int i = 0, count = 0; i < referers.size() && count < MAX_REFERER_COUNT; i++) {
        Referer<T> referer = referers.get((i + index) % referers.size());
        if (referer.isAvailable()) {
            refersHolder.add(referer);
            count++;
        }
    }
}

// get non-negative int
private int getNextNonNegative() {
    return MathUtil.getNonNegative(idx.incrementAndGet());
}

idx 是一个共享实例变量,每次 doSelectToHolder 递增 1,然后与 referer 数取模,以达到轮询效果。同样的,为了满足 HA 的失败转移策略,这里会选取多个可用 referer,最多取 10 个。

参考

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...