Dubbo提供了4中负载均衡策略
① Random LoadBalance(随机均衡算法)
② RoundRobin LoadBalance(权重轮询均衡算法)
③ LeastAction LoadBalance(最少活跃调用数均衡算法)
④ ConsistentHash LoadBalance(一致性Hash均衡算法)
缺省时为Random随机调用
1.Random LoadBalance(随机均衡算法)
官方:
随机,按权重设置随机概率。
在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。
2.RoundRobin LoadBalance(权重轮询均衡算法)
官方:
轮询,按公约后的权重设置轮询比率。
存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调用第二台时就卡在那里,久而久之,所有请求都卡在第二台机器上。
Round Robin轮询算法,是按照公约后的权重设置轮询比率,即权重轮询算法(Weighted Round-Robin),它是基于轮询算法改进而来的
轮询调度算法的原理是:每一次把来自用户的请求轮流分配给内部中的服务器。如从1开始,一直到N(其中N是内部服务器的总个数),然后重新开始循环。
该算法的优点:
简洁,无需记录当前所有连接的状态,所以它是一种无状态调度。
缺点:
轮询调度算法假设所有服务器的处理性能都相同,不关心每台服务器的当前连接数和相应速度。当请求服务间隔时间变化比较大时,轮询调度算法容易导致服务器间的负载不平衡。
所以此种均衡算法适合于服务器组中所有服务器都有相同的软硬件配置并且平均服务请求相对均衡的情况。但是在实际情况中,可能并不是这种情况。由于每台服务器的配置、安装的业务应用等不同,其处理能力会不一样。所以,我们根据服务器的不同处理能力,给每个服务器分配不同的权值,使其能够接受相应权值数的服务请求。
权重轮询调度算法的流程:
假设有一组服务器S={S0,S1,...Sn-1}
W(Si)表示服务器Si的权值
指示变量i表示上一次选择的服务器
指示变量cw表示当前调度的权值
max(S)表示集合S中所有服务的最大权值
gcw(S)表示集合S中所有服务器权值的最大公约数
变量i初始化为-1,cw初始化为0
while (true) { i = (i + 1) mod n; if (i == 0) { cw = cw - gcd(S); if (cw <= 0) { cw = max(S); if (cw == 0) return NULL; } } if (W(Si) >= cw) return Si; } |
这种算法的逻辑实现如图2所示,图中我们假定四台服务器的处理能力为3:1:1:1
由于权重轮询调度算法考虑到了不同服务器的处理能力,所以这种均衡算法能确保高性能的服务器得到更多的使用率,避免低性能的服务器负载过重。所以在实际应用中比较常见。
3.LeastAction LoadBalance(最少活跃调用数均衡算法)
官方:
最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。
使慢的提供者收到更少请求,因为越慢的提供者的调用前后技术差会越大。
4.ConsistentHash LoadBalance(一致性Hash均衡算法)
官方
一致性hash,相同参数的请求总是发送到同一提供者
当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其他提供者,不会引起剧烈变动弄。
一致性hash算法可以解决服务提供者的增加、移除及挂掉时的情况,能尽可能小的改变已存在key的映射关系,尽可能的满足单调性的要求。
一致性hash通过构建虚拟节点,能尽可能避免分配失衡,具有很好的平衡性。
以下示例假设对象(Object)就相当于Client发的请求,cache相当于服务提供者。
环形hash空间
考虑通常的hash算法都是将value映射到一个32位的key值,也即是0~2^32-1次方的数值空间。可以将这个空间想象成一个首(0)尾(2^32-1)相接的圆环,如:
把对象映射到hash空间
接下来考虑四个对象object1~object4,通过hash函数计算出hash值key在环上的分布,如图:
hash(object1) = key1;
...
hash(object4) = key4;
把cache映射到hash空间
Consistent hashing的基本思想就是将对象和cache都映射到同一个hash数值空间中,并且使用相同的hash算法
假设现在有A,B,C共3台cache,那么映射结果如下图所示,它们在hash空间中,以对应的hash值排序
hash(cache A) = key A
...
hash(cache C) = key C
cache的hash计算,一般的方法可以使用cache机器的ip地址或者机器名作为hash输入。
把对象映射到cache
现在cache和对象都已经通过同一个hash算法映射到hash数值空间中来了,接下来要考虑的就是如何将对象映射到cache上面了。
在这个环形空间中,如果沿着顺时针方向从对象的key值出发,知道遇到一个cache,那么就将该对象存储在这个cache上,因为对象和cache的hash值是固定的,因此这个cache必然是唯一和固定的,这样就找到了对象和cache的映射方法。
依据上面的方法,对象object1将被存储到cache A上,object2和object3对应到cache C,object4对应到cache B
考察cache的变动
一致性hash算法可以解决服务提供者的增加、移除及挂掉时的情况,能尽可能小的改变已存在key的映射关系,尽可能满足单调性的要求。
1)移除cache
考虑假设cache B挂掉了,根据上面的映射方法,这时受影响的将仅是那些yan沿cache B逆时针便利直到下一个cache(cache C)之间的对象,也即是本来映射到cache B上的那些对象。
因此,这里仅需要变动object4,将其重新映射到cache C上即可
2)添加cache
再考虑添加一台新的cache D的情况,假设在这个环形hash空间中cache D被映射在对象object2和object3之间,这时受影响的将仅是那些沿cache D逆时针遍历直到下一个cache(cache B)之间的对象(它们本来是映射到cache C上的对象的一部分),将这些对象重新映射到cache D上即可
因此,这里仅需要变动对象object2,将其重新映射到cache D上。
虚拟节点
考虑hash算法的另一个指标是平衡性(Balance),定义如下:
平衡性是指hash的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。
hash算法并不是保证绝对的平衡,如果cache较少的话,对象并不能被均匀的映射到cache上,比如在上面的例子中,仅部署cache A和cache C的情况下,在4个对象中,cache A仅存储了object1,而cache C则存储了object2、object3、object4,分布式很不均衡的。
为了解决这种情况,consistent hashing引入了虚拟节点的概念,它可以定义如下:
“虚拟节点”(virtual node)是实际节点在hash空间的复制品(repllica),一个节点对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在hash空间中以hash值排列。
仍以仅部署cache A和cache C的情况为例,此情况cache分布并不均匀。现在我们引入虚拟节点,并设置“复制个数”为2,这就意味着一共会存在4个“虚拟节点”,cache A1,cache A2代表了cache A;cache C1,cache C2代表了cache C;假设一种比较理想的情况:
此时,对象到虚拟节点的映射关系为
object 1 -> cache A2;object 2 -> cache A1;object 3 -> cache C1;object 4 -> cache C2;
因此object1和object2都映射到了cache A上,而object3和object4映射到了cache C上,平衡性有了很大提高。
引入“虚拟节点”后映射关系从 { 对象 -> 节点 } 转换到了 { 对象 -> 虚拟节点 } 查询物体所在的cache时的映射关系为:
“虚拟节点”的hash计算可以采用对应节点的ip地址加数字后缀的方式,例如,假设cache A的ip地址为202.168.14.241
引入虚拟节点前,计算cache A的hash值:
hash("202.168.14.241");
引入虚拟节点后,计算虚拟节点cache A1和 cache A2的hash值
hash("202.168.14.241#1");//cache A1
hash("202.168.14.241#2");//cache A2
附录:
1.RandomLoadBalance算法
public class RandomLoadBalance extends AbstractLoadBalance {
public static final String NAME = "random";
private final Random random = new Random();
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
int length = invokers.size(); // 总个数
int totalWeight = 0; // 总权重
boolean sameWeight = true; // 权重是否都一样
for (int i = 0; i < length; i++) {
int weight = getWeight(invokers.get(i), invocation);
totalWeight += weight; // 累计总权重
if (sameWeight && i > 0
&& weight != getWeight(invokers.get(i - 1), invocation)) {
sameWeight = false; // 计算所有权重是否一样
}
}
if (totalWeight > 0 && ! sameWeight) {
// 如果权重不相同且权重大于0则按总权重数随机
int offset = random.nextInt(totalWeight);
// 并确定随机值落在哪个片断上
for (int i = 0; i < length; i++) {
offset -= getWeight(invokers.get(i), invocation);
if (offset < 0) {
return invokers.get(i);
}
}
}
// 如果权重相同或权重为0则均等随机
return invokers.get(random.nextInt(length));
}
}
2.RoundRobinLoadBalance算法
public class RoundRobinLoadBalance extends AbstractLoadBalance {
public static final String NAME = "roundrobin";
private final ConcurrentMap<String, AtomicPositiveInteger> sequences = new ConcurrentHashMap<String, AtomicPositiveInteger>();
private final ConcurrentMap<String, AtomicPositiveInteger> weightSequences = new ConcurrentHashMap<String, AtomicPositiveInteger>();
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
int length = invokers.size(); // 总个数
int maxWeight = 0; // 最大权重
int minWeight = Integer.MAX_VALUE; // 最小权重
for (int i = 0; i < length; i++) {
int weight = getWeight(invokers.get(i), invocation);
maxWeight = Math.max(maxWeight, weight); // 累计最大权重
minWeight = Math.min(minWeight, weight); // 累计最小权重
}
if (maxWeight > 0 && minWeight < maxWeight) { // 权重不一样
AtomicPositiveInteger weightSequence = weightSequences.get(key);
if (weightSequence == null) {
weightSequences.putIfAbsent(key, new AtomicPositiveInteger());
weightSequence = weightSequences.get(key);
}
int currentWeight = weightSequence.getAndIncrement() % maxWeight;
List<Invoker<T>> weightInvokers = new ArrayList<Invoker<T>>();
for (Invoker<T> invoker : invokers) { // 筛选权重大于当前权重基数的Invoker
if (getWeight(invoker, invocation) > currentWeight) {
weightInvokers.add(invoker);
}
}
int weightLength = weightInvokers.size();
if (weightLength == 1) {
return weightInvokers.get(0);
} else if (weightLength > 1) {
invokers = weightInvokers;
length = invokers.size();
}
}
AtomicPositiveInteger sequence = sequences.get(key);
if (sequence == null) {
sequences.putIfAbsent(key, new AtomicPositiveInteger());
sequence = sequences.get(key);
}
// 取模轮循
return invokers.get(sequence.getAndIncrement() % length);
}
}
3.LeastActionLoadBalance算法
public class LeastActiveLoadBalance extends AbstractLoadBalance {
public static final String NAME = "leastactive";
private final Random random = new Random();
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
int length = invokers.size(); // 总个数
int leastActive = -1; // 最小的活跃数
int leastCount = 0; // 相同最小活跃数的个数
int[] leastIndexs = new int[length]; // 相同最小活跃数的下标
int totalWeight = 0; // 总权重
int firstWeight = 0; // 第一个权重,用于于计算是否相同
boolean sameWeight = true; // 是否所有权重相同
for (int i = 0; i < length; i++) {
Invoker<T> invoker = invokers.get(i);
int active = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive(); // 活跃数
int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.WEIGHT_KEY, Constants.DEFAULT_WEIGHT); // 权重
if (leastActive == -1 || active < leastActive) { // 发现更小的活跃数,重新开始
leastActive = active; // 记录最小活跃数
leastCount = 1; // 重新统计相同最小活跃数的个数
leastIndexs[0] = i; // 重新记录最小活跃数下标
totalWeight = weight; // 重新累计总权重
firstWeight = weight; // 记录第一个权重
sameWeight = true; // 还原权重相同标识
} else if (active == leastActive) { // 累计相同最小的活跃数
leastIndexs[leastCount ++] = i; // 累计相同最小活跃数下标
totalWeight += weight; // 累计总权重
// 判断所有权重是否一样
if (sameWeight && i > 0
&& weight != firstWeight) {
sameWeight = false;
}
}
}
// assert(leastCount > 0)
if (leastCount == 1) {
// 如果只有一个最小则直接返回
return invokers.get(leastIndexs[0]);
}
if (! sameWeight && totalWeight > 0) {
// 如果权重不相同且权重大于0则按总权重数随机
int offsetWeight = random.nextInt(totalWeight);
// 并确定随机值落在哪个片断上
for (int i = 0; i < leastCount; i++) {
int leastIndex = leastIndexs[i];
offsetWeight -= getWeight(invokers.get(leastIndex), invocation);
if (offsetWeight <= 0)
return invokers.get(leastIndex);
}
}
// 如果权重相同或权重为0则均等随机
return invokers.get(leastIndexs[random.nextInt(leastCount)]);
}
}
4.ConsistentHashLoadBalance算法
public class ConsistentHashLoadBalance extends AbstractLoadBalance {
private final ConcurrentMap<String, ConsistentHashSelector<?>> selectors = new ConcurrentHashMap<String, ConsistentHashSelector<?>>();
@SuppressWarnings("unchecked")
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
int identityHashCode = System.identityHashCode(invokers);
ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
if (selector == null || selector.getIdentityHashCode() != identityHashCode) {
selectors.put(key, new ConsistentHashSelector<T>(invokers, invocation.getMethodName(), identityHashCode));
selector = (ConsistentHashSelector<T>) selectors.get(key);
}
return selector.select(invocation);
}
private static final class ConsistentHashSelector<T> {
private final TreeMap<Long, Invoker<T>> virtualInvokers;
private final int replicaNumber;
private final int identityHashCode;
private final int[] argumentIndex;
public ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {
this.virtualInvokers = new TreeMap<Long, Invoker<T>>();
this.identityHashCode = System.identityHashCode(invokers);
URL url = invokers.get(0).getUrl();
this.replicaNumber = url.getMethodParameter(methodName, "hash.nodes", 160);
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) {
for (int i = 0; i < replicaNumber / 4; i++) {
byte[] digest = md5(invoker.getUrl().toFullString() + i);
for (int h = 0; h < 4; h++) {
long m = hash(digest, h);
virtualInvokers.put(m, invoker);
}
}
}
}
public int getIdentityHashCode() {
return identityHashCode;
}
public Invoker<T> select(Invocation invocation) {
String key = toKey(invocation.getArguments());
byte[] digest = md5(key);
Invoker<T> invoker = sekectForKey(hash(digest, 0));
return invoker;
}
private String toKey(Object[] args) {
StringBuilder buf = new StringBuilder();
for (int i : argumentIndex) {
if (i >= 0 && i < args.length) {
buf.append(args[i]);
}
}
return buf.toString();
}
private Invoker<T> sekectForKey(long hash) {
Invoker<T> invoker;
Long key = hash;
if (!virtualInvokers.containsKey(key)) {
SortedMap<Long, Invoker<T>> tailMap = virtualInvokers.tailMap(key);
if (tailMap.isEmpty()) {
key = virtualInvokers.firstKey();
} else {
key = tailMap.firstKey();
}
}
invoker = virtualInvokers.get(key);
return invoker;
}
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[0 + number * 4] & 0xFF))
& 0xFFFFFFFFL;
}
private byte[] md5(String value) {
MessageDigest md5;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e.getMessage(), e);
}
md5.reset();
byte[] bytes = null;
try {
bytes = value.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e.getMessage(), e);
}
md5.update(bytes);
return md5.digest();
}
}
}