最近看Dubbo源码时,看到了相关算法,以此为问题出发点,总结下这方面相关的常见算法。(本文和Dubbo源码并没有太大的关系,只是属于这个系列中遇到的知识总结)
负载均衡的目的是什么?
讨论负载均衡,那么归根结底其要解决的问题是什么?当一台服务器的承受能力达到上限时,那么就需要多台服务器来组成集群,提升应用整体的吞吐量,那么这个时候就涉及到如何合理分配客户端请求到集群中不同的机器,这个过程就叫做负载均衡,当然这也是负载均衡要解决的问题。由于服务器之间的处理能力差异,因此每台服务器需要有自己的权重比例,为了更好的描述后面所涉及到的算法,因此抽象出一个类,代表集群中的服务器。接口代表负载均衡算法。
publicclassServer{
/**
* 服务器地址
*/
privateStringaddress;
/**
* 服务器权重
*/
privateIntegerweight;
}
publicinterfaceLoadBalance{
/**
* 根据负载均衡算法选择最合适的一个Server
* @param servers 客户端集合
* @return result
*/
Serverselect(Listservers);
}
权重随机算法
单纯的随机算法通过伪随机数来保证请求均匀的分布到对应的Server上,但是其忽略了每一个服务器处理能力的差异,这样就导致处理能力差的服务可能因为这种绝对的均衡策略而崩掉,改进策略就是根据权重占比随机。算法很简单,就是一根数轴。然后利用伪随机数产生点,确定点落在了哪个区域从而选择对应的。清单1: 权重随机算法
@Override
publicServerselect(Listservers){
ThreadLocalRandomlocalRandom=ThreadLocalRandom.current();
// 计算总比重
inttotalWeight=;
for(Serverserver:servers){
totalWeight+=server.getWeight();
}
// 按照权重选择
intrandomWeight=localRandom.nextInt(totalWeight);
for(Serverserver:servers){
randomWeight-=server.getWeight();
if(randomWeight
returnserver;
}
}
// default
intlength=servers.size();
returnservers.get(localRandom.nextInt(length));
}
算法的关键点是如何高效的确定点落在的区域,其流程是这样,假设在数轴上如下排列。
置随机数,随机数范围即数轴的总长度,假设得到的值为6
用该值依次减去数轴上的每一个区域,直到第一次小于0时,那么就属于那个区域。,因此6会落在b区域。
非平滑权重轮询算法
轮询算法是指依次访问可用服务器列表,其和随机本质是一样的处理,在无权重因素下,轮询只是在选数轴上的点时采取自增对长度取余方式。有权重因素下依然自增取余,再看选取的点落在了哪个区域。依旧使用上图,举个例子:无权重的轮询得到的结果为:有权重的轮询得到的结果为:.算法的关键点是每次如何选择下一个,其流程是这样的,定义一个全局的自增变量(需要线程安全),在Java中可以使用原子类,然后每次选择前线自增,然后利用结果与该数轴取余,再计算余数落在的区域,即被选中的节点。
平滑权重轮询算法
对于这三个服务实例,权重轮询会得到这样的访问顺序,这种情况其权重差过大,对于服务器来说依然存在集中访问,为了解决这个问题,Nginx实现了一种平滑的轮询算法,所谓的平滑,是需要打乱集中访问的顺序节点,对于上述权重实例,Nginx的算法得出的访问顺序为,这样的分布显然比直接轮询合理的多。清单二:平滑权重轮询算法
privatestaticfinalConcurrentMapServerMap=newConcurrentHashMap();
@Override
publicServerselect(Listservers){
Serverbest=null;
inttotalWeight=;
for(Serverserver:servers){
AtomicIntegerweightServer=ServerMap.get(server);
if(null==weightServer){
weightServer=newAtomicInteger();
ServerMap.putIfAbsent(server,weightServer);
}
intweight=server.getWeight();
// 加权
weightServer.addAndGet(weight);
totalWeight+=weight;
// 根据权选择
if(null==best||weightServer.get()>ServerMap.get(best).get()){
best=server;
}
}
if(null==best){
thrownewIllegalStateException("can't select client");
}
// 降权
AtomicIntegerbestWeightServer=ServerMap.get(best);
bestWeightServer.set(totalWeight-bestWeightServer.get());
printSorts(servers);
returnbest;
}
整个实现算法非常巧妙,大概思想是每一个的权重都是动态可改变的,在遍历过程中对每一个的权重做累加,然后选出权重最高的作为best,选中后再对best做降权,利用降权操作实现平滑的目的。以作为输入,选择10次,其输出结果为,下面是部分详情,帮助理解加权与降权的流程。
servername:aweight:5effective:5current:5
servername:bweight:2effective:2current:2
servername:cweight:3effective:3current:3
Server(address=a,weight=5)// 第一次选择
servername:aweight:5effective:5current:
servername:bweight:2effective:2current:4
servername:cweight:3effective:3current:6
Server(address=a,weight=5)// 第二次选择
servername:aweight:5effective:5current:5
servername:bweight:2effective:2current:6
servername:cweight:3effective:3current:1
Server(address=c,weight=3)// 第三次选择
servername:aweight:5effective:5current:
servername:bweight:2effective:2current:8
servername:cweight:3effective:3current:4
Server(address=a,weight=5)// 第四次选择
servername:aweight:5effective:5current:5
servername:bweight:2effective:2current:
servername:cweight:3effective:3current:7
Server(address=b,weight=2)// 第五次选择
ip hash算法
ip hash算法又叫,其根据客户端的ip计算hash值,再利用该hash值对服务器个数取余,从而定位到具体的某一台服务器上,该算法的好处是只要客户端ip不变,那么他的请求总是到同一台服务器,对于缓存等是比较友好的。然而这也是该算法的缺点,动态调整服务器会导致这个hash分布被重新分配,然而在集群中服务器的上下线是经常变动的,因此该算法一般不是很实用,其解决方案是接下来要说的。
一致性Hash负载均衡算法
无论是随机还是轮询算法,对于一个客户端的多次请求,每次落到的很大可能是不同的,如果这是一台缓存服务器,那么这就对缓存同步带来了很大的挑战,当系统繁忙时,主从延迟带来的同步缓慢,可能就造成了同一客户端两次访问得到不同的结果。解决方案是利用hash算法定位到对应的服务器。
普通的Hash:当客户端请求到达是则使用 hash(client) % N,其中N是服务器数量,利用这个表达式计算出该客户端对应的Server处理,因为客户端总是同一个那么对应的Server也总是同一个。该算法致命的问题是增减服务器,也就是,该操作会导致取余的结果变化,重新分配所有的Client,为了解决这个问题,一致性Hash算法诞生了。
一致性Hash:一致性Hash是把服务器分布变成一个环形,每一个hash(clinet)的结果会在该环上顺时针寻找第一个与其邻的节点,具体可以参考负载均衡--一致性hash算法,里面的几幅图描述的很形象。
在不考虑权重的条件下,对于三个Server,其组成的数轴首尾相连组成一个环。对于这个环,其规则如下:
计算,如果该点落在了a-b之前,则找顺时针的邻接点,也就是a。
计算,如果该点落在了a-c之前,则找顺时针的邻接点,也就是c。
计算,如果该点落在了a-b之前,则找顺时针的邻接点,也就是b。
因为对于来说其hash结果是固定的,因此能保证每一个Client总是能落到唯一确定的一个上。考虑到特殊情况,当,也就是服务器增加或者减少,比如服务器宕机了,那么整个环只剩和,那么原本落在,上的client依然会落到其上,只有原本落在节点上的client才会重新选择Server。反之增加节点也是如此,尽可能的降低重新hash分配的client数量。考虑权重的话,所期望的图应该是按照一定比例设置,那么对应的落点计算变为。
因为权重是5所以其占了圆的一半,因此点落到b-a区间的概率为0.5。还有一种做法是利用虚拟节点(给创建5个hash值不同的副本),思路是把圆分成等分为10分(a,b,c权重之和为10),然后分配5个,2个,3个,这样增大了hash到区域的几率,也就实现了权重。在Java中,得益于数据结构的强大,其中方法可以直接得到某一个key之后的元素,对应于环中操作就是很容易获取到某一点的相邻点。
/**
* 表示一致性Hash算法中的环
*/
privatestaticfinalConcurrentSkipListMapServerMap=newConcurrentSkipListMap();
publicServerselect(Listservers,StringclientIdentify){
// 放入环中
for(Serverserver:servers){
Longhash=hash(server.getAddress());
if(!ServerMap.containsKey(hash)){
addServer(hash,server);
}
}
// 计算client
Longhash=hash(clientIdentify);
// 定位到其后面的元素
ConcurrentNavigableMaptailMap=ServerMap.tailMap(hash);
if(null==tailMap.firstEntry()){
tailMap=ServerMap.headMap(hash);
}
// 获取到邻近的Server
returntailMap.firstEntry().getValue();
}
备注
微信的排版有点蛋疼,代码量有点大,还是建议去PC端查看。
我的博客地址为:https://mrdear.cn
领取专属 10元无门槛券
私享最新 技术干货