1 负载均衡简介 大型网站都要面对庞大的用户量,高并发,海量数据等挑战。为了提升系统整体的性能,可以采用垂直扩展和水平扩展两种方式。 垂直扩展:在网站发展早期,可以从单机的角度通过增加硬件处理能力,比如CPU处理能力,内存容量,磁盘等方面,实现服务器处理能力的提升。但是,单机是有性能瓶颈的,一旦触及瓶颈,再想提升,付出的成本和代价会极高。这显然不能满足大型分布式系统(网站)所有应对的大流量,高并发,海量数据等挑战。 水平扩展:通过集群来分担大型网站的流量。集群中的应用服务器(节点)通常被设计成无状态,用户可以请求任何一个节点,这些节点共同分担访问压力。水平扩展有两个要点:应用集群:将同一应用部署到多台机器上,组成处理集群,接收负载均衡设备分发的请求,进行处理,并返回相应数据。负载均衡:将用户访问请求,通过某种算法,分发到集群中的节点。 什么是负载均衡? 负载均衡(LoadBalance,简称LB)是高并发、高可用系统必不可少的关键组件,目标是尽力将网络流量平均分发到多个服务器上,以提高系统整体的响应速度和可用性。 负载均衡的主要作用如下:高并发:负载均衡通过算法调整负载,尽力均匀的分配应用集群中各节点的工作量,以此提高应用集群的并发处理能力(吞吐量)。伸缩性:添加或减少服务器数量,然后由负载均衡进行分发控制。这使得应用集群具备伸缩性。高可用:负载均衡器可以监控候选服务器,当服务器不可用时,自动跳过,将请求分发给可用的服务器。这使得应用集群具备高可用的特性。安全防护:有些负载均衡软件或硬件提供了安全性功能,如:黑白名单处理、防火墙,防DDos攻击等。 2 负载均衡的分类 支持负载均衡的技术很多,我们可以通过不同维度去进行分类。 载体维度分类 从支持负载均衡的载体来看,可以将负载均衡分为两类:硬件负载均衡、软件负载均衡。 1、硬件负载均衡 硬件负载均衡,一般是在定制处理器上运行的独立负载均衡服务器,价格昂贵,土豪专属。硬件负载均衡的主流产品有:F5和A10。 硬件负载均衡的优点:功能强大:支持全局负载均衡并提供较全面的、复杂的负载均衡算法。性能强悍:硬件负载均衡由于是在专用处理器上运行,因此吞吐量大,可支持单机百万以上的并发。安全性高:往往具备防火墙,防DDos攻击等安全功能。 硬件负载均衡的缺点:成本昂贵:购买和维护硬件负载均衡的成本都很高。扩展性差:当访问量突增时,超过限度不能动态扩容。 2、软件负载均衡 软件负载均衡,应用最广泛,无论大公司还是小公司都会使用。 软件负载均衡从软件层面实现负载均衡,一般可以在任何标准物理设备上运行。 软件负载均衡的主流产品有:Nginx、HAProxy、LVS。LVS可以作为四层负载均衡器。其负载均衡的性能要优于Nginx。HAProxy可以作为HTTP和TCP负载均衡器。Nginx、HAProxy可以作为四层或七层负载均衡器。 软件负载均衡的优点:扩展性好:适应动态变化,可以通过添加软件负载均衡实例,动态扩展到超出初始容量的能力。成本低廉:软件负载均衡可以在任何标准物理设备上运行,降低了购买和运维的成本。 软件负载均衡的缺点:性能略差:相比于硬件负载均衡,软件负载均衡的性能要略低一些。 网络通信分类 软件负载均衡从通信层面来看,又可以分为四层和七层负载均衡。 七层负载均衡:就是可以根据访问用户的HTTP请求头、URL信息将请求转发到特定的主机。DNS重定向HTTP重定向反向代理 四层负载均衡:基于IP地址和端口进行请求的转发。修改IP地址修改MAC地址 1、DNS负载均衡 DNS负载均衡一般用于互联网公司,复杂的业务系统不适合使用。大型网站一般使用DNS负载均衡作为第一级负载均衡手段,然后在内部使用其它方式做第二级负载均衡。DNS负载均衡属于七层负载均衡。 DNS即域名解析服务,是OSI第七层网络协议。DNS被设计为一个树形结构的分布式应用,自上而下依次为:根域名服务器,一级域名服务器,二级域名服务器,,本地域名服务器。显然,如果所有数据都存储在根域名服务器,那么DNS查询的负载和开销会非常庞大。 因此,DNS查询相对于DNS层级结构,是一个逆向的递归流程,DNS客户端依次请求本地DNS服务器,上一级DNS服务器,上上一级DNS服务器,,根DNS服务器(又叫权威DNS服务器),一旦命中,立即返回。为了减少查询次数,每一级DNS服务器都会设置DNS查询缓存。 DNS负载均衡的工作原理就是:基于DNS查询缓存,按照负载情况返回不同服务器的IP地址。 DNS重定向的优点:使用简单:负载均衡工作,交给DNS服务器处理,省掉了负载均衡服务器维护的麻烦提高性能:可以支持基于地址的域名解析,解析成距离用户最近的服务器地址(类似CDN的原理),可以加快访问速度,改善性能。 DNS重定向的缺点:可用性差:DNS解析是多级解析,新增修改DNS后,解析时间较长;解析过程中,用户访问网站将失败;扩展性低:DNS负载均衡的控制权在域名商那里,无法对其做更多的改善和扩展;维护性差:也不能反映服务器的当前运行状态;支持的算法少;不能区分服务器的差异(不能根据系统与服务的状态来判断负载)。 2、HTTP负载均衡 HTTP负载均衡是基于HTTP重定向实现的。HTTP负载均衡属于七层负载均衡。 HTTP重定向原理是:根据用户的HTTP请求计算出一个真实的服务器地址,将该服务器地址写入HTTP重定向响应中,返回给浏览器,由浏览器重新进行访问。 HTTP重定向的优点:方案简单。 HTTP重定向的缺点:性能较差:每次访问需要两次请求服务器,增加了访问的延迟。降低搜索排名:使用重定向后,搜索引擎会视为SEO作弊。如果负载均衡器宕机,就无法访问该站点。 由于其缺点比较明显,所以这种负载均衡策略实际应用较少。 3、反向代理负载均衡 反向代理(ReverseProxy)方式是指以代理服务器来接受网络请求,然后将请求转发给内网中的服务器,并将从内网中的服务器上得到的结果返回给网络请求的客户端。反向代理负载均衡属于七层负载均衡。 反向代理服务的主流产品:Nginx、Apache。 正向代理与反向代理有什么区别?正向代理:发生在客户端,是由用户主动发起的。翻墙软件就是典型的正向代理,客户端通过主动访问代理服务器,让代理服务器获得需要的外网数据,然后转发回客户端。反向代理:发生在服务端,用户不知道代理的存在。 反向代理是如何实现负载均衡的呢?以Nginx为例,如下所示: 首先,在代理服务器上设定好负载均衡规则。然后,当收到客户端请求,反向代理服务器拦截指定的域名或IP请求,根据负载均衡算法,将请求分发到候选服务器上。其次,如果某台候选服务器宕机,反向代理服务器会有容错处理,比如分发请求失败3次以上,将请求分发到其他候选服务器上。 反向代理的优点:多种负载均衡算法:支持多种负载均衡算法,以应对不同的场景需求。可以监控服务器:基于HTTP协议,可以监控转发服务器的状态,如:系统负载、响应时间、是否可用、连接数、流量等,从而根据这些数据调整负载均衡的策略。 反向代理的缺点:额外的转发开销:反向代理的转发操作本身是有性能开销的,可能会包括创建连接,等待连接响应,分析响应结果等操作。增加系统复杂度:反向代理常用于做分布式应用的水平扩展,但反向代理服务存在以下问题,为了解决以下问题会给系统整体增加额外的复杂度和运维成本:反向代理服务如果自身宕机,就无法访问站点,所以需要有高可用方案,常见的方案有:主备模式(一主一备)、双主模式(互为主备)。反向代理服务自身也存在性能瓶颈,随着需要转发的请求量不断攀升,需要有可扩展方案。 4、IP负载均衡 IP负载均衡是在网络层通过修改请求目的地址进行负载均衡。 如上图所示,IP均衡处理流程大致为:客户端请求192。168。137。10,由负载均衡服务器接收到报文。负载均衡服务器根据算法选出一个服务节点192。168。0。1,然后将报文请求地址改为该节点的IP。真实服务节点收到请求报文,处理后,返回响应数据到负载均衡服务器。负载均衡服务器将响应数据的源地址改负载均衡服务器地址,返回给客户端。 IP负载均衡在内核进程完成数据分发,较反向代理负载均衡有更好的从处理性能。但是,由于所有请求响应都要经过负载均衡服务器,集群的吞吐量受制于负载均衡服务器的带宽。 5、数据链路层负载均衡 数据链路层负载均衡是指在通信协议的数据链路层修改mac地址进行负载均衡。 在Linux平台上最好的链路层负载均衡开源产品是LVS(LinuxVirtualServer)。LVS是基于Linux内核中netfilter框架实现的负载均衡系统。netfilter是内核态的Linux防火墙机制,可以在数据包流经过程中,根据规则设置若干个关卡(hook函数)来执行相关的操作。 LVS的工作流程大致如下:当用户访问www。sina。com。cn时,用户数据通过层层网络,最后通过交换机进入LVS服务器网卡,并进入内核网络层。进入PREROUTING后经过路由查找,确定访问的目的VIP是本机IP地址,所以数据包进入到INPUT链上IPVS是工作在INPUT链上,会根据访问的vipport判断请求是否IPVS服务,如果是则调用注册的IPVSHOOK函数,进行IPVS相关主流程,强行修改数据包的相关数据,并将数据包发往POSTROUTING链上。POSTROUTING上收到数据包后,根据目标IP地址(后端服务器),通过路由选路,将数据包最终发往后端的服务器上。 开源LVS版本有3种工作模式,每种模式工作原理截然不同,说各种模式都有自己的优缺点,分别适合不同的应用场景,不过最终本质的功能都是能实现均衡的流量调度和良好的扩展性。主要包括三种模式:DR模式、NAT模式、Tunnel模式。 3 负载均衡算法 负载均衡器的实现可以分为两个部分:根据负载均衡算法在候选服务器列表选出一个服务器;将请求数据发送到该服务器上。 负载均衡算法是负载均衡服务核心中的核心。负载均衡产品多种多样,但是各种负载均衡算法原理是共性的。负载均衡算法有很多种,分别适用于不同的应用场景,本文仅介绍最为常见的负载均衡算法的特性及原理:轮询、随机、最小活跃数、源地址哈希、一致性哈希。 注:负载均衡算法的实现,推荐阅读《Dubbo官方负载均衡算法说明(https:reurl。cc9O6b2Y)》,源码讲解非常详细,非常值得借鉴。 随机 1、随机算法 随机(Random)算法将请求随机分发到候选服务器。 随机算法适合服务器硬件相同的场景。学习过概率论的都知道,调用量较小的时候,可能负载并不均匀,调用量越大,负载越均衡。 下面是随机算法实现示例。 负载均衡接口:publicinterfaceLoadBalanceNextendsNode{Nselect(ListNnodes,Stringip);} 负载均衡抽象类:publicabstractclassBaseLoadBalanceNextendsNodeimplementsLoadBalanceN{OverridepublicNselect(ListNnodes,Stringip){if(CollectionUtil。isEmpty(nodes)){returnnull;}如果nodes列表中仅有一个node,直接返回即可,无需进行负载均衡if(nodes。size()1){returnnodes。get(0);}returndoSelect(nodes,ip);}protectedabstractNdoSelect(ListNnodes,Stringip);} 服务器节点类:publicclassNodeimplementsComparableNode{protectedStringurl;protectedIntegerweight;protectedIntegeractive;。。。} 随机算法实现:publicclassRandomLoadBalanceNextendsNodeextendsBaseLoadBalanceNimplementsLoadBalanceN{privatefinalRandomrandomnewRandom();OverrideprotectedNdoSelect(ListNnodes,Stringip){在列表中随机选取一个节点intindexrandom。nextInt(nodes。size());returnnodes。get(index);}} 2、加权随机算法 加权随机(WeightedRandom)算法在随机算法的基础上,按照概率调整权重,进行负载分配。 加权随机算法实现示例:publicclassWeightRandomLoadBalanceNextendsNodeextendsBaseLoadBalanceNimplementsLoadBalanceN{privatefinalRandomrandomThreadLocalRandom。current();OverrideprotectedNdoSelect(ListNnodes,Stringip){intlengthnodes。size();AtomicIntegertotalWeightnewAtomicInteger(0);for(Nnode:nodes){Integerweightnode。getWeight();totalWeight。getAndAdd(weight);}if(totalWeight。get()0){intoffsetrandom。nextInt(totalWeight。get());for(Nnode:nodes){让随机值offset减去权重值offsetnode。getWeight();if(offset0){返回相应的Nodereturnnode;}}}直接随机返回一个returnnodes。get(random。nextInt(length));}} 轮询 1、轮询算法 轮询(RoundRobin)算法的策略是:将请求依次分发到候选服务器。 如下图所示,负载均衡器收到来自客户端的6个请求,(1,3,5)的请求会被发送到服务器1,(2,4,6)的请求会被发送到服务器2。 该算法适合场景:各服务器处理能力相近,且每个事务工作量差异不大。如果存在较大差异,那么处理较慢的服务器就可能会积压请求,最终无法承担过大的负载。 下面是轮询算法示例。 轮询负载均衡算法实现:publicclassRoundRobinLoadBalanceNextendsNodeextendsBaseLoadBalanceNimplementsLoadBalanceN{privatefinalAtomicIntegerpositionnewAtomicInteger(0);OverrideprotectedNdoSelect(ListNnodes,Stringip){intlengthnodes。size();如果位置值已经等于节点数,重置为0position。compareAndSet(length,0);Nnodenodes。get(position。get());position。getAndIncrement();returnnode;}} 2、加权轮询算法 加权轮询(WeightedRoundRobbin)算法在轮询算法的基础上,增加了权重属性来调节转发服务器的请求数目。性能高、处理速度快的节点应该设置更高的权重,使得分发时优先将请求分发到权重较高的节点上。 如下图所示,服务器A设置权重为5,服务器B设置权重为1,负载均衡器收到来自客户端的6个请求,那么(1,2,3,4,5)请求会被发送到服务器A,(6)请求会被发送到服务器B。 下面是加权轮询算法实现示例。 以下实现基于Dubbo加权轮询算法做了一些简化。publicclassWeightRoundRobinLoadBalanceNextendsNodeextendsBaseLoadBalanceNimplementsLoadBalanceN{60秒privatestaticfinalintRECYCLEPERIOD60000;Nodehashcode到WeightedRoundRobin的映射关系privateConcurrentMapInteger,WeightedRoundRobinweightMapnewConcurrentHashMap();原子更新锁privateAtomicBooleanupdateLocknewAtomicBoolean();OverrideprotectedNdoSelect(ListNnodes,Stringip){inttotalWeight0;longmaxCurrentLong。MINVALUE;获取当前时间longnowSystem。currentTimeMillis();NselectedNodenull;WeightedRoundRobinselectedWRRnull;下面这个循环主要做了这样几件事情:1。遍历Node列表,检测当前Node是否有相应的WeightedRoundRobin,没有则创建2。检测Node权重是否发生了变化,若变化了,则更新WeightedRoundRobin的weight字段3。让current字段加上自身权重,等价于currentweight4。设置lastUpdate字段,即lastUpdatenow5。寻找具有最大current的Node,以及Node对应的WeightedRoundRobin,暂存起来,留作后用6。计算权重总和for(Nnode:nodes){inthashCodenode。hashCode();WeightedRoundRobinweightedRoundRobinweightMap。get(hashCode);intweightnode。getWeight();if(weight0){weight0;}检测当前Node是否有对应的WeightedRoundRobin,没有则创建if(weightedRoundRobinnull){weightedRoundRobinnewWeightedRoundRobin();设置Node权重weightedRoundRobin。setWeight(weight);存储url唯一标识identifyString到weightedRoundRobin的映射关系weightMap。putIfAbsent(hashCode,weightedRoundRobin);weightedRoundRobinweightMap。get(hashCode);}Node权重不等于WeightedRoundRobin中保存的权重,说明权重变化了,此时进行更新if(weight!weightedRoundRobin。getWeight()){weightedRoundRobin。setWeight(weight);}让current加上自身权重,等价于currentweightlongcurrentweightedRoundRobin。increaseCurrent();设置lastUpdate,表示近期更新过weightedRoundRobin。setLastUpdate(now);找出最大的currentif(currentmaxCurrent){maxCurrentcurrent;将具有最大current权重的Node赋值给selectedNodeselectedNodenode;将Node对应的weightedRoundRobin赋值给selectedWRR,留作后用selectedWRRweightedRoundRobin;}计算权重总和totalWeightweight;}对weightMap进行检查,过滤掉长时间未被更新的节点。该节点可能挂了,nodes中不包含该节点,所以该节点的lastUpdate长时间无法被更新。若未更新时长超过阈值后,就会被移除掉,默认阈值为60秒。if(!updateLock。get()nodes。size()!weightMap。size()){if(updateLock。compareAndSet(false,true)){try{遍历修改,即移除过期记录weightMap。entrySet()。removeIf(itemnowitem。getValue()。getLastUpdate()RECYCLEPERIOD);}finally{updateLock。set(false);}}}if(selectedNode!null){让current减去权重总和,等价于currenttotalWeightselectedWRR。decreaseCurrent(totalWeight);返回具有最大current的NodereturnselectedNode;}shouldnothappenherereturnnodes。get(0);}protectedstaticclassWeightedRoundRobin{服务提供者权重privateintweight;当前权重privateAtomicLongcurrentnewAtomicLong(0);最后一次更新时间privatelonglastUpdate;publiclongincreaseCurrent(){currentcurrentweight;returncurrent。addAndGet(weight);}publiclongdecreaseCurrent(inttotal){currentcurrenttotal;returncurrent。addAndGet(1total);}publicintgetWeight(){returnweight;}publicvoidsetWeight(intweight){this。weightweight;初始情况下,current0current。set(0);}publicAtomicLonggetCurrent(){returncurrent;}publicvoidsetCurrent(AtomicLongcurrent){this。currentcurrent;}publiclonggetLastUpdate(){returnlastUpdate;}publicvoidsetLastUpdate(longlastUpdate){this。lastUpdatelastUpdate;}}} 最小活跃数 最小活跃数(LeastActive)算法将请求分发到连接数请求数最少的候选服务器(目前处理请求最少的服务器)。特点:根据候选服务器当前的请求连接数,动态分配。场景:适用于对系统负载较为敏感或请求连接时长相差较大的场景。 由于每个请求的连接时长不一样,如果采用简单的轮循或随机算法,都可能出现某些服务器当前连接数过大,而另一些服务器的连接过小的情况,这就造成了负载并非真正均衡。虽然,轮询或算法都可以通过加权重属性的方式进行负载调整,但加权方式难以应对动态变化。 例如下图中,(1,3,5)请求会被发送到服务器1,但是(1,3)很快就断开连接,此时只有(5)请求连接服务器1;(2,4,6)请求被发送到服务器2,只有(2)的连接断开。该系统继续运行时,服务器2会承担过大的负载。 最小活跃数算法会记录当前时刻,每个候选节点正在处理的连接数,然后选择连接数最小的节点。该策略能够动态、实时地反应服务器的当前状况,较为合理地将负责分配均匀,适用于对当前系统负载较为敏感的场景。 例如下图中,服务器1当前连接数最小,那么新到来的请求6就会被发送到服务器1上。 加权最小活跃数(WeightedLeastConnection)在最小活跃数的基础上,根据服务器的性能为每台服务器分配权重,再根据权重计算出每台服务器能处理的连接数。 最小活跃数算法实现要点:活跃调用数越小,表明该服务节点处理能力越高,单位时间内可处理更多的请求,应优先将请求分发给该服务。 在具体实现中,每个服务节点对应一个活跃数active。初始情况下,所有服务提供者活跃数均为0。每收到一个请求,活跃数加1,完成请求后则将活跃数减1。在服务运行一段时间后,性能好的服务提供者处理请求的速度更快,因此活跃数下降的也越快,此时这样的服务提供者能够优先获取到新的服务请求、这就是最小活跃数负载均衡算法的基本思想。 最小活跃数算法实现: 以下实现基于Dubbo最小活跃数负载均衡算法做了些许改动。publicclassLeastActiveLoadBalanceNextendsNodeextendsBaseLoadBalanceNimplementsLoadBalanceN{privatefinalRandomrandomnewRandom();OverrideprotectedNdoSelect(ListNnodes,Stringip){intlengthnodes。size();最小的活跃数intleastActive1;具有相同最小活跃数的服务者提供者(以下用Node代称)数量intleastCount0;leastIndexs用于记录具有相同最小活跃数的Node在nodes列表中的下标信息int〔〕leastIndexsnewint〔length〕;inttotalWeight0;第一个最小活跃数的Node权重值,用于与其他具有相同最小活跃数的Node的权重进行对比,以检测是否所有具有相同最小活跃数的Node的权重均相等intfirstWeight0;booleansameWeighttrue;遍历nodes列表for(inti0;ilength;i){Nnodenodes。get(i);发现更小的活跃数,重新开始if(leastActive1node。getActive()leastActive){使用当前活跃数更新最小活跃数leastActiveleastActivenode。getActive();更新leastCount为1leastCount1;记录当前下标值到leastIndexs中leastIndexs〔0〕i;totalWeightnode。getWeight();firstWeightnode。getWeight();sameWeighttrue;当前Node的活跃数node。getActive()与最小活跃数leastActive相同}elseif(node。getActive()leastActive){在leastIndexs中记录下当前Node在nodes集合中的下标leastIndexs〔leastCount〕i;累加权重totalWeightnode。getWeight();检测当前Node的权重与firstWeight是否相等,不相等则将sameWeight置为falseif(sameWeighti0node。getWeight()!firstWeight){sameWeightfalse;}}}当只有一个Node具有最小活跃数,此时直接返回该Node即可if(leastCount1){returnnodes。get(leastIndexs〔0〕);}有多个Node具有相同的最小活跃数,但它们之间的权重不同if(!sameWeighttotalWeight0){随机生成一个〔0,totalWeight)之间的数字intoffsetWeightrandom。nextInt(totalWeight);循环让随机数减去具有最小活跃数的Node的权重值,当offset小于等于0时,返回相应的Nodefor(inti0;ileastCount;i){intleastIndexleastIndexs〔i〕;获取权重值,并让随机数减去权重值offsetWeightnodes。get(leastIndex)。getWeight();if(offsetWeight0){returnnodes。get(leastIndex);}}}如果权重相同或权重为0时,随机返回一个Nodereturnnodes。get(leastIndexs〔random。nextInt(leastCount)〕);}} 源地址哈希 源地址哈希(IPHash)算法根据请求源IP,通过哈希计算得到一个数值,用该数值在候选服务器列表的进行取模运算,得到的结果便是选中的服务器。 可以保证同一IP的客户端的请求会转发到同一台服务器上,用来实现会话粘滞(StickySession)。 特点:保证特定用户总是请求到相同的服务器,若服务器宕机,会话会丢失。 源地址哈希算法实现示例:publicclassIpHashLoadBalanceNextendsNodeextendsBaseLoadBalanceNimplementsLoadBalanceN{OverrideprotectedNdoSelect(ListNnodes,Stringip){if(StrUtil。isBlank(ip)){ip127。0。0。1;}intlengthnodes。size();intindexhash(ip)length;returnnodes。get(index);}publicinthash(Stringtext){returnHashUtil。fnvHash(text);}} 一致性哈希 一致性哈希(ConsistentHash)算法的目标是:相同的请求尽可能落到同一个服务器上。 一致性哈希可以很好的解决稳定性问题,可以将所有的存储节点排列在首尾相接的Hash环上,每个key在计算Hash后会顺时针找到临接的存储节点存放。而当有节点加入或退出时,仅影响该节点在Hash环上顺时针相邻的后续节点。 相同的请求是指:一般在使用一致性哈希时,需要指定一个key用于hash计算,可能是:用户ID请求方IP请求服务名称,参数列表构成的串 尽可能是指:服务器可能发生上下线,少数服务器的变化不应该影响大多数的请求。 当某台候选服务器宕机时,原本发往该服务器的请求,会基于虚拟节点,平摊到其它候选服务器,不会引起剧烈变动。 优点:加入和删除节点只影响哈希环中顺时针方向的相邻的节点,对其他节点无影响。 缺点:加减节点会造成哈希环中部分数据无法命中。当使用少量节点时,节点变化将大范围影响哈希环中数据映射,不适合少量数据节点的分布式方案。普通的一致性哈希分区在增减节点时需要增加一倍或减去一半节点才能保证数据和负载的均衡。 注意:因为一致性哈希分区的这些缺点,一些分布式系统采用虚拟槽对一致性哈希进行改进,比如Dynamo系统。 一致性哈希算法示例:publicclassConsistentHashLoadBalanceNextendsNodeextendsBaseLoadBalanceNimplementsLoadBalanceN{privatefinalConcurrentMapString,ConsistentHashSelectorlt;?selectorsnewConcurrentHashMap();SuppressWarnings(unchecked)OverrideprotectedNdoSelect(ListNnodes,Stringip){分片数,这里设为节点数的4倍IntegerreplicaNumnodes。size()4;获取nodes原始的hashcodeintidentityHashCodeSystem。identityHashCode(nodes);如果nodes是一个新的List对象,意味着节点数量发生了变化此时selector。identityHashCode!identityHashCode条件成立ConsistentHashSelectorNselector(ConsistentHashSelectorN)selectors。get(ip);if(selectornullselector。identityHashCode!identityHashCode){创建新的ConsistentHashSelectorselectors。put(ip,newConsistentHashSelector(nodes,identityHashCode,replicaNum));selector(ConsistentHashSelectorN)selectors。get(ip);}调用ConsistentHashSelector的select方法选择Nodereturnselector。select(ip);}一致性哈希选择器privatestaticfinalclassConsistentHashSelectorNextendsNode{存储虚拟节点privatefinalTreeMapLong,NvirtualNodes;privatefinalintidentityHashCode;构造器paramnodes节点列表paramidentityHashCodehashcodeparamreplicaNum分片数ConsistentHashSelector(ListNnodes,intidentityHashCode,IntegerreplicaNum){this。virtualNodesnewTreeMap();this。identityHashCodeidentityHashCode;获取虚拟节点数,默认为100if(replicaNumnull){replicaNum100;}for(Nnode:nodes){for(inti0;ireplicaNum4;i){对url进行md5运算,得到一个长度为16的字节数组byte〔〕digestmd5(node。getUrl());对digest部分字节进行4次hash运算,得到四个不同的long型正整数for(intj0;j4;j){h0时,取digest中下标为03的4个字节进行位运算h1时,取digest中下标为47的4个字节进行位运算h2,h3时过程同上longmhash(digest,j);将hash到node的映射关系存储到virtualNodes中,virtualNodes需要提供高效的查询操作,因此选用TreeMap作为存储结构virtualNodes。put(m,node);}}}}publicNselect(Stringkey){对参数key进行md5运算byte〔〕digestmd5(key);取digest数组的前四个字节进行hash运算,再将hash值传给selectForKey方法,寻找合适的NodereturnselectForKey(hash(digest,0));}privateNselectForKey(longhash){查找第一个大于或等于当前hash的节点Map。EntryLong,NentryvirtualNodes。ceilingEntry(hash);如果hash大于Node在哈希环上最大的位置,此时entrynull,需要将TreeMap的头节点赋值给entryif(entrynull){entryvirtualNodes。firstEntry();}返回Nodereturnentry。getValue();}}计算hash值publicstaticlonghash(byte〔〕digest,intnumber){return(((long)(digest〔3number4〕0xFF)24)((long)(digest〔2number4〕0xFF)16)((long)(digest〔1number4〕0xFF)8)(digest〔number4〕0xFF))0xFFFFFFFFL;}计算MD5值publicstaticbyte〔〕md5(Stringvalue){MessageDigestmd5;try{md5MessageDigest。getInstance(MD5);}catch(NoSuchAlgorithmExceptione){thrownewIllegalStateException(e。getMessage(),e);}md5。reset();byte〔〕bytesvalue。getBytes(StandardCharsets。UTF8);md5。update(bytes);returnmd5。digest();}} 以上示例基于Dubbo的一致性哈希负载均衡算法做了一些简化。