Dubbo的负载均衡策略剖析

1 Dubbo的负载均衡策略概述

Dubbo的负载均衡策略应用于服务消费方。当服务提供者是集群时,通过在消费方设置负载均衡策略,避免大量请求一直集中在其中的某一个或者某几个服务提供方机器上

Dubbo提供了多种负载均衡策略,默认为随机策略-Random LoadBalance,即每次随机调用一台服务提供者的服务。

负载均衡策略的核心方法是各负载均衡策略类的 doSelect() 方法,用于从服务提供者列表中选择其中一个来调用

调用该方法的地方为 AbstractLoadBalance#select() 方法。源码如下所示。

public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
    if (CollectionUtils.isEmpty(invokers)) {
        return null;
    }
    if (invokers.size() == 1) {
        return invokers.get(0);
    }
    return doSelect(invokers, url, invocation);
}

2 源码剖析

下面将介绍一些主要的负载均衡策略,以及对其源码进行解读。

2.1 随机策略-Random LoadBalance

2.1.1 概述

要点:每次随机调用一台服务提供者的服务,并且可以设置不同服务提供者的权重。

使用:

<dubbo:reference ... loadbalance="random"/>

2.1.2 源码剖析

实现类是RandomLoadBalance,源码如下所示。

/**
 * This class select one provider from multiple providers randomly.
 * You can define weights for each provider:
 * If the weights are all the same then it will use random.nextInt(number of invokers).
 * If the weights are different then it will use random.nextInt(w1 + w2 + ... + wn)
 * Note that if the performance of the machine is better than others, you can set a larger weight.
 * If the performance is not so good, you can set a smaller weight.
 */
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
    // Number of invokers
    int length = invokers.size();

    if (!needWeightLoadBalance(invokers, invocation)) {
        return invokers.get(ThreadLocalRandom.current().nextInt(length));
    }

    // Every invoker has the same weight?
    boolean sameWeight = true;
    // the maxWeight of every invoker, the minWeight = 0 or the maxWeight of the last invoker
    int[] weights = new int[length];
    // The sum of weights
    int totalWeight = 0;
    for (int i = 0; i < length; i++) {
        int weight = getWeight(invokers.get(i), invocation);
        // Sum
        totalWeight += weight;
        // save for later use
        weights[i] = totalWeight;
        if (sameWeight && totalWeight != weight * (i + 1)) {
            sameWeight = false;
        }
    }
    if (totalWeight > 0 && !sameWeight) {
        // If (not every invoker has the same weight & at least one invoker's weight>0), select randomly based on totalWeight.
        int offset = ThreadLocalRandom.current().nextInt(totalWeight);
        // Return an invoker based on the random value.
        if (length <= 4) {
            for (int i = 0; i < length; i++) {
                // 注:和dubbo老版本不一样的点是权重大的不一定会首先被选中,因为 weights[i] = totalWeight
                if (offset < weights[i]) {
                    return invokers.get(i);
                }
            }
        } else {
            int i = Arrays.binarySearch(weights, offset);
            if (i < 0) {
                i = -i - 1;
            } else {
                while (weights[i+1] == offset) {
                    i++;
                }
                i++;
            }
            return invokers.get(i);
        }
    }
    // If all invokers have the same weight value or totalWeight=0, return evenly.
    return invokers.get(ThreadLocalRandom.current().nextInt(length));
}

2.2 轮循策略-RoundRobin LoadBalance

2.2.1 概述

要点:轮循调用不同的服务提供者。设置的服务提供者的权重越高,该机器被调用的比例越高。当某台机器执行很慢时,将导致该机器上的请求积压。

2.2.2 源码剖析

实现类是RoundRobinLoadBalance,源码如下所示。

protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
    // methodWeightMap-以调用的方法名作为key,服务提供者和对应的权重为value
    // map-服务提供者为key,服务提供者对应的权重为value
    String key = invokers.get(0).getUrl().getServiceKey() + "." + RpcUtils.getMethodName(invocation);
    ConcurrentMap<String, WeightedRoundRobin> map = ConcurrentHashMapUtils.computeIfAbsent(methodWeightMap, key, k -> new ConcurrentHashMap<>());
    int totalWeight = 0;
    long maxCurrent = Long.MIN_VALUE;
    long now = System.currentTimeMillis();
    Invoker<T> selectedInvoker = null;
    WeightedRoundRobin selectedWRR = null;
    for (Invoker<T> invoker : invokers) {
        // identifyString-服务提供者信息
        String identifyString = invoker.getUrl().toIdentityString();
        int weight = getWeight(invoker, invocation);
        WeightedRoundRobin weightedRoundRobin = ConcurrentHashMapUtils.computeIfAbsent(map, identifyString, k -> {
            WeightedRoundRobin wrr = new WeightedRoundRobin();
            wrr.setWeight(weight);
            return wrr;
        });

        if (weight != weightedRoundRobin.getWeight()) {
            //weight changed
            weightedRoundRobin.setWeight(weight);
        }

        // 设置weightedRoundRobin的 current
        // current.addAndGet(weight)
        long cur = weightedRoundRobin.increaseCurrent();
        weightedRoundRobin.setLastUpdate(now);
        if (cur > maxCurrent) {
            maxCurrent = cur;
            selectedInvoker = invoker;
            selectedWRR = weightedRoundRobin;
        }
        totalWeight += weight;
    }

    if (invokers.size() != map.size()) {
        map.entrySet().removeIf(item -> now - item.getValue().getLastUpdate() > RECYCLE_PERIOD);
    }
    if (selectedInvoker != null) {
        // 重新设置被选中的服务提供者的weightedRoundRobin中的current
        // current.addAndGet(-1 * total);
        selectedWRR.sel(totalWeight);
        // 返回被选中的服务提供者
        return selectedInvoker;
    }
    // should not happen here
    return invokers.get(0);
}

选择服务提供者对应的weightedRoundRobin中的current值最大的服务提供者进行调用。

轮循策略的关键在于维护好每个服务提供者对应的current值当某个服务提供者被选中时,使其current值变小,从而降低下次调用时该机器被再次选中的概率,即增加其他机器在下次调用时被选中的概率。

2.2.3 轮循调用过程剖析

2.2.3.1 服务提供者设置的权重相同

假设有三台服务提供者机器,机器名称和对应的权重如下:
A-1;  B-1;  C-1

调用过程如下:

上述案例将按照 A-B-C 的顺序轮循调用。

由此可知,当服务提供者设置的权重相同时,将依次轮循调用每台服务提供者机器,且每台机器被调用到的比率相同
 

2.2.3.2 服务提供者设置的权重不同

假设有三台服务提供者机器,机器名称和对应的权重如下:
A-1;  B-2;  C-3
 

调用过程如下:

上述案例将按照 C-B-A-C-B-C 的顺序轮循调用。

由此可知,服务提供者的权重越高,该机器被调用到的比率越高。
 

2.3 最少活跃调用数策略-LeastActive LoadBalance

2.3.1 概述

要点:在每个服务提供者里为每个方法维护一个活跃数计数器,用来记录服务提供者中针对每个方法当前同时处理请求的个数。路由选择时会选择该活跃数最小的机器进行调用。

这种机制确保了在高并发场景下,请求能够被均匀地分发到各个服务提供者,避免某些服务提供者因负载过高而性能下降。

2.3.2 源码剖析

实现类是LeastActiveLoadBalance,源码如下所示。

protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
    // Number of invokers
    int length = invokers.size();
    // The least active value of all invokers
    int leastActive = -1;
    // The number of invokers having the same least active value (leastActive)
    int leastCount = 0;
    // The index of invokers having the same least active value (leastActive)
    int[] leastIndexes = new int[length];
    // the weight of every invokers
    int[] weights = new int[length];
    // The sum of the warmup weights of all the least active invokers
    int totalWeight = 0;
    // The weight of the first least active invoker
    int firstWeight = 0;
    // Every least active invoker has the same weight value?
    boolean sameWeight = true;


    // Filter out all the least active invokers
    for (int i = 0; i < length; i++) {
        Invoker<T> invoker = invokers.get(i);
        // Get the active number of the invoker
        int active = RpcStatus.getStatus(invoker.getUrl(), RpcUtils.getMethodName(invocation)).getActive();
        // Get the weight of the invoker's configuration. The default value is 100.
        int afterWarmup = getWeight(invoker, invocation);
        // save for later use
        weights[i] = afterWarmup;
        // If it is the first invoker or the active number of the invoker is less than the current least active number
        if (leastActive == -1 || active < leastActive) {
            // Reset the active number of the current invoker to the least active number
            leastActive = active;
            // Reset the number of least active invokers
            leastCount = 1;
            // Put the first least active invoker first in leastIndexes
            leastIndexes[0] = i;
            // Reset totalWeight
            totalWeight = afterWarmup;
            // Record the weight the first least active invoker
            firstWeight = afterWarmup;
            // Each invoke has the same weight (only one invoker here)
            sameWeight = true;
            // If current invoker's active value equals with leaseActive, then accumulating.
        } else if (active == leastActive) {
            // Record the index of the least active invoker in leastIndexes order
            leastIndexes[leastCount++] = i;
            // Accumulate the total weight of the least active invoker
            totalWeight += afterWarmup;
            // If every invoker has the same weight?
            if (sameWeight && afterWarmup != firstWeight) {
                sameWeight = false;
            }
        }
    }
    // Choose an invoker from all the least active invokers
    if (leastCount == 1) {
        // If we got exactly one invoker having the least active value, return this invoker directly.
        return invokers.get(leastIndexes[0]);
    }
    if (!sameWeight && totalWeight > 0) {
        // If (not every invoker has the same weight & at least one invoker's weight>0), select randomly based on
        // totalWeight.
        int offsetWeight = ThreadLocalRandom.current().nextInt(totalWeight);
        // Return a invoker based on the random value.
        for (int i = 0; i < leastCount; i++) {
            int leastIndex = leastIndexes[i];
            offsetWeight -= weights[leastIndex];
            if (offsetWeight < 0) {
                return invokers.get(leastIndex);
            }
        }
    }
    // If all invokers have the same weight value or totalWeight=0, return evenly.
    return invokers.get(leastIndexes[ThreadLocalRandom.current().nextInt(leastCount)]);
}

其中活跃数计数器对象为 RpcStatus,主要属性为 active。

public class RpcStatus {

    private static final ConcurrentMap<String, RpcStatus> SERVICE_STATISTICS = new ConcurrentHashMap<String,
        RpcStatus>();

    private static final ConcurrentMap<String, ConcurrentMap<String, RpcStatus>> METHOD_STATISTICS =
        new ConcurrentHashMap<String, ConcurrentMap<String, RpcStatus>>();

    private final ConcurrentMap<String, Object> values = new ConcurrentHashMap<String, Object>();

    private final AtomicInteger active = new AtomicInteger();

   
    // ...


}

LeastActiveLoadBalance 需要与 ActiveLimitFilter 一起使用才能达到负载均衡的效果,因为ActiveLimitFilter 会记录每个接口方法的活跃请求数。

活跃调用数通常会在以下几个时刻被设置或更新:

  • 调用开始时:当一个请求被发送到服务提供者时,该服务提供者的活跃调用数会增加
  • 调用结束时:当服务提供者完成一个请求并返回结果时,该服务提供者的活跃调用数会减少

ActiveLimitFilter 记录每个接口方法的活跃请求数的代码如下所示。

@Activate(group = CONSUMER, value = ACTIVES_KEY)
public class ActiveLimitFilter implements Filter, Filter.Listener {

    private static final String ACTIVE_LIMIT_FILTER_START_TIME = "active_limit_filter_start_time";

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        URL url = invoker.getUrl();
        String methodName = RpcUtils.getMethodName(invocation);
        int max = invoker.getUrl().getMethodParameter(methodName, ACTIVES_KEY, 0);
        final RpcStatus rpcStatus = RpcStatus.getStatus(invoker.getUrl(), RpcUtils.getMethodName(invocation));
        // 增加活跃调用数
        if (!RpcStatus.beginCount(url, methodName, max)) {
            long timeout = invoker.getUrl().getMethodParameter(RpcUtils.getMethodName(invocation), TIMEOUT_KEY, 0);
            long start = System.currentTimeMillis();
            long remain = timeout;
            synchronized (rpcStatus) {
                while (!RpcStatus.beginCount(url, methodName, max)) {
                    try {
                        rpcStatus.wait(remain);
                    } catch (InterruptedException e) {
                        // ignore
                    }
                    long elapsed = System.currentTimeMillis() - start;
                    remain = timeout - elapsed;
                    if (remain <= 0) {
                        throw new RpcException(RpcException.LIMIT_EXCEEDED_EXCEPTION,
                                "Waiting concurrent invoke timeout in client-side for service:  " +
                                        invoker.getInterface().getName() + ", method: " + RpcUtils.getMethodName(invocation) +
                                        ", elapsed: " + elapsed + ", timeout: " + timeout + ". concurrent invokes: " +
                                        rpcStatus.getActive() + ". max concurrent invoke limit: " + max);
                    }
                }
            }
        }

        invocation.put(ACTIVE_LIMIT_FILTER_START_TIME, System.currentTimeMillis());

        return invoker.invoke(invocation);
    }

    @Override
    public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
        String methodName = RpcUtils.getMethodName(invocation);
        URL url = invoker.getUrl();
        int max = invoker.getUrl().getMethodParameter(methodName, ACTIVES_KEY, 0);

        // 减少活跃调用数
        RpcStatus.endCount(url, methodName, getElapsed(invocation), true);
        notifyFinish(RpcStatus.getStatus(url, methodName), max);
    }

    @Override
    public void onError(Throwable t, Invoker<?> invoker, Invocation invocation) {
        String methodName = RpcUtils.getMethodName(invocation);
        URL url = invoker.getUrl();
        int max = invoker.getUrl().getMethodParameter(methodName, ACTIVES_KEY, 0);

        if (t instanceof RpcException) {
            RpcException rpcException = (RpcException) t;
            if (rpcException.isLimitExceed()) {
                return;
            }
        }

        // 减少活跃调用数
        RpcStatus.endCount(url, methodName, getElapsed(invocation), false);
        notifyFinish(RpcStatus.getStatus(url, methodName), max);
    }

    private long getElapsed(Invocation invocation) {
        Object beginTime = invocation.get(ACTIVE_LIMIT_FILTER_START_TIME);
        return beginTime != null ? System.currentTimeMillis() - (Long) beginTime : 0;
    }

    private void notifyFinish(final RpcStatus rpcStatus, int max) {
        if (max > 0) {
            synchronized (rpcStatus) {
                rpcStatus.notifyAll();
            }
        }
    }
}


public static boolean beginCount(URL url, String methodName, int max) {
    max = (max <= 0) ? Integer.MAX_VALUE : max;
    RpcStatus appStatus = getStatus(url);
    RpcStatus methodStatus = getStatus(url, methodName);
    if (methodStatus.active.get() == Integer.MAX_VALUE) {
        return false;
    }
    for (int i; ; ) {
        i = methodStatus.active.get();

        if (i == Integer.MAX_VALUE || i + 1 > max) {
            return false;
        }

        if (methodStatus.active.compareAndSet(i, i + 1)) {
            break;
        }
    }

    appStatus.active.incrementAndGet();

    return true;
}

private static void endCount(RpcStatus status, long elapsed, boolean succeeded) {
    status.active.decrementAndGet();
    status.total.incrementAndGet();
    status.totalElapsed.addAndGet(elapsed);

    if (status.maxElapsed.get() < elapsed) {
        status.maxElapsed.set(elapsed);
    }

    if (succeeded) {
        if (status.succeededMaxElapsed.get() < elapsed) {
            status.succeededMaxElapsed.set(elapsed);
        }

    } else {
        status.failed.incrementAndGet();
        status.failedElapsed.addAndGet(elapsed);
        if (status.failedMaxElapsed.get() < elapsed) {
            status.failedMaxElapsed.set(elapsed);
        }
    }
}



使用案例如下

<dubbo:reference id="yourService" interface="com.example.YourService" loadbalance="leastactive" filter="activelimit" />

2.4 一致性Hash策略-ConsistentHash LoadBalance

2.4.1 概述

要点:相同参数的请求总是被发到同一服务提供者进行处理。

原理:简单来说就是首先计算服务提供者的hash值,并将该值放到一个hash环上。当请求过来时,计算本次请求的hash值,并根据该值到hash环上按顺时针找距离该值最近的hash值,此hash值对应的服务提供者将处理此次请求。

因为相同参数计算得到的hash值相同,因此相同参数的请求总是被发送到同一服务提供者进行处理。

当节点较少时,如果hash值分布不均匀,将会出现一致性hash倾斜的问题,导致机器服务不均衡。此时可以通过增加机器或者增加虚拟节点的方式解决上述问题。从使用成本来看,显然通过增加虚拟节点更合适。以下是为每台机器引入一个虚拟节点后的一致性hash环示意图。

备注:

(1)服务提供者的hash值根据服务提供者的ip进行计算。

(2)请求的hash值根据请求参数(根据调用的方法名或者方法的第一个参数)计算。

优缺点:让固定的请求落到同一台服务器上,这样每台服务器就会固定处理一部分请求,从而达到一定的负载均衡作用。这适用于需要将同一个请求参数对应到同个服务器的等场景。但是不如其他策略(如最小活跃调用数策略)那样,使得处理慢的服务提供者收到更少的请求。

2.4.2 源码剖析

实现类是ConsistentHashLoadBalance,源码如下所示。

/**
 *  "具体方法名"与"hash值与服务提供者之间的映射关系"之间的映射关系
 */
private final ConcurrentMap<String, ConsistentHashSelector<?>> selectors = new ConcurrentHashMap<String, ConsistentHashSelector<?>>();


@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
    String methodName = RpcUtils.getMethodName(invocation);

    // 调用的方法名
    String key = invokers.get(0).getUrl().getServiceKey() + "." + methodName;
    // using the hashcode of list to compute the hash only pay attention to the elements in the list
    int invokersHashCode = invokers.hashCode();
    
    // 获取hash值与服务提供者之间的映射关系
    ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
    if (selector == null || selector.identityHashCode != invokersHashCode) {
        selectors.put(key, new ConsistentHashSelector<T>(invokers, methodName, invokersHashCode));
        selector = (ConsistentHashSelector<T>) selectors.get(key);
    }

    // 根据请求参数获取服务提供者
    return selector.select(invocation);
}

创建hash值与服务提供者的映射关系的源码如下,其关键在于创建了一个均匀分布着服务提供者的hash的hash环。

private static final class ConsistentHashSelector<T> {

    // 创建hash值与服务提供者的映射关系(包括创建了服务提供者在hash环上的虚拟节点)
    private final TreeMap<Long, Invoker<T>> virtualInvokers;

    private final int replicaNumber;

    private final int identityHashCode;

    private final int[] argumentIndex;

    ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {
        this.virtualInvokers = new TreeMap<Long, Invoker<T>>();
        this.identityHashCode = identityHashCode;
        URL url = invokers.get(0).getUrl();

        // 虚拟节点数(每个方法都可以自定义 HASH_NODES)
        this.replicaNumber = url.getMethodParameter(methodName, HASH_NODES, 160);
        String[] index = COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, HASH_ARGUMENTS, "0"));
        argumentIndex = new int[index.length];
        for (int i = 0; i < index.length; i++) {
            argumentIndex[i] = Integer.parseInt(index[i]);
        }
        for (Invoker<T> invoker : invokers) {
            String address = invoker.getUrl().getAddress();
            for (int i = 0; i < replicaNumber / 4; i++) {
                // 根据机器ip计算某个服务提供者的hash值
                byte[] digest = Bytes.getMD5(address + i);
                for (int h = 0; h < 4; h++) {
                    long m = hash(digest, h);
                    virtualInvokers.put(m, invoker);
                }
            }
        }
    }

根据请求参数获取服务提供者的源码如下所示。

public Invoker<T> select(Invocation invocation) {
    byte[] digest = Bytes.getMD5(RpcUtils.getMethodName(invocation));
    // 根据请求参数对应的hash值,通过一致性hash环,获取对应的服务提供者
    return selectForKey(hash(digest, 0));
}

public static String getMethodName(Invocation invocation) {
    if ($INVOKE.equals(invocation.getMethodName())
        && invocation.getArguments() != null
        && invocation.getArguments().length > 0
        && invocation.getArguments()[0] instanceof String) {
        return (String) invocation.getArguments()[0];
    }
    return invocation.getMethodName();
}


private long hash(byte[] digest, int number) {
    return (((long) (digest[3 + number * 4] & 0xFF) << 24)
            | ((long) (digest[2 + number * 4] & 0xFF) << 16)
            | ((long) (digest[1 + number * 4] & 0xFF) << 8)
            | (digest[number * 4] & 0xFF))
            & 0xFFFFFFFFL;
}

private Invoker<T> selectForKey(long hash) {
    Map.Entry<Long, Invoker<T>> entry = virtualInvokers.ceilingEntry(hash);
    if (entry == null) {
        entry = virtualInvokers.firstEntry();
    }
    return entry.getValue();
}

3 自定义负载均衡策略

3.1 自定义LoadBalance

创建一个继承 AbstractLoadBalance 的类,并重写 doSelect() 方法。举例如下。

public class MyLoadBalance extends AbstractLoadBalance {
    @Override
    protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        Invoker invoker = null;
        
        // 自定义负载均衡算法,从invokers中选择一个Invoker
        
        return invoker;
    }
}

3.2 配置和使用

在 resources 目录下, 添加 META-INF/dubbo 目录, 继而添加 org.apache.dubbo.rpc.cluster.LoadBalance 文件。并将自定义的 LoadBalance 类配置到该文件中。

myLoadBalance=org.apache.dubbo.rpc.cluster.loadbalance.MyLoadBalance

然后在消费接口时指定使用自定义的策略。

<dubbo:reference ... loadbalance="myLoadBalance"/>

备注:Dubbo自带的负载均衡策略配置如下所示

  • 24
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值