负载均衡算法
在集群负载均衡时,Dubbo提供了4种均衡策略,如:RandomLoadBalance(随机均衡算法)、;RoundRobinLoadBalance(权重轮循均衡算法)、LeastActionLoadBalance(最少活跃调用数均衡算法)、ConsistentHashLoadBalance(一致性Hash均衡算法)。缺省时为Random随机调用。这四种算法的原理简要介绍如下:
Round-Robin既是轮询算法,是按照公约后的权重设置轮询比率,即权重轮询算法(WeightedRound-Robin),它是基于轮询算法改进而来的。这里之所以写RoundRobin是为了跟Dubbo中的内容保持一致。
轮询调度算法的原理是:每一次把来自用户的请求轮流分配给内部中的服务器。如:从1开始,一直到N(其中,N是内部服务器的总个数),然后重新开始循环。
该算法的优点:
其简洁性,它无需记录当前所有连接的状态,所以它是一种无状态调度。
该算法的缺点:
轮询调度算法假设所有服务器的处理性能都相同,不关心每台服务器的当前连接数和响应速度。当请求服务间隔时间变化比较大时,轮询调度算法容易导致服务器间的负载不平衡。
所以此种均衡算法适合于服务器组中的所有服务器都有相同的软硬件配置并且平均服务请求相对均衡的情况。但是,在实际情况中,可能并不是这种情况。由于每台服务器的配置、安装的业务应用等不同,其处理能力会不一样。所以,我们根据服务器的不同处理能力,给每个服务器分配不同的权值,使其能够接受相应权值数的服务请求。
权重轮询调度算法流程
假设有一组服务器S={S0,S1,…,Sn-1},W(Si)表示服务器Si的权值,一个指示变量i表示上一次选择的服务器,指示变量cw表示当前调度的权值,max(S)表示集合S中所有服务器的最大权值,gcd(S)表示集合S中所有服务器权值的最大公约数。变量i初始化为-1,cw初始化为零。其算法如下:
while(true){ i=(i+1)modn; if(i==0){ cw=cw-gcd(S); if(cw<=0){ cw=max(S); if(cw==0) returnNULL; } } if(W(Si)>=cw) returnSi; } |
这种算法的逻辑实现如图2所示,图中我们假定四台服务器的处理能力为3:1:1:1。
图1权重轮询调度实现逻辑图示
由于权重轮询调度算法考虑到了不同服务器的处理能力,所以这种均衡算法能确保高性能的服务器得到更多的使用率,避免低性能的服务器负载过重。所以,在实际应用中比较常见。
一致性Hash,相同参数的请求总是发到同一个提供者。一:一致性Hash算法可以解决服务提供者的增加、移除及挂掉时的情况,能尽可能小的改变已存在key映射关系,尽可能的满足单调性的要求。二:一致性Hash通过构建虚拟节点,能尽可能避免分配失衡,具有很好的平衡性。
一致性Hash下面就来按照5个步骤简单讲讲consistenthash算法的基本原理。因为以下资料来自于互联网,现说明几点:一、下面例子中的对象就相当于Client发的请求,cache相当于服务提供者。
环形hash空间
考虑通常的hash算法都是将value映射到一个32为的key值,也即是0~2^32-1次方的数值空间;我们可以将这个空间想象成一个首(0)尾(2^32-1)相接的圆环,如下面图2所示的那样。
图2环形hash空间
把对象映射到hash空间
接下来考虑4个对象object1~object4,通过hash函数计算出的hash值key在环上的分布如图3所示。
hash(object1)=key1;
……
hash(object4)=key4;
图34个对象的key值分布
把cache映射到hash空间
Consistenthashing的基本思想就是将对象和cache都映射到同一个hash数值空间中,并且使用相同的hash算法。
假设当前有A,B和C共3台cache,那么其映射结果将如图4所示,他们在hash空间中,以对应的hash值排列。
hash(cacheA)=keyA;
……
hash(cacheC)=keyC;
图4cache和对象的key值分布
说到这里,顺便提一下cache的hash计算,一般的方法可以使用cache机器的IP地址或者机器名作为hash输入。
把对象映射到cache
现在cache和对象都已经通过同一个hash算法映射到hash数值空间中了,接下来要考虑的就是如何将对象映射到cache上面了。
在这个环形空间中,如果沿着顺时针方向从对象的key值出发,直到遇见一个cache,那么就将该对象存储在这个cache上,因为对象和cache的hash值是固定的,因此这个cache必然是唯一和确定的。这样不就找到了对象和cache的映射方法了吗!
依然继续上面的例子(参见图4),那么根据上面的方法,对象object1将被存储到cacheA上;object2和object3对应到cacheC;object4对应到cacheB;
考察cache的变动
前面讲过,一致性Hash算法可以解决服务提供者的增加、移除及挂掉时的情况,能尽可能小的改变已存在key映射关系,尽可能的满足单调性的要求。
移除cache
考虑假设cacheB挂掉了,根据上面讲到的映射方法,这时受影响的将仅是那些沿cacheB逆时针遍历直到下一个cache(cacheC)之间的对象,也即是本来映射到cacheB上的那些对象。
因此这里仅需要变动对象object4,将其重新映射到cacheC上即可;参见图5。
图5CacheB被移除后的cache映射
添加cache
再考虑添加一台新的cacheD的情况,假设在这个环形hash空间中,cacheD被映射在对象object2和object3之间。这时受影响的将仅是那些沿cacheD逆时针遍历直到下一个cache(cacheB)之间的对象(它们是也本来映射到cacheC上对象的一部分),将这些对象重新映射到cacheD上即可。
因此这里仅需要变动对象object2,将其重新映射到cacheD上;参见图6。
图6添加cacheD后的映射关系
虚拟节点
考虑Hash算法的另一个指标是平衡性(Balance),定义如下:
平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。
hash算法并不是保证绝对的平衡,如果cache较少的话,对象并不能被均匀的映射到cache上,比如在上面的例子中,仅部署cacheA和cacheC的情况下,在4个对象中,cacheA仅存储了object1,而cacheC则存储了object2、object3和object4;分布是很不均衡的。
为了解决这种情况,consistenthashing引入了“虚拟节点”的概念,它可以如下定义:
“虚拟节点”(virtualnode)是实际节点在hash空间的复制品(replica),一实际个节点对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在hash空间中以hash值排列。
仍以仅部署cacheA和cacheC的情况为例,在图5中我们已经看到,cache分布并不均匀。现在我们引入虚拟节点,并设置“复制个数”为2,这就意味着一共会存在4个“虚拟节点”,cacheA1,cacheA2代表了cacheA;cacheC1,cacheC2代表了cacheC;假设一种比较理想的情况,参见图7。
图7引入“虚拟节点”后的映射关系
此时,对象到“虚拟节点”的映射关系为:
objec1->cacheA2;objec2->cacheA1;objec3->cacheC1;objec4->cacheC2;
因此对象object1和object2都被映射到了cacheA上,而object3和object4映射到了cacheC上;平衡性有了很大提高。
引入“虚拟节点”后,映射关系就从{对象->节点}转换到了{对象->虚拟节点}。查询物体所在cache时的映射关系如图8所示。
图8查询对象所在cache
“虚拟节点”的hash计算可以采用对应节点的IP地址加数字后缀的方式。例如假设cacheA的IP地址为202.168.14.241。
引入“虚拟节点”前,计算cacheA的hash值:
Hash(“202.168.14.241”);
引入“虚拟节点”后,计算“虚拟节”点cacheA1和cacheA2的hash值:
Hash(“202.168.14.241#1”);//cacheA1
Hash(“202.168.14.241#2”);//cacheA2
RandomLoadBalance与LeastActionLoadBalance算法比较简单,可以参照Dubbo文档中的给的描述及后面代码附录。Dubbo文档截图如下图9所示:
图9负载均衡算法
publicclassRandomLoadBalanceextendsAbstractLoadBalance{
publicstaticfinalStringNAME="random";
privatefinalRandomrandom=newRandom();
protected
intlength=invokers.size();//总个数
inttotalWeight=0;//总权重
booleansameWeight=true;//权重是否都一样
for(inti=0;i intweight=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则按总权重数随机 intoffset=random.nextInt(totalWeight); //并确定随机值落在哪个片断上 for(inti=0;i offset-=getWeight(invokers.get(i),invocation); if(offset<0){ returninvokers.get(i); } } } //如果权重相同或权重为0则均等随机 returninvokers.get(random.nextInt(length)); } } publicclassRoundRobinLoadBalanceextendsAbstractLoadBalance{ publicstaticfinalStringNAME="roundrobin"; privatefinalConcurrentMap privatefinalConcurrentMap protected Stringkey=invokers.get(0).getUrl().getServiceKey()+"."+invocation.getMethodName(); intlength=invokers.size();//总个数 intmaxWeight=0;//最大权重 intminWeight=Integer.MAX_VALUE;//最小权重 for(inti=0;i intweight=getWeight(invokers.get(i),invocation); maxWeight=Math.max(maxWeight,weight);//累计最大权重 minWeight=Math.min(minWeight,weight);//累计最小权重 } if(maxWeight>0&&minWeight AtomicPositiveIntegerweightSequence=weightSequences.get(key); if(weightSequence==null){ weightSequences.putIfAbsent(key,newAtomicPositiveInteger()); weightSequence=weightSequences.get(key); } intcurrentWeight=weightSequence.getAndIncrement()%maxWeight; List for(Invoker if(getWeight(invoker,invocation)>currentWeight){ weightInvokers.add(invoker); } } intweightLength=weightInvokers.size(); if(weightLength==1){ returnweightInvokers.get(0); }elseif(weightLength>1){ invokers=weightInvokers; length=invokers.size(); } } AtomicPositiveIntegersequence=sequences.get(key); if(sequence==null){ sequences.putIfAbsent(key,newAtomicPositiveInteger()); sequence=sequences.get(key); } //取模轮循 returninvokers.get(sequence.getAndIncrement()%length); } } publicclassLeastActiveLoadBalanceextendsAbstractLoadBalance{ publicstaticfinalStringNAME="leastactive"; privatefinalRandomrandom=newRandom(); protected intlength=invokers.size();//总个数 intleastActive=-1;//最小的活跃数 intleastCount=0;//相同最小活跃数的个数 int[]leastIndexs=newint[length];//相同最小活跃数的下标 inttotalWeight=0;//总权重 intfirstWeight=0;//第一个权重,用于于计算是否相同 booleansameWeight=true;//是否所有权重相同 for(inti=0;i Invoker intactive=RpcStatus.getStatus(invoker.getUrl(),invocation.getMethodName()).getActive();//活跃数 intweight=invoker.getUrl().getMethodParameter(invocation.getMethodName(),Constants.WEIGHT_KEY,Constants.DEFAULT_WEIGHT);//权重 if(leastActive==-1||active leastActive=active;//记录最小活跃数 leastCount=1;//重新统计相同最小活跃数的个数 leastIndexs[0]=i;//重新记录最小活跃数下标 totalWeight=weight;//重新累计总权重 firstWeight=weight;//记录第一个权重 sameWeight=true;//还原权重相同标识 }elseif(active==leastActive){//累计相同最小的活跃数 leastIndexs[leastCount++]=i;//累计相同最小活跃数下标 totalWeight+=weight;//累计总权重 //判断所有权重是否一样 if(sameWeight&&i>0 &&weight!=firstWeight){ sameWeight=false; } } } //assert(leastCount>0) if(leastCount==1){ //如果只有一个最小则直接返回 returninvokers.get(leastIndexs[0]); } if(!sameWeight&&totalWeight>0){ //如果权重不相同且权重大于0则按总权重数随机 intoffsetWeight=random.nextInt(totalWeight); //并确定随机值落在哪个片断上 for(inti=0;i<leastCount;i++){ intleastIndex=leastIndexs[i]; offsetWeight-=getWeight(invokers.get(leastIndex),invocation); if(offsetWeight<=0) returninvokers.get(leastIndex); } } //如果权重相同或权重为0则均等随机 returninvokers.get(leastIndexs[random.nextInt(leastCount)]); } } publicclassConsistentHashLoadBalanceextendsAbstractLoadBalance{ privatefinalConcurrentMap @SuppressWarnings("unchecked") @Override protected Stringkey=invokers.get(0).getUrl().getServiceKey()+"."+invocation.getMethodName(); intidentityHashCode=System.identityHashCode(invokers); ConsistentHashSelector if(selector==null||selector.getIdentityHashCode()!=identityHashCode){ selectors.put(key,newConsistentHashSelector selector=(ConsistentHashSelector } returnselector.select(invocation); } privatestaticfinalclassConsistentHashSelector privatefinalTreeMap privatefinalintreplicaNumber; privatefinalintidentityHashCode; privatefinalint[]argumentIndex; publicConsistentHashSelector(List this.virtualInvokers=newTreeMap this.identityHashCode=System.identityHashCode(invokers); URLurl=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=newint[index.length]; for(inti=0;i argumentIndex[i]=Integer.parseInt(index[i]); } for(Invoker for(inti=0;i<replicaNumber/4;i++){ byte[]digest=md5(invoker.getUrl().toFullString()+i); for(inth=0;h<4;h++){ longm=hash(digest,h); virtualInvokers.put(m,invoker); } } } } publicintgetIdentityHashCode(){ returnidentityHashCode; } publicInvoker Stringkey=toKey(invocation.getArguments()); byte[]digest=md5(key); Invoker returninvoker; } privateStringtoKey(Object[]args){ StringBuilderbuf=newStringBuilder(); for(inti:argumentIndex){ if(i>=0&&i buf.append(args[i]); } } returnbuf.toString(); } privateInvoker Invoker Longkey=hash; if(!virtualInvokers.containsKey(key)){ SortedMap if(tailMap.isEmpty()){ key=virtualInvokers.firstKey(); }else{ key=tailMap.firstKey(); } } invoker=virtualInvokers.get(key); returninvoker; } privatelonghash(byte[]digest,intnumber){ 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; } privatebyte[]md5(Stringvalue){ MessageDigestmd5; try{ md5=MessageDigest.getInstance("MD5"); }catch(NoSuchAlgorithmExceptione){ thrownewIllegalStateException(e.getMessage(),e); } md5.reset(); byte[]bytes=null; try{ bytes=value.getBytes("UTF-8"); }catch(UnsupportedEncodingExceptione){ thrownewIllegalStateException(e.getMessage(),e); } md5.update(bytes); returnmd5.digest(); } } }2、RoundRobinLoadBalance算法
3、LeastActionLoadBalance算法
4、ConsistentHashLoadBalance算法