本文分享内容如下
- 随机算法分析
- 轮询算法分析
- 最小活动数算法分析
- 一致性hash算法分析
下面逐个进行分析
随机算法分析
实现类:RandomLoadBalance
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
int length = invokers.size(); // Number of invokers
int totalWeight = 0; // The sum of weights
boolean sameWeight = true; // Every invoker has the same weight?
for (int i = 0; i < length; i++) {//计算全部invoker 权重和
int weight = getWeight(invokers.get(i), invocation);
totalWeight += weight; // Sum
if (sameWeight && i > 0
&& weight != getWeight(invokers.get(i - 1), invocation)) {
sameWeight = false;
}
}
if (totalWeight > 0 && !sameWeight) {//如果权重和大于0且多个invoker 权重不同
// If (not every invoker has the same weight & at least one invoker's weight>0), select randomly based on totalWeight.
int offset = random.nextInt(totalWeight);//在权重和范围内计算出随机权重值
// Return a invoker based on the random value.
for (int i = 0; i < length; i++) {//按照invokers的顺序,看随机权重值落在那个invoker的权重值范围内,返回这个invoker
offset -= getWeight(invokers.get(i), invocation);
if (offset < 0) {
return invokers.get(i);
}
}
}
// If all invokers have the same weight value or totalWeight=0, return evenly.
//如果所有的invoker有相同的权重值或权重和等于0,在invokers中随机选中一个返回。
return invokers.get(random.nextInt(length));
}
处理逻辑:先计算全部invoker 权重和,如果权重和大于0且多个invoker 权重不同,在权重和范围内计算出随机权重值,按照invokers的顺序,看随机权重值落在那个invoker的权重值范围内,返回这个invoker,//如果所有的invoker有相同的权重值或权重和等于0,在invokers中随机选中一个返回。
小结: 权重结合随机的算法实现。
轮询算法分析
实现类:RoundRobinLoadBalance
最简单的轮询算法就是返回下个节点,到达尾部从头部开始,这里的实现 还需要处理权重
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
//key eg: tuling.dubbo.server.UserService.getUser 简称为 完整方法名
int length = invokers.size(); // Number of invokers
int maxWeight = 0; // The maximum weight
int minWeight = Integer.MAX_VALUE; // The minimum weight
final LinkedHashMap<Invoker<T>, IntegerWrapper> invokerToWeightMap = new LinkedHashMap<Invoker<T>, IntegerWrapper>();
int weightSum = 0;
for (int i = 0; i < length; i++) {
int weight = getWeight(invokers.get(i), invocation);
maxWeight = Math.max(maxWeight, weight); // Choose the maximum weight
minWeight = Math.min(minWeight, weight); // Choose the minimum weight
if (weight > 0) {
invokerToWeightMap.put(invokers.get(i), new IntegerWrapper(weight));//记录每个invoker对应的权重
weightSum += weight;//计算总权重
}
}
//获取上次完整方法名对应的执行次数记录。
AtomicPositiveInteger sequence = sequences.get(key);
if (sequence == null) {
sequences.putIfAbsent(key, new AtomicPositiveInteger());
sequence = sequences.get(key);
}
//增加一次执行次数。
int currentSequence = sequence.getAndIncrement();
if (maxWeight > 0 && minWeight < maxWeight) { 如果最大权重大于0且 最小权重小于最大权重,
int mod = currentSequence % weightSum;算出权重轮询值
for (int i = 0; i < maxWeight; i++) {//轮询 ,权重小的先被淘汰
for (Map.Entry<Invoker<T>, IntegerWrapper> each : invokerToWeightMap.entrySet()) {
final Invoker<T> k = each.getKey();
final IntegerWrapper v = each.getValue();
if (mod == 0 && v.getValue() > 0) {
return k;
}
if (v.getValue() > 0) {
v.decrement();
mod--;
}
}
}
}
// Round robin 简单轮询
return invokers.get(currentSequence % length);
}
处理逻辑:获取最大权重值,最小权重值,记录每个invoker对应的权重,计算总权重,然后获取上次完整方法名对应的执行次数记录,增加一次执行次数。如果权重有效且值不同,通过执行次数取模总权重值获取权重轮询值, 循环轮询, 减少权重轮询值同时减少invoker的权重值,权重小被淘汰,直到权重轮询值被耗尽,返回这个invoker。如果权重相同或所有的invoker权重值都为0,执行简单了轮询。
最小活动数算法分析
实现类:LeastActiveLoadBalance
最少活跃数的含义
官方解释:最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差,使慢的机器收到更少。
例如,每个服务维护一个活跃数计数器。当A机器开始处理请求,该计数器加1,此时A还未处理完成。若处理完毕则计数器减1。而B机器接受到请求后很快处理完毕。那么A,B的活跃数分别是1,0。当又产生了一个新的请求,则选择B机器去执行(B活跃数最小),这样使块的机器A收到少的请求。
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)]);
}
}
处理逻辑:总的逻辑是选出最小活动数的invoker,如果有多个invoker相同,计算出他们的权重和,在权重和范围内计算出随机权重值,按照这些invokers的顺序,看随机权重值落在那个invoker的权重值范围内,返回这个invoker,如果权重相同或权重为0则均等随机从这些invokers选中一个。
一致性hash算法分析
先了解一下什么是一致性hash算法
假设一个图片服务,由于图片数据量比较大,如下图,将数据分布在4个节点 A,B,C,D上,这时希望其中一个节点挂掉,不会影响其他节点,一个简单的策略就是将A,B,C,D构成一个环形容灾集群,如果一个节点挂掉,则访问它相邻的下个节点。

这个方案看似美好,却有重大缺陷。如果A挂掉了 按照顺序 原来A的流量打到B上,这是 如果B扛不住,就会导致B,C,D相继挂掉,造成雪崩效应。那么怎么解决这个问题呢?
解决思路 让2个以上的节点分担 A的流量,如下图 设置 A的节点为3个虚拟节点 A1,A2,A3。假设A挂掉了 相当于 A1,A2,A3全部挂掉,A1的流程打到B上,A2的流程打到C上,A3的流程打到D上。 这样 A的流量由B,C,D分担,不容易发生雪崩效应。这个就是一致性hash算法要解决的问题。

要注意的是: 虚拟节点要与其他节点交错分布,否则解决不了雪崩效应,如下图的虚拟节点分布,还是会有雪崩效应。

那么虚拟节点是不是越多越好?肯定不是 ,如A,B,C,D 4个节点 A节点设置3个虚拟节点就可以了,多了没有什么实际作用。 每个节点的虚拟节点 等于 实际节点数据-1即可,注意要交错分布。一致性hash 负载策略通常用于 缓存服务,文件服务如 图片,音乐文件,文档。这些服务发生崩溃时通常不能通过扩展节点达到分担流量的效果,这些服务是有状态的服务。
dubbo一致性hash算法分析
主要配置
hash.arguments : 当进行调用时候根据调用方法的哪几个参数生成key,并根据key来通过一致性hash算法来选择调用结点。例如调用方法invoke(String s1,String s2); 若hash.arguments为0(默认值),则仅取invoke的参数1(s1), 若hash.arguments为(0,1),取invoke的 s1,s2 来生成hashCode。
hash.nodes: 为结点的副本数。
缺省只对第一个参数Hash,如果要修改,请配置
<dubbo:parameter key="hash.arguments" value="0,1" />
缺省用160份虚拟节点,如果要修改,请配置
<dubbo:parameter key="hash.nodes" value="320" />
实现类分析
实现类:ConsistentHashLoadBalance
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// ServiceKey就是 接口名,如:tuling.dubbo.server.UserService
String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
//key eg: tuling.dubbo.server.UserService.getUser
int identityHashCode = System.identityHashCode(invokers);
ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
if (selector == null || selector.identityHashCode != identityHashCode) {
selectors.put(key, new ConsistentHashSelector<T>(invokers, invocation.getMethodName(), identityHashCode));
selector = (ConsistentHashSelector<T>) selectors.get(key);
}
return selector.select(invocation);//依赖ConsistentHashSelector
}
ConsistentHashSelector分析
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();
this.replicaNumber = url.getMethodParameter(methodName, "hash.nodes", 160);//获取虚拟节点数 默认160。
String[] index = Constants.COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, "hash.arguments", "0"));//获取hash.arguments 并解析。
argumentIndex = new int[index.length];
for (int i = 0; i < index.length; i++) {
argumentIndex[i] = Integer.parseInt(index[i]);
}
//创建虚拟节点
//为每一个invoker生成replicaNumber 虚拟节点
for (Invoker<T> invoker : invokers) {
String address = invoker.getUrl().getAddress();
for (int i = 0; i < replicaNumber / 4; i++) {
byte[] digest = md5(address + i);//根据MD5算法为每4个节点生成一个信息摘要,长度为16字节 128位
for (int h = 0; h < 4; h++) {
long m = hash(digest, h); //将128位分为4部分, 抽取 其中 的 0~31 ,32~63,64~95,96~128 位,每32位转换成long类型作为虚拟节点的key. 这个key的高32位全部为0
virtualInvokers.put(m, invoker);
}
}
}
}
// number值传入的有 0,1,2,3 ,依次获取digest中的4个字节,拼接成long类型 (高32位全部为0)。
// 这是个截取4个字节拼装long值的方法,没看出有hash作用。
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;
}
这里的md5,hash 是为了什么?
md5 获取 执行字符串的唯一标识 byte数组。
hash在这里的作用是取出32位组成一个long值,看不出其他作用。
hash的虚拟节点必须交错分布,否则 容易有雪崩效应,这里的算法是怎么处理的??
没有看出有交错配置的实现。有待以后研究
public Invoker<T> select(Invocation invocation) {
String key = toKey(invocation.getArguments());//根据方法参数 生成key
byte[] digest = md5(key);
return selectForKey(hash(digest, 0));//这里只取 0~31作为获取节点的hash值
}
private String toKey(Object[] args) {//拼接方法参数,作为key
StringBuilder buf = new StringBuilder();
for (int i : argumentIndex) {
if (i >= 0 && i < args.length) {
buf.append(args[i]);
}
}
return buf.toString();
}
private Invoker<T> selectForKey(long hash) {
Map.Entry<Long, Invoker<T>> entry = virtualInvokers.tailMap(hash, true).firstEntry();
if (entry == null) {
entry = virtualInvokers.firstEntry();
}
return entry.getValue();
}
selectForKey 依赖TreeMap的排序功能获取到了 key大于hash值的第一个节点。
知识点:
public SortedMap<K,V> tailMap(K fromKey)
截取Key大于等于fromKey的所有元素
public NavigableMap<K,V> tailMap(K fromKey, boolean inclusive)
当inclusive为true时,截取Key大于等于fromKey的所有元素,否则截取Key大于fromKey的所有元素
总结:dubbo的一致性hash算法 依赖选择器ConsistentHashSelector, 它创建的时候初始化了虚拟节点,放入TreeMap, 选择节点的时候 根据参数生成key,依赖TreeMap排序功能 找出节点。
参考资料:
treemap函数常用方法 讲解:https://www.cnblogs.com/xiaostudy/p/9511910.html
一致性哈希负载均衡算法的探讨:https://lexburner.github.io/consistent-hash-lb/
深入探讨hash算法。
Dubbo负载均衡:一致性Hash的实现分析:https://blog.csdn.net/revivedsun/article/details/71022871
本文详细分析了Dubbo的四种负载均衡算法:随机、轮询、最小活动数和一致性Hash。随机算法根据权重和进行随机选择;轮询算法考虑权重差异进行轮询;最小活动数算法选择活跃数最小的节点;一致性Hash算法解决节点失效后的流量分布问题,防止雪崩效应。每种算法的实现逻辑和关键点都有详细说明。
264

被折叠的 条评论
为什么被折叠?



