dubbo中支持的负载均衡有以下4种,
常用的配置方式如下:
<dubbo:reference id="demoService" interface="com.alibaba.dubbo.demo.DemoService" loadbalance="roundrobin" weight="90"/>
<dubbo:service interface="com.alibaba.dubbo.demo.DemoService" ref="demoService" loadbalance="roundrobin"/>
public class RandomLoadBalance extends AbstractLoadBalance {
public static final String NAME = "random";
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
int length = invokers.size(); // Invoker个数
boolean sameWeight = true; // 每个Invoker是否具有同样的权重
int[] weights = new int[length]; // 每个Invoker的权重放在weights数组中
int firstWeight = getWeight(invokers.get(0), invocation); // 第一个Invoker的权重
weights[0] = firstWeight;
int totalWeight = firstWeight; // totalWeight表示所有Invoker的权重总和,初始化为第一个Invoker的权重
for (int i = 1; i < length; i++) {
int weight = getWeight(invokers.get(i), invocation);
weights[i] = weight; // 第i个Invoker的权重
totalWeight += weight; // 累加
if (sameWeight && weight != firstWeight) {
sameWeight = false;
}
}
// 若所有的 Invoker 有不相同的权重值 && totalWeight>0,则基于totalWeight随机选择Invoker
if (totalWeight > 0 && !sameWeight) {
int offset = ThreadLocalRandom.current().nextInt(totalWeight);
for (int i = 0; i < length; i++) {
offset -= weights[i];
if (offset < 0) {
return invokers.get(i);
}
}
}
// 如果所有的Invoker都具有相同的权重值 or totalWeight=0, 随机返回一个.
return invokers.get(ThreadLocalRandom.current().nextInt(length));
}
}
由上知,若Invoker中有不同的权重,则进行以下流程,否则随机选择一个Invoker
**Step 1:**从[0,totalWeight ]之间生成一个伪随机数 offset
**Step 2:**遍历所有的invoker,若其权重减去 offset 小于0,则返回
注:权重默认值100
public class RoundRobinLoadBalance extends AbstractLoadBalance {
//.......
protected static class WeightedRoundRobin {
private int weight;
private AtomicLong current = new AtomicLong(0);
private long lastUpdate;
//......
}
//......
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
ConcurrentMap<String, WeightedRoundRobin> map = methodWeightMap.get(key);
if (map == null) {
methodWeightMap.putIfAbsent(key, new ConcurrentHashMap<String, WeightedRoundRobin>());
map = methodWeightMap.get(key);
}
int totalWeight = 0;
long maxCurrent = Long.MIN_VALUE;
long now = System.currentTimeMillis();
Invoker<T> selectedInvoker = null;
WeightedRoundRobin selectedWRR = null;
// 找出权重最大的 Invoker,每个 Invoker 维持一个 WeightedRoundRobin ,每一次调用该方法就会把当前的Invoker持有的 WeightedRoundRobin 中的weight加倍
for (Invoker<T> invoker : invokers) {
String identifyString = invoker.getUrl().toIdentityString();
WeightedRoundRobin weightedRoundRobin = map.get(identifyString);
int weight = getWeight(invoker, invocation);
if (weightedRoundRobin == null) {
weightedRoundRobin = new WeightedRoundRobin();
weightedRoundRobin.setWeight(weight);
map.putIfAbsent(identifyString, weightedRoundRobin);
}
if (weight != weightedRoundRobin.getWeight()) {
//weight changed
weightedRoundRobin.setWeight(weight);
}
long cur = weightedRoundRobin.increaseCurrent();
weightedRoundRobin.setLastUpdate(now);
if (cur > maxCurrent) {
maxCurrent = cur;
selectedInvoker = invoker;
selectedWRR = weightedRoundRobin;
}
totalWeight += weight; // 累加
}
// 更新 methodWeightMap 过程:创建新map,遍历 newMap 若当前的 Invoker 中的更新时间距离现在已经超过一次循环周期(默认60000毫秒,即60秒)就从newMap中移除,从而达到更新 methodWeightMap 的目的(对 进行检查,过滤掉长时间未被更新的节点。该节点可能挂了,invokers 中不包含该节点,所以该节点的 lastUpdate 长时间无法被更新。若未更新时长超过阈值后,就会被移除掉)
if (!updateLock.get() && invokers.size() != map.size()) {
if (updateLock.compareAndSet(false, true)) {
try {
// copy -> modify -> update reference
ConcurrentMap<String, WeightedRoundRobin> newMap = new ConcurrentHashMap<String, WeightedRoundRobin>();
newMap.putAll(map);
Iterator<Entry<String, WeightedRoundRobin>> it = newMap.entrySet().iterator();
while (it.hasNext()) {
Entry<String, WeightedRoundRobin> item = it.next();
// 若当前的 Invoker 中的更新时间距离现在已经超过一次循环周期,移出,则上面for循环进行筛选时就会重新对当前的 Invoker 生成 WeightedRoundRobin
if (now - item.getValue().getLastUpdate() > RECYCLE_PERIOD) {
it.remove();
}
}
methodWeightMap.put(key, newMap);
} finally {
updateLock.set(false);
}
}
}
if (selectedInvoker != null) {
// 选中Invoker后会把当前Invoker生成的WeightedRoundRobin当前权重减去总权重
selectedWRR.sel(totalWeight);
return selectedInvoker;
}
// should not happen here
return invokers.get(0);
}
//......
}
分析一:很多文章上说轮询负载均衡有个缺点:
轮询负载均衡算法也有不足的地方,存在慢的 Provider 累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上,导致整个系统变慢。
以上说法是对于旧版本的轮询负载均衡有道理的,上面版本的已经解决了这个问题,该版本的轮询负载均衡已经脱离了调用次数的限制,通过不断的减轻现有调用的当前权重来达到了轮询的目的。举例如下:
当前机器有3个provider,P1、P2、P3,权重分别是10,100,20
第一次请求:
- P1 WeightedRoundRobin [weight=10, current=10]
- P2 WeightedRoundRobin [weight=100, current=100]
- P3 WeightedRoundRobin [weight=20, current=20]
选中P2,totalWeight=130,执行完 P2变成WeightedRoundRobin [weight=100, current=-30]
第二次请求:
- P1 WeightedRoundRobin [weight=10, current=20]
- P2 WeightedRoundRobin [weight=100, current=70]
- P3 WeightedRoundRobin [weight=20, current=40]
选中P2,totalWeight=130,执行完 P2变成WeightedRoundRobin [weight=100, current=-60]
第三次请求:
- P1 WeightedRoundRobin [weight=10, current=30]
- P2 WeightedRoundRobin [weight=100, current=40]
- P3 WeightedRoundRobin [weight=20, current=60]
选中P3,totalWeight=130,执行完 P3变成WeightedRoundRobin [weight=20, current=-70]
由上可见,虽然P2权重比较大,但经过每一次调用后,P2的当前权重逐渐变小,这样权重较小的provide有了被调用的机会
分析二:在更新 methodWeightMap 过程中,什么时候会出现源码中的 invokers.size() != map.size()
该节点可能挂了,invokers 中不包含该节点,所以该节点的 lastUpdate 长时间无法被更新。若未更新时长超过阈值后,就会被移除掉
public class LeastActiveLoadBalance extends AbstractLoadBalance {
public static final String NAME = "leastactive";
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
int length = invokers.size(); // provider个数
int leastActive = -1; // 所有invoker的最小活跃值
int leastCount = 0; //最小活跃数的 Invoker 的个数
int[] leastIndexes = new int[length]; //最小活跃数的 Invoker 的索引
int[] weights = new int[length]; // 每个invoker的权重
int totalWeight = 0;
int firstWeight = 0;
boolean sameWeight = true; // 每个Invoker是否具有同样的权重
for (int i = 0; i < length; i++) {
Invoker<T> invoker = invokers.get(i);
int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive();
int afterWarmup = getWeight(invoker, invocation);
weights[i] = afterWarmup;
if (leastActive == -1 || active < leastActive) {
leastActive = active;
leastCount = 1;
leastIndexes[0] = i;
totalWeight = afterWarmup;
firstWeight = afterWarmup;
sameWeight = true;
} else if (active == leastActive) {
leastIndexes[leastCount++] = i;
totalWeight += afterWarmup;
if (sameWeight && i > 0 && afterWarmup != firstWeight) {
sameWeight = false;
}
}
} // for
// 如果具有最小活跃数的 Invoker 只有一个,直接返回该 Invoker
if (leastCount == 1) {
return invokers.get(leastIndexes[0]);
}
// 如果最小活跃数的 Invoker 有多个,且权重不相等同时总权重大于0,这时随机生成一个权重,范围在 (0,totalWeight) 间内。最后根据随机生成的权重,来选择 Invoker。
if (!sameWeight && totalWeight > 0) {
int offsetWeight = ThreadLocalRandom.current().nextInt(totalWeight);
for (int i = 0; i < leastCount; i++) {
int leastIndex = leastIndexes[i];
offsetWeight -= weights[leastIndex];
if (offsetWeight < 0) {
return invokers.get(leastIndex);
}
}
}
return invokers.get(leastIndexes[ThreadLocalRandom.current().nextInt(leastCount)]);
}
}
源码比较清晰,不再解释。
public class ConsistentHashLoadBalance extends AbstractLoadBalance {
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;
// 获取 invokers 原始的 hashcode
int identityHashCode = System.identityHashCode(invokers);
ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
// 如果 invokers 是一个新的 List 对象,意味着服务提供者数量发生了变化,可能新增也可能减少了。此时 selector.identityHashCode != identityHashCode 条件成立
if (selector == null || selector.identityHashCode != identityHashCode) {
// 创建新的 ConsistentHashSelector
selectors.put(key, new ConsistentHashSelector<T>(invokers, methodName, identityHashCode));
selector = (ConsistentHashSelector<T>) selectors.get(key);
}
// 调用 ConsistentHashSelector 的 select 方法选择 Invoker
return selector.select(invocation);
}
private static final class ConsistentHashSelector<T> {
// 使用 TreeMap 存储 Invoker 虚拟节点
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();
// 获取虚拟节点数,默认为160
this.replicaNumber = url.getMethodParameter(methodName, "hash.nodes", 160);
// 获取参与 hash 计算的参数下标值,默认对第一个参数进行 hash 运算
String[] index = Constants.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++) {
// 对 address + i 进行 md5 运算,得到一个长度为16的字节数组
byte[] digest = md5(address + i);
// 对 digest 部分字节进行4次 hash 运算,得到四个不同的 long 型正整数
for (int h = 0; h < 4; h++) {
// h = 0 时,取 digest 中下标为 0 ~ 3 的4个字节进行位运算
// h = 1 时,取 digest 中下标为 4 ~ 7 的4个字节进行位运算
// h = 2, h = 3 时过程同上
long m = hash(digest, h);
// 将 hash 到 invoker 的映射关系存储到 virtualInvokers 中,virtualInvokers 需要提供高效的查询操作,因此选用 TreeMap 作为存储结构
virtualInvokers.put(m, invoker);
}
}
}
}
public Invoker<T> select(Invocation invocation) {
// 将参数转为 key
String key = toKey(invocation.getArguments());
// 对参数 key 进行 md5 运算
byte[] digest = md5(key);
// 取 digest 数组的前四个字节进行 hash 运算,再将 hash 值传给 selectForKey 方法,寻找合适的 Invoker
return selectForKey(hash(digest, 0));
}
private Invoker<T> selectForKey(long hash) {
// 到 TreeMap 中查找第一个节点值大于或等于当前 hash 的 Invoker
Map.Entry<Long, Invoker<T>> entry = virtualInvokers.tailMap(hash, true).firstEntry();
// 如果 hash 大于 Invoker 在圆环上最大的位置,此时 entry = null,需要将 TreeMap 的头节点赋值给 entry
if (entry == null) {
entry = virtualInvokers.firstEntry();
}
// 返回 Invoker
return entry.getValue();
}
}
}
dubbo官方文档写得比较详细,且更新比较快,可以参考:http://dubbo.apache.org/zh-cn/docs/source_code_guide/loadbalance.html>