dubbo集群容错之loadbalance负载均衡
LoadBalance
首先查看 LoadBalance 接口
Invoker select(List> invokers, URL url, Invocation invocation) throws RpcException;
LoadBalance 定义了一个方法就是从 invokers 列表中选取一个
AbstractLoadBalance
AbstractLoadBalance 抽象类是所有负载均衡策略实现类的父类,实现了LoadBalance接口 的方法,同时提供抽象方法交由子类实现,
public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
if (invokers == null || invokers.size() == 0)
return null;
if (invokers.size() == 1)
return invokers.get(0);
return doSelect(invokers, url, invocation);
}
protected abstract <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation);
Random LoadBalance
- 随机,按权重设置随机概率。
- 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。
- 算法分析:通过下面这段代码,获取提供者服务的权重,然后计算出总权重,通过总权重计算出offset(根据0~累加的权重值 选出的一个随机数),然后再减去每个服务的的权重,如果发现小于0,则调用当前服务。
-
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; //计算所有权重是否一样 } } //循环检测,如果offset小于0,则返回该服务 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)); }
栗子:结合代码
3个服务 A,B,C 其中A的权重为1,B的权重为2,C的权重为3,那么invokers中的服务的顺序有6种情况,分别为ABC,BAC,ACB,CAB,BCA,CBA
-
此时的 totalWeight=1+2+3=6,此时offset=random.nextInt(totalWeight)=random.nextInt(6)=【0,1,2,3,4,5】中某个随机数
-
随机碰撞到ABC的情况表 offset offset offset offset offset offset invokers 0 1 2 3 4 5 ABC A B B C C C BAC B B A C C C ACB A C C C B B CAB C C C A B B BCA B B C C C A CBA C C C B B 文字描述:
当offset=0的时候,如果invokers=ABC 则调用的是A,因为0-1<0 ; 当offset=4的时候,会经过几个循环
①offset=4-1=3>0,此时offset=3,继续下一步
②offset=3-2=1>0,此时offset=1,继续下一步
③offset=1-3=-2<0,调用 此时调用C
-
RoundRobin LoadBalance
- 轮循,按公约后的权重设置轮循比率。
- 存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。
- 详细步骤:
-
1)获取轮询key 服务名+方法名
获取可供调用的invokers个数length
设置最大权重的默认值maxWeight=0
设置最小权重的默认值minWeight=Integer.MAX_VALUE
2)遍历所有Inokers,比较出得出maxWeight和minWeight
3)如果权重是不一样的
根据key获取自增序列
自增序列加一与最大权重取模默认得到currentWeigth
遍历所有invokers筛选出大于currentWeight的invokers
设置可供调用的invokers的个数length
4)自增序列加一并与length取模,从invokers获取invoker
-
public class RoundRobinLoadBalance extends AbstractLoadBalance { public static final String NAME = "roundrobin"; private final ConcurrentMap<String, AtomicPositiveInteger> sequences = 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(); //①获取轮询key(服务名+方法名) int length = invokers.size(); // Number of invokers ②获取可供调用的invokers个数 int maxWeight = 0; // The maximum weight 最大权重值 ③设置最大权重的默认值为0 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++) {//④遍历invokers,比较找出maxWeight和minWeight //获取权重值 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)); weightSum += weight; } } //⑤如果权重不一样 AtomicPositiveInteger sequence = sequences.get(key); if (sequence == null) { sequences.putIfAbsent(key, new AtomicPositiveInteger()); //⑥根据key获取自增序列 sequence = sequences.get(key); } int currentSequence = sequence.getAndIncrement(); if (maxWeight > 0 && minWeight < maxWeight) { //轮询当前值 //⑦自增序列加一与最大权重取模默认得到currentWeigth 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);//自增序列加一并与length取模,从invokers获取invoker } private static final class IntegerWrapper { private int value; public IntegerWrapper(int value) { this.value = value; } public int getValue() { return value; } public void setValue(int value) { this.value = value; } public void decrement() { this.value--; } } }
LeastActive LoadBalance
- 最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。
- 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。
-
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; // 最小活跃数调的下标 坐标id totalWeight = weight; // 权重 firstWeight = weight; // 第一个权重 sameWeight = true; // 重新设置为权重相同 } else if (active == leastActive) { // 如果活跃数相同,则记录权重 leastIndexs[leastCount++] = i; // 累计相同最小活跃数下标 totalWeight += weight; //累计总权重 // If every invoker has the same weight? if (sameWeight && i > 0 && weight != firstWeight) { sameWeight = false; } } } // assert(leastCount > 0) if (leastCount == 1) { // If we got exactly one invoker having the least active value, return this invoker directly. return invokers.get(leastIndexs[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. //如果权重不相同且权重大于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); } } // If all invokers have the same weight value or totalWeight=0, return evenly. //如果权重相同或权重为0则均等随机 return invokers.get(leastIndexs[random.nextInt(leastCount)]); } }
例子.每个服务有一个
活跃计数器
,那么我们假如有A,B两个提供者.计数均为0.当A提供者开始处理请求,该计数+1,此时A还没处理完,当处理完后则计数-1.而B请求接收到请求处理得很快.B处理完后A还没处理完,所以此时A,B的计数为1,0.那么当有新的请求来的时候,就会选择B提供者(B的活跃计数比A小) -
算法分析:
-
假设A,B,C,节点的最小活跃数分别是1,1,2权重为1,2,3,则
leastIndexs
(该数组是最小活跃数组)数组内容为[A,B].A,B的权重是1和2,所以调用A的概率为 1/(1+2) = 1/3,B的概率为 2/(1+2) = 2/3。 -
其中活跃数的变化是在
com.alibaba.dubbo.rpc.filter.ActiveLimitFilter
中,如果没有配置dubbo:reference
的actives
属性,默认是调用前活跃数+1,调用结束-1
ConsistentHash LoadBalance
- 一致性 Hash,相同参数的请求总是发到同一提供者。
- 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。
- 算法参见:http://en.wikipedia.org/wiki/Consistent_hashing
- 缺省只对第一个参数 Hash,如果要修改,请配置
<dubbo:parameter key="hash.arguments" value="0,1" />
- 缺省用 160 份虚拟节点,如果要修改,请配置
<dubbo:parameter key="hash.nodes" value="320" />
-
在业务开发中,我们常把数据持久化到数据库中。如果需要读取这些数据,除了直接从数据库中读取外,为了减轻数据库的访问压力以及提高访问速度,我们更多地引入缓存来对数据进行存取。读取数据的过程一般为:
1.这里通过描述图片加入缓存的数据读取过程
对于分布式缓存,不同机器上存储不同对象的数据。为了实现这些缓存机器的负载均衡,可以使用式子1来定位对象缓存的存储机器:
m = hash(图片名称) mod n ——式子1
三台服务器使用哈希对3 求余,那么一定是0,1,2.这样真好跟服务器编号相同,根据对应的图片名称就可找到对应图片缓存的服务器。
存在问题:当服务器数量发生变化时,会引起缓存雪崩,可能引起整体系统压力过大而崩溃;即便不发生上述情况,缓存的位置几乎都会发生改变,因此需要考虑怎么才能尽量减少受影响的缓存呢?
一致性哈希算法:
它也是通过取模的方法,但是不是对服务器的数量进行,而是对2^32取模。还是以咱们得到三台服务器为例,这次hash(服务器的IP地址) % 2^32,计算得到的结果一定是在0~2^32-1之间的某个整数,此时就用这个结果数代表服务器,那么三台服务器可以入如下图表示;
当我们需要缓存照片时,仍然使用图片名称作为key,计算hash(图片名称)%2^32,那么可以用橘色的点表示,但是到底缓存到哪台服务器上呢?A 为什么?因为从图片位置顺时针最先遇到的服务器就是A,因此将被缓存到服务器A上。
一致性哈希算法:
优点:当某台服务器出现down机,那么原来缓存在服务器B上的图片将被缓存至服务器C中,其中只有原来服务器B缓存的照片位置发生改变,其他照片未改动。
缺陷:hash环的偏斜---->我们理想化的吧三台服务器均匀的映射到hash环上,无奈理想很丰满,显示太骨感,想象的和实际情况往往反差很大。
实际上很有可能出现下面右图的情况:1,2,3,4,6图片均被混村到服务器A上了,服务器B上只有5图片,C服务器上压根没有缓存上图片,这就不开心了。如何解决这样的问题呢?一致性哈希算法提出了“虚拟节点”来解决
虚拟节点:
怎么能均匀分布到真hash环上呢,主要咱们只有三个服务器,没有那么多节点可以分布,想到了分身,记得大话西游牛魔王对打孙悟空,都是牛虱和猴子猴孙的PK,既然没有真正多余的物理服务器节点,那么只能讲现有的物理节点通过虚拟的方法复制出来,成为虚拟节点,加入到hash环中。
“虚拟节点”是“实际节点”(物理服务器)在hash环上的复制品,一个实际节点能对应过个虚拟节点。引入的虚拟节点越多,混村的分布月均衡。
配置
服务端服务级别
<dubbo:service interface="..." loadbalance="roundrobin" />
客户端服务级别
<dubbo:reference interface="..." loadbalance="roundrobin" />
服务端方法级别
<dubbo:service interface="...">
<dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:service>
客户端方法级别
<dubbo:reference interface="...">
<dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:reference>