应用办公生活信息教育商业
投稿投诉
商业财经
汽车智能
教育国际
房产环球
信息数码
热点科技
生活手机
晨报新闻
办公软件
科学动态
应用生物
体育时事

一文看懂一致性Hash算法

  作者:kylinkzhang,CSIG后台开发工程师
  导语一致性Hash算法是解决分布式缓存等问题的一种算法;本文介绍了一致性Hash算法的原理,并给出了一种实现和实际运用的案例;一致性Hash算法背景
  考虑这么一种场景:
  我们有三台缓存服务器编号node0、node1、node2,现在有3000万个key,希望可以将这些个key均匀的缓存到三台机器上,你会想到什么方案呢?
  我们可能首先想到的方案是:取模算法hash(key)N,即:对key进行hash运算后取模,N是机器的数量;
  这样,对key进行hash后的结果对3取模,得到的结果一定是0、1或者2,正好对应服务器node0、node1、node2,存取数据直接找对应的服务器即可,简单粗暴,完全可以解决上述的问题;
  取模算法虽然使用简单,但对机器数量取模,在集群扩容和收缩时却有一定的局限性:因为在生产环境中根据业务量的大小,调整服务器数量是常有的事;
  而服务器数量N发生变化后hash(key)N计算的结果也会随之变化!
  比如:一个服务器节点挂了,计算公式从hash(key)3变成了hash(key)2,结果会发生变化,此时想要访问一个key,这个key的缓存位置大概率会发生改变,那么之前缓存key的数据也会失去作用与意义;
  大量缓存在同一时间失效,造成缓存的雪崩,进而导致整个缓存系统的不可用,这基本上是不能接受的;
  为了解决优化上述情况,一致性hash算法应运而生
  一致性Hash算法详述算法原理一致性哈希算法在1997年由麻省理工学院提出,是一种特殊的哈希算法,在移除或者添加一个服务器时,能够尽可能小地改变已存在的服务请求与处理请求服务器之间的映射关系;
  一致性哈希解决了简单哈希算法在分布式哈希表(DistributedHashTable,DHT)中存在的动态伸缩等问题;
  一致性hash算法本质上也是一种取模算法;
  不过,不同于上边按服务器数量取模,一致性hash是对固定值232取模;
  IPv4的地址是4组8位2进制数组成,所以用232可以保证每个IP地址会有唯一的映射;hash环
  我们可以将这232个值抽象成一个圆环,圆环的正上方的点代表0,顺时针排列,以此类推:1、2、3直到2321,而这个由2的32次方个点组成的圆环统称为hash环;
  服务器映射到hash环
  在对服务器进行映射时,使用hash(服务器ip)232,即:
  使用服务器IP地址进行hash计算,用哈希后的结果对232取模,结果一定是一个0到2321之间的整数;
  而这个整数映射在hash环上的位置代表了一个服务器,依次将node0、node1、node2三个缓存服务器映射到hash环上;
  对象key映射到服务器
  在对对应的Key映射到具体的服务器时,需要首先计算Key的Hash值:hash(key)232;
  注:此处的Hash函数可以和之前计算服务器映射至Hash环的函数不同,只要保证取值范围和Hash环的范围相同即可(即:232);
  将Key映射至服务器遵循下面的逻辑:
  从缓存对象key的位置开始,沿顺时针方向遇到的第一个服务器,便是当前对象将要缓存到的服务器;
  假设我们有semlinker、kakuqo、lolo、fer四个对象,分别简写为o1、o2、o3和o4;
  首先,使用哈希函数计算这个对象的hash值,值的范围是〔0,2321〕:
  图中对象的映射关系如下:hash(o1)k1;hash(o2)k2;hash(o3)k3;hash(o4)k4;
  同时3台缓存服务器,分别为CS1、CS2和CS3:
  则可知,各对象和服务器的映射关系如下:K1CS1K4CS3K2CS2K3CS1
  即:
  以上便是一致性Hash的工作原理;
  可以看到,一致性Hash就是:将原本单个点的Hash映射,转变为了在一个环上的某个片段上的映射!
  下面我们来看几种服务器扩缩容的场景;
  服务器扩缩容场景服务器减少
  假设CS3服务器出现故障导致服务下线,这时原本存储于CS3服务器的对象o4,需要被重新分配至CS2服务器,其它对象仍存储在原有的机器上:
  此时受影响的数据只有CS2和CS3服务器之间的部分数据!
  服务器增加
  假如业务量激增,我们需要增加一台服务器CS4,经过同样的hash运算,该服务器最终落于t1和t2服务器之间,具体如下图所示:
  此时,只有t1和t2服务器之间的部分对象需要重新分配;
  在以上示例中只有o3对象需要重新分配,即它被重新到CS4服务器;
  在前面我们已经说过:如果使用简单的取模方法,当新添加服务器时可能会导致大部分缓存失效,而使用一致性哈希算法后,这种情况得到了较大的改善,因为只有少部分对象需要重新分配!
  数据偏斜服务器性能平衡问题引出问题
  在上面给出的例子中,各个服务器几乎是平均被均摊到Hash环上;
  但是在实际场景中很难选取到一个Hash函数这么完美的将各个服务器散列到Hash环上;
  此时,在服务器节点数量太少的情况下,很容易因为节点分布不均匀而造成数据倾斜问题;
  如下图被缓存的对象大部分缓存在node4服务器上,导致其他节点资源浪费,系统压力大部分集中在node4节点上,这样的集群是非常不健康的:
  同时,还有另一个问题:
  在上面新增服务器CS4时,CS4只分担了CS1服务器的负载,服务器CS2和CS3并没有因为CS4服务器的加入而减少负载压力;如果CS4服务器的性能与原有服务器的性能一致甚至可能更高,那么这种结果并不是我们所期望的;
  虚拟节点
  针对上面的问题,我们可以通过:引入虚拟节点来解决负载不均衡的问题:
  即将每台物理服务器虚拟为一组虚拟服务器,将虚拟服务器放置到哈希环上,如果要确定对象的服务器,需先确定对象的虚拟服务器,再由虚拟服务器确定物理服务器;
  如下图所示:
  在图中:o1和o2表示对象,v1v6表示虚拟服务器,s1s3表示实际的物理服务器;
  虚拟节点的计算
  虚拟节点的hash计算通常可以采用:对应节点的IP地址加数字编号后缀hash(10。24。23。2271)的方式;
  举个例子,node1节点IP为10。24。23。227,正常计算node1的hash值:hash(10。24。23。2271)232
  假设我们给node1设置三个虚拟节点,node11、node12、node13,对它们进行hash后取模:hash(10。24。23。2271)232hash(10。24。23。2272)232hash(10。24。23。2273)232注意:
  分配的虚拟节点个数越多,映射在hash环上才会越趋于均匀,节点太少的话很难看出效果;
  引入虚拟节点的同时也增加了新的问题,要做虚拟节点和真实节点间的映射,对象key虚拟节点实际节点之间的转换;
  使用场景
  一致性hash在分布式系统中应该是实现负载均衡的首选算法,它的实现比较灵活,既可以在客户端实现,也可以在中间件上实现,比如日常使用较多的缓存中间件memcached和redis集群都有用到它;
  memcached的集群比较特殊,严格来说它只能算是伪集群,因为它的服务器之间不能通信,请求的分发路由完全靠客户端来的计算出缓存对象应该落在哪个服务器上,而它的路由算法用的就是一致性hash;
  还有redis集群中hash槽的概念,虽然实现不尽相同,但思想万变不离其宗,看完本篇的一致性hash,你再去理解redis槽位就轻松多了;
  其它的应用场景还有很多:RPC框架Dubbo用来选择服务提供者分布式关系数据库分库分表:数据与节点的映射关系LVS负载均衡调度器
  一致性Hash算法实现
  下面我们根据上面的讲述,使用Golang实现一个一致性Hash算法,这个算法具有一些下面的功能特性:一致性Hash核心算法;支持自定义Hash算法;支持自定义虚拟节点个数;具体源代码见:
  https:github。comJasonkayZKconsistenthashingdemo
  下面开始实现吧!
  结构体、错误以及常量定义结构体定义
  首先定义每一台缓存服务器的数据结构:
  corehost。gotypeHoststruct{thehostid:ip:portNamestringtheloadboundofthehostLoadBoundint64}
  其中:Name:缓存服务器的Ip地址端口,如:127。0。0。1:8000LoadBound:缓存服务器当前处理的请求缓存数,这个字段在后文含有负载边界值的一致性Hash中会用到;
  其次,定义一致性Hash的结构:
  corealgorithm。goConsistentisanimplementationofconsistenthashingalgorithmtypeConsistentstruct{thenumberofreplicasreplicaNumintthetotalloadsofallreplicastotalLoadint64thehashfunctionforkeyshashFuncfunc(keystring)uint64themapofvirtualnodestohostshostMapmap〔string〕HostthemapofhashedvirtualnodestohostnamereplicaHostMapmap〔uint64〕stringthehashringsortedHostsHashSet〔〕uint64thehashringlocksync。RWMutex}
  其中:replicaNum:表示每个真实的缓存服务器在Hash环中存在的虚拟节点数;totalLoad:所有物理服务器对应的总缓存请求数(这个字段在后文含有负载边界值的一致性Hash中会用到);hashFunc:计算Hash环映射以及Key映射的散列函数;hostMap:物理服务器名称对应的Host结构体映射;replicaHostMap:Hash环中虚拟节点对应真实缓存服务器名称的映射;sortedHostsHashSet:Hash环;sync。RWMutex:操作Hash环时用到的读写锁;
  大概的结构如上所示,下面我们来看一些常量和错误的定义;
  常量和错误定义
  常量的定义如下:
  corealgorithm。goconst(TheformatofthehostreplicanamehostReplicaFormatsd)var(thedefaultnumberofreplicasdefaultReplicaNum10theloadboundfactorref:https:research。googleblog。com201704consistenthashingwithboundedloads。htmlloadBoundFactor0。25thedefaultHashfunctionforkeysdefaultHashFuncfunc(keystring)uint64{out:sha512。Sum512(〔〕byte(key))returnbinary。LittleEndian。Uint64(out〔:〕)})
  分别表示:defaultReplicaNum:默认情况下,每个真实的物理服务器在Hash环中虚拟节点的个数;loadBoundFactor:负载边界因数(这个字段在后文含有负载边界值的一致性Hash中会用到);defaultHashFunc:默认的散列函数,这里用到的是SHA512算法,并取的是unsignedint64,这一点和上面介绍的02321有所区别!hostReplicaFormat:虚拟节点名称格式,这里的虚拟节点的格式为:sd,和上文提到的10。24。23。2271的格式有所区别,但是道理是一样的!
  还有一些错误的定义:
  coreerror。govar(ErrHostAlreadyExistserrors。New(hostalreadyexists)ErrHostNotFounderrors。New(hostnotfound))
  分别表示服务器已经注册,以及缓存服务器未找到;
  下面来看具体的方法实现!
  注册注销缓存服务器注册缓存服务器
  注册缓存服务器的代码如下:
  corealgorithm。gofunc(cConsistent)RegisterHost(hostNamestring)error{c。Lock()deferc。Unlock()if,ok:c。hostMap〔hostName〕;ok{returnErrHostAlreadyExists}c。hostMap〔hostName〕Host{Name:hostName,LoadBound:0,}fori:0;ic。replicaNum;i{hashedIdx:c。hashFunc(fmt。Sprintf(hostReplicaFormat,hostName,i))c。replicaHostMap〔hashedIdx〕hostNamec。sortedHostsHashSetappend(c。sortedHostsHashSet,hashedIdx)}sorthashesinascendingordersort。Slice(c。sortedHostsHashSet,func(iint,jint)bool{ifc。sortedHostsHashSet〔i〕c。sortedHostsHashSet〔j〕{returntrue}returnfalse})returnnil}
  代码比较简单,简单说一下;
  首先,检查服务器是否已经注册,如果已经注册,则直接返回已经注册的错误;
  随后,创建一个Host对象,并且在for循环中创建多个虚拟节点:根据hashFunc计算服务器散列值【注:此处计算的散列值可能和之前的值存在冲突,本实现中暂不考虑这种场景】;将散列值加入replicaHostMap中;将散列值加入sortedHostsHashSet中;
  最后,对Hash环进行排序;
  这里使用数组作为Hash环只是为了便于说明,在实际实现中建议选用其他数据结构进行实现,以获取更好的性能;
  当缓存服务器信息写入replicaHostMap映射以及Hash环后,即完成了缓存服务器的注册;
  注销缓存服务器
  注销缓存服务器的代码如下:
  corealgorithm。gofunc(cConsistent)UnregisterHost(hostNamestring)error{c。Lock()deferc。Unlock()if,ok:c。hostMap〔hostName〕;!ok{returnErrHostNotFound}delete(c。hostMap,hostName)fori:0;ic。replicaNum;i{hashedIdx:c。hashFunc(fmt。Sprintf(hostReplicaFormat,hostName,i))delete(c。replicaHostMap,hashedIdx)c。delHashIndex(hashedIdx)}returnnil}Removehashedhostindexfromthehashringfunc(cConsistent)delHashIndex(valuint64){idx:1l:0r:len(c。sortedHostsHashSet)1forlr{m:(lr)2ifc。sortedHostsHashSet〔m〕val{idxmbreak}elseifc。sortedHostsHashSet〔m〕val{lm1}elseifc。sortedHostsHashSet〔m〕val{rm1}}ifidx!1{c。sortedHostsHashSetappend(c。sortedHostsHashSet〔:idx〕,c。sortedHostsHashSet〔idx1:〕。。。)}}
  和注册缓存服务器相反,将服务器在Map映射以及Hash环中去除即完成了注销;
  这里的逻辑和上面注册的逻辑极为类似,这里不再赘述!
  查询Key(核心)
  查询Key是整个一致性Hash算法的核心,但是实现起来也并不复杂;
  代码如下:
  corealgorithm。gofunc(cConsistent)GetKey(keystring)(string,error){hashedKey:c。hashFunc(key)idx:c。searchKey(hashedKey)returnc。replicaHostMap〔c。sortedHostsHashSet〔idx〕〕,nil}func(cConsistent)searchKey(keyuint64)int{idx:sort。Search(len(c。sortedHostsHashSet),func(iint)bool{returnc。sortedHostsHashSet〔i〕key})ifidxlen(c。sortedHostsHashSet){makesearchasaringidx0}returnidx}
  代码首先计算key的散列值;
  随后,在Hash环上顺时针寻找可以缓存的第一台缓存服务器:idx:sort。Search(len(c。sortedHostsHashSet),func(iint)bool{returnc。sortedHostsHashSet〔i〕key})
  注意到,如果key比当前Hash环中最大的虚拟节点的hash值还大,则选择当前Hash环中hash值最小的一个节点(即环形的逻辑):ifidxlen(c。sortedHostsHashSet){makesearchasaringidx0}
  searchKey返回了虚拟节点在Hash环数组中的index;
  随后,我们使用map返回index对应的缓存服务器的名称即可;
  至此,一致性Hash算法基本实现,接下来我们来验证一下;
  一致性Hash算法实践与检验算法验证前准备缓存服务器准备
  在验证算法之前,我们还需要准备几台缓存服务器;
  为了简单起见,这里使用了HTTP服务器作为缓存服务器,具体代码如下所示:
  servermain。gopackagemainimport(flagfmtnethttpsynctime)typeCachedMapstruct{KvMapsync。MapLocksync。RWMutex}var(cacheCachedMap{KvMap:sync。Map{}}portflag。String(p,8080,port)regHosthttp:localhost:18888expireTime10)funcmain(){flag。Parse()stopChan:make(chaninterface{})startServer(port)stopChan}funcstartServer(portstring){hostName:fmt。Sprintf(localhost:s,port)fmt。Printf(startserver:s,port)err:registerHost(hostName)iferr!nil{panic(err)}http。HandleFunc(,kvHandle)errhttp。ListenAndServe(:port,nil)iferr!nil{errunregisterHost(hostName)iferr!nil{panic(err)}panic(err)}}funckvHandle(whttp。ResponseWriter,rhttp。Request){r。ParseForm()if,ok:cache。KvMap。Load(r。Form〔key〕〔0〕);!ok{val:fmt。Sprintf(hello:s,r。Form〔key〕〔0〕)cache。KvMap。Store(r。Form〔key〕〔0〕,val)fmt。Printf(cachedkey:{s:s},r。Form〔key〕〔0〕,val)time。AfterFunc(time。Duration(expireTime)time。Second,func(){cache。KvMap。Delete(r。Form〔key〕〔0〕)fmt。Printf(removedcachedkeyafter3s:{s:s},r。Form〔key〕〔0〕,val)})}val,:cache。KvMap。Load(r。Form〔key〕〔0〕),err:fmt。Fprintf(w,val。(string))iferr!nil{panic(err)}}funcregisterHost(hoststring)error{resp,err:http。Get(fmt。Sprintf(sregister?hosts,regHost,host))iferr!nil{returnerr}deferresp。Body。Close()returnnil}funcunregisterHost(hoststring)error{resp,err:http。Get(fmt。Sprintf(sunregister?hosts,regHost,host))iferr!nil{returnerr}deferresp。Body。Close()returnnil}
  代码接受由命令行指定的p参数指定服务器端口号;
  代码执行后,会调用startServer函数启动一个http服务器;
  在startServer函数中,首先调用registerHost在代理服务器上进行注册(下文会讲),并监听路径,具体代码如下:funcstartServer(portstring){hostName:fmt。Sprintf(localhost:s,port)fmt。Printf(startserver:s,port)err:registerHost(hostName)iferr!nil{panic(err)}http。HandleFunc(,kvHandle)errhttp。ListenAndServe(:port,nil)iferr!nil{errunregisterHost(hostName)iferr!nil{panic(err)}panic(err)}}
  kvHandle函数对请求进行处理:funckvHandle(whttp。ResponseWriter,rhttp。Request){r。ParseForm()if,ok:cache。KvMap。Load(r。Form〔key〕〔0〕);!ok{val:fmt。Sprintf(hello:s,r。Form〔key〕〔0〕)cache。KvMap。Store(r。Form〔key〕〔0〕,val)fmt。Printf(cachedkey:{s:s},r。Form〔key〕〔0〕,val)time。AfterFunc(time。Duration(expireTime)time。Second,func(){cache。KvMap。Delete(r。Form〔key〕〔0〕)fmt。Printf(removedcachedkeyafter3s:{s:s},r。Form〔key〕〔0〕,val)})}val,:cache。KvMap。Load(r。Form〔key〕〔0〕),err:fmt。Fprintf(w,val。(string))iferr!nil{panic(err)}}
  首先,解析来自路径的参数:?keyxxx;
  随后,查询服务器中的缓存(为了简单起见,这里使用sync。Map来模拟缓存):如果缓存不存在,则写入缓存,并通过time。AfterFunc设置缓存过期时间(expireTime);
  最后,返回缓存;
  缓存代理服务器准备
  有了缓存服务器之后,我们还需要一个代理服务器来选择具体选择哪个缓存服务器来请求;
  代码如下:
  proxyproxy。gopackageproxyimport(fmtgithub。comjasonkayzkconsistenthashingdemocoreioioutilnethttptime)typeProxystruct{consistentcore。Consistent}NewProxycreatesanewProxyfuncNewProxy(consistentcore。Consistent)Proxy{proxy:Proxy{consistent:consistent,}returnproxy}func(pProxy)GetKey(keystring)(string,error){host,err:p。consistent。GetKey(key)iferr!nil{return,err}resp,err:http。Get(fmt。Sprintf(http:s?keys,host,key))iferr!nil{return,err}deferresp。Body。Close()body,:ioutil。ReadAll(resp。Body)fmt。Printf(Responsefromhosts:s,host,string(body))returnstring(body),nil}func(pProxy)RegisterHost(hoststring)error{err:p。consistent。RegisterHost(host)iferr!nil{returnerr}fmt。Println(fmt。Sprintf(registerhost:ssuccess,host))returnnil}func(pProxy)UnregisterHost(hoststring)error{err:p。consistent。UnregisterHost(host)iferr!nil{returnerr}fmt。Println(fmt。Sprintf(unregisterhost:ssuccess,host))returnnil}
  代理服务器的逻辑很简单,就是创建一个一致性Hash结构:Consistent,把Consistent和请求缓存服务器的逻辑进行了一层封装;
  算法验证启动代理服务器
  启动代理服务器的代码如下:packagemainimport(fmtgithub。comjasonkayzkconsistenthashingdemocoregithub。comjasonkayzkconsistenthashingdemoproxynethttp)var(port18888pproxy。NewProxy(core。NewConsistent(10,nil)))funcmain(){stopChan:make(chaninterface{})startServer(port)stopChan}funcstartServer(portstring){http。HandleFunc(register,registerHost)http。HandleFunc(unregister,unregisterHost)http。HandleFunc(key,getKey)fmt。Printf(startproxyserver:s,port)err:http。ListenAndServe(:port,nil)iferr!nil{panic(err)}}funcregisterHost(whttp。ResponseWriter,rhttp。Request){r。ParseForm()err:p。RegisterHost(r。Form〔host〕〔0〕)iferr!nil{w。WriteHeader(http。StatusInternalServerError),fmt。Fprintf(w,err。Error())return},fmt。Fprintf(w,fmt。Sprintf(registerhost:ssuccess,r。Form〔host〕〔0〕))}funcunregisterHost(whttp。ResponseWriter,rhttp。Request){r。ParseForm()err:p。UnregisterHost(r。Form〔host〕〔0〕)iferr!nil{w。WriteHeader(http。StatusInternalServerError),fmt。Fprintf(w,err。Error())return},fmt。Fprintf(w,fmt。Sprintf(unregisterhost:ssuccess,r。Form〔host〕〔0〕))}funcgetKey(whttp。ResponseWriter,rhttp。Request){r。ParseForm()val,err:p。GetKey(r。Form〔key〕〔0〕)iferr!nil{w。WriteHeader(http。StatusInternalServerError),fmt。Fprintf(w,err。Error())return},fmt。Fprintf(w,fmt。Sprintf(key:s,val:s,r。Form〔key〕〔0〕,val))}
  和缓存服务器类似,这里采用HTTP服务器来模拟;
  代理服务器监听18888端口的几个路由:register:注册缓存服务器;unregister:注销缓存服务器;key:查询缓存Key;
  这里为了简单起见,使用了这种方式进行服务注册,实际使用时请使用其他组件进行实现!
  接下来启动缓存服务器:startproxyserver:18888
  启动缓存服务器
  分别启动三个缓存服务器:gorunservermain。gop8080startserver:8080gorunservermain。gop8081startserver:8081gorunservermain。gop8082startserver:8082
  同时,代理服务器输出:registerhost:localhost:8080successregisterhost:localhost:8081successregisterhost:localhost:8082success
  可以看到缓存服务器已经成功注册;
  请求代理服务器获取Key
  可以使用curl命令请求代理服务器获取缓存key:curllocalhost:18888key?key123key:123,val:hello:123
  此时,代理服务器输出:Responsefromhostlocalhost:8080:hello:123
  同时,8000端口的缓存服务器输出:cachedkey:{123:hello:123}removedcachedkeyafter10s:{123:hello:123}
  可以看到,8000端口的服务器对key值进行了缓存,并在10秒后清除了缓存;
  尝试多次获取Key
  尝试获取多个Key:Responsefromhostlocalhost:8082:hello:45363456Responsefromhostlocalhost:8080:hello:4Responsefromhostlocalhost:8082:hello:1Responsefromhostlocalhost:8080:hello:2Responsefromhostlocalhost:8082:hello:3Responsefromhostlocalhost:8080:hello:4Responsefromhostlocalhost:8082:hello:5Responsefromhostlocalhost:8080:hello:6Responsefromhostlocalhost:8082:hello:sdkbnfoerwtnbreResponsefromhostlocalhost:8082:hello:sd45555254tg423i5gvj4v5Responsefromhostlocalhost:8081:hello:0Responsefromhostlocalhost:8082:hello:032452345
  可以看到不同的key被散列到了不同的缓存服务器;
  接下来我们通过debug查看具体的变量来一探究竟;
  通过Debug查看注册和Hash环
  开启debug,并注册单个缓存服务器后,查看Consistent中的值:
  注册三个缓存服务器后,查看Consistent中的值:
  从debug中的变量,我们就可以很清楚的看到注册不同数量的服务器时,一致性Hash上服务器的动态变化!
  以上就是基本的一致性Hash算法的实现了!
  但是很多时候,我们的缓存服务器需要同时处理大量的缓存请求,而通过上面的算法,我们总是会去同一台缓存服务器去获取缓存数据;
  如果很多的热点数据都落在了同一台缓存服务器上,则可能会出现性能瓶颈;
  Google在2017年提出了:含有负载边界值的一致性Hash算法;
  下面我们在基本的一致性Hash算法的基础上,实现含有负载边界值的一致性Hash!
  含有负载边界值的一致性Hash算法描述
  17年时,Google提出了含有负载边界值的一致性Hash算法,此算法主要应用于在实现一致性的同时,实现负载的平均性;此算法最初由Vimeo的AndrewRodland在haproxy中实现并开源;
  参考:
  https:ai。googleblog。com201704consistenthashingwithboundedloads。html
  arvix论文地址:
  https:arxiv。orgabs1608。01350
  这个算法将缓存服务器视为一个含有一定容量的桶(可以简单理解为Hash桶),将客户端视为球,则平均性目标表示为:所有约等于平均密度(球的数量除以桶的数量):
  实际使用时,可以设定一个平均密度的参数,将每个桶的容量设置为平均加载时间的下上限(1);
  具体的计算过程如下:首先,计算key的Hash值;随后,沿着Hash环顺时针寻找第一台满足条件(平均容量限制)的服务器;获取缓存;
  例如下面的图:
  使用哈希函数将6个球和3个桶分配给Hash环上的随机位置,假设每个桶的容量设置为2,按ID值的递增顺序分配球;1号球顺时针移动,进入C桶;2号球进入A桶;3号和4号球进入B桶;5号球进入C桶;然后6号球顺时针移动,首先击中B桶;但是桶B的容量为2,并且已经包含球3和4,所以球6继续移动到达桶C,但该桶也已满;最后,球6最终进入具有备用插槽的桶A;
  算法实现
  在上面基本一致性Hash算法实现的基础上,我们继续实现含有负载边界值的一致性Hash算法;
  在核心算法中添加根据负载情况查询Key的函数,以及增加释放负载值的函数;
  根据负载情况查询Key的函数:
  corealgorithm。gofunc(cConsistent)GetKeyLeast(keystring)(string,error){c。RLock()deferc。RUnlock()iflen(c。replicaHostMap)0{return,ErrHostNotFound}hashedKey:c。hashFunc(key)idx:c。searchKey(hashedKey)Findthefirsthostthatmayservethekeyi:idxfor{host:c。replicaHostMap〔c。sortedHostsHashSet〔i〕〕loadChecked,err:c。checkLoadCapacity(host)iferr!nil{return,err}ifloadChecked{returnhost,nil}iifidxgoestotheendofthering,startfromthebeginningifilen(c。replicaHostMap){i0}}}func(cConsistent)checkLoadCapacity(hoststring)(bool,error){asafetycheckifsomeoneperformedc。Donemorethanneededifc。totalLoad0{c。totalLoad0}varavgLoadPerNodefloat64avgLoadPerNodefloat64((c。totalLoad1)int64(len(c。hostMap)))ifavgLoadPerNode0{avgLoadPerNode1}avgLoadPerNodemath。Ceil(avgLoadPerNode(1loadBoundFactor))candidateHost,ok:c。hostMap〔host〕if!ok{returnfalse,ErrHostNotFound}iffloat64(candidateHost。LoadBound)1avgLoadPerNode{returntrue,nil}returnfalse,nil}
  在GetKeyLeast函数中,首先根据searchKey函数,顺时针获取可能满足条件的第一个虚拟节点;
  随后调用checkLoadCapacity校验当前缓存服务器的负载数是否满足条件:candidateHost。LoadBound1(c。totalLoad1)len(hosts)(1loadBoundFactor)
  如果不满足条件,则沿着Hash环走到下一个虚拟节点,继续判断是否满足条件,直到满足条件;
  这里使用的是无条件的for循环,因为一定存在低于平均负载(1loadBoundFactor)的虚拟节点!
  增加释放负载值的函数:
  corealgorithm。gofunc(cConsistent)Inc(hostNamestring){c。Lock()deferc。Unlock()atomic。AddInt64(c。hostMap〔hostName〕。LoadBound,1)atomic。AddInt64(c。totalLoad,1)}func(cConsistent)Done(hoststring){c。Lock()deferc。Unlock()if,ok:c。hostMap〔host〕;!ok{return}atomic。AddInt64(c。hostMap〔host〕。LoadBound,1)atomic。AddInt64(c。totalLoad,1)}
  逻辑比较简单,就是原子的对对应缓存服务器进行负载加减一操作;
  算法测试修改代理服务器代码
  在代理服务器中增加路由:
  proxyproxy。gofunc(pProxy)GetKeyLeast(keystring)(string,error){host,err:p。consistent。GetKeyLeast(key)iferr!nil{return,err}p。consistent。Inc(host)time。AfterFunc(time。Second10,func(){dropthehostafter10seconds(fortesting)!fmt。Printf(droppinghost:safter10second,host)p。consistent。Done(host)})resp,err:http。Get(fmt。Sprintf(http:s?keys,host,key))iferr!nil{return,err}deferresp。Body。Close()body,:ioutil。ReadAll(resp。Body)fmt。Printf(Responsefromhosts:s,host,string(body))returnstring(body),nil}
  注意:这里模拟的是单个key请求可能会持续10s钟;
  启动代理服务器时增加路由:
  main。gofuncstartServer(portstring){。。。。。。http。HandleFunc(keyleast,getKeyLeast)。。。。。。}funcgetKeyLeast(whttp。ResponseWriter,rhttp。Request){r。ParseForm()val,err:p。GetKeyLeast(r。Form〔key〕〔0〕)iferr!nil{w。WriteHeader(http。StatusInternalServerError),fmt。Fprintf(w,err。Error())return},fmt。Fprintf(w,fmt。Sprintf(key:s,val:s,r。Form〔key〕〔0〕,val))}
  测试
  启动代理服务器,并开启三台缓存服务器;
  通过下面的命令获取含有负载边界的Key:curllocalhost:18888keyleast?key123key:123,val:hello:123
  多次请求后的结果如下:startproxyserver:18888registerhost:localhost:8080successregisterhost:localhost:8081successregisterhost:localhost:8082successResponsefromhostlocalhost:8080:hello:123Responsefromhostlocalhost:8080:hello:123Responsefromhostlocalhost:8082:hello:123Responsefromhostlocalhost:8082:hello:123Responsefromhostlocalhost:8081:hello:123Responsefromhostlocalhost:8080:hello:123Responsefromhostlocalhost:8082:hello:123Responsefromhostlocalhost:8081:hello:123Responsefromhostlocalhost:8080:hello:123Responsefromhostlocalhost:8082:hello:123Responsefromhostlocalhost:8081:hello:123Responsefromhostlocalhost:8080:hello:123Responsefromhostlocalhost:8082:hello:123Responsefromhostlocalhost:8081:hello:123Responsefromhostlocalhost:8080:hello:123Responsefromhostlocalhost:8080:hello:123Responsefromhostlocalhost:8082:hello:123Responsefromhostlocalhost:8080:hello:123Responsefromhostlocalhost:8082:hello:123Responsefromhostlocalhost:8082:hello:123
  可以看到,缓存被均摊到了其他服务器(这是由于一个缓存请求会持续10s导致的)!
  总结
  本文抛砖引玉的讲解了一致性Hash算法的原理,并提供了Go的实现;
  在此基础之上,根据Google的论文实现了带有负载边界的一致性Hash算法;
  当然上面的代码在实际生产环境下仍然需要部分改进,如:服务注册;缓存服务器实现;心跳检测;
  大家在实际使用时,可以根据需要,搭配实际的组件!

关键粒子的重量有点重,让物理学家感到困惑费米国家加速器实验室于2022年4月提供的这张未注明日期的照片显示了该设施在伊利诺伊州巴达维亚郊外的对撞探测器。在2022年4月7日星期四发布的结果中,实验室的科学家计算出,W……推荐安卓小米手机的几款宝藏APP,玩机小白都会用今天我们推荐几款关于小米和安卓机可以使用的宝藏app。大家赶快收藏点赞吧!第一个就以小米手机为主,你是不是觉得MIUI黑科技太多你没开发出来呢?那么接下来这款软件可以让你……吃猪血清肺?一吃猪血就拉黑粑粑,这是身体在排毒吗?从古至今,中国人的餐桌上从来不缺各式各样动物,爆炒鸡心、青椒炒肥肠、熘肝尖、毛血旺看似重口味的饮食习惯,却富含很多营养物质,例如动物内脏铁元素含量高,而且还有人体所需的全部13……女人会不会打扮,看她背的包包就知道了,差别比你想象中的还要大女人不可无包。为什么会这么说呢?当你拥有一个你喜爱的包包,你可以用它装可爱,装温柔,装下世间所有美好不可否认,包包对于女人来说真的很重要。包治百病可不是空穴来风哦一……靴子,建议多挑这些显瘦增高款,比马丁靴百搭,很适合小个子小个子女生就要注意了:秋冬的靴子可千万别乱选!对于小个子女生来说,选对一双显瘦增高的靴子比选对一件好看的衣服还重要。这些显瘦又增高的靴子,比马丁靴还要好看,还要显高,小个……杏花怒放金山岭长城邀约游客同赏春日盛景游客在金山岭长城赏花。陈琦嘉摄盛开的杏花为长城增添一份秀美陈琦嘉摄游客在金山岭长城盛开的杏花前自拍留念。陈琦嘉摄金山岭长城杏花盛开陈琦嘉摄杏花盛开下的金……哎,难啊!赛季报销,又一年生涯结束了!为何他这么命苦?NBA常规赛接近尾声,季后赛竞争和排位进入白热火状态,很多球队的最终排名不到最后时刻都不确定。常规赛将在4月9日结束,而有一人则提前结束了本赛季,他就是罗斯。对于球……郭生白养生妙招青春永驻的秘密第二讲中医是生命本能系统医学(二)(上一篇:【自塑自我】它是很简单的一个事儿,可是每一个人,又看它不很简单是很复杂的。)郭老:我们先说周先生是人,人是谁?人是动物,……湿疹与你的饮食有关吗?哪些该吃哪些不该吃?湿疹或特应性皮炎是一种皮肤表面干燥、发痒及斑块的疾病。这通常是由于体内炎症而发展的,因此食用不会引起炎症的食物可能有助于减轻症状。有时,医生可能会建议避免已知会使湿……马伊琍难得洋气反被嘲,穿狗啃式牛仔裤,网友却说像垃圾堆捡来的女明星们走机场可真不是一件马虎的事儿,在很多媒体和粉丝蹲守的机场上,为了呈现出美美的状态,不让自己的丑照流传,女明星们在出发走机场时更是煞费苦心,从衣着搭配到妆容发型,都会经过……斯诺克再爆大冷!特鲁姆普遭横扫,罗伯逊被4北京时间3月26日,斯诺克直布罗陀公开赛继续进行,在最新结束的几场八强争夺赛中,冷门真的是迭爆,特鲁姆普04爆冷遭里奇沃顿横扫,澳洲火炮罗伯逊14爆冷不敌杰克琼斯,而中国选手丁……如何预防脑血栓发作?老医生告诉你!转发给家人上篇文章我们讲到了脑血栓发作的八个前兆,那么如何有效预防呢?今天张喜海院长就告诉大家十秒防栓法。保持一个动作十秒,就可以有效打扫你的血管,预防血栓抬脚。这时候肯定有……
沃尔沃计划到2025年将每辆车的碳足迹减少40沃尔沃启动了汽车行业最雄心勃勃的计划之一,旨在在2018年至2025年之间将每辆汽车的生命周期碳足迹减少40。这是迈向沃尔沃汽车到2040年成为气候中立公司的宏伟目标的第……加拿大电气化启动电动汽车充电网络ElectrifyCanada宣布将启动其全国性的电动汽车充电网络,并计划在全国各地的精选加拿大轮胎地点推出20多个装置。ElectrifyCanada和加拿大轮胎公司(……福特EV客户可通过福特通行证获得智能手机访问权限福特利用全面的福特充电解决方案生态系统解决电动车车主最大的担忧之一,即他们无法快速便捷地充电,这将为家庭和整个欧洲提供无缝,集成的充电方式。福特汽车将从明年开始交付新的全……现役美国VS世界,如果一场定胜负,谁会是最终的胜利者?2015年全明星周末,NBA联盟将新秀赛从原本的一年级对阵二年级,改为了美国新秀队VS世界新秀队,那如果将全明星赛也改成美国本土球员对阵世界球员的话,那哪支球队会是最后的胜利者……丰田在东京展示全电动城市车丰田汽车宣布,它将在2020年计划在日本商业推出之前,在2019年东京车展的未来博览会特别展览中展示其新的,可投入生产的超紧凑型电池电动汽车。超紧凑型两座BEV专为满足定……全新宾利飞驰的生产正在进行中宾利汽车(BentleyMotors)证实,全新的ldquo;飞驰rdquo;(FlyingSpur)(终极豪华旅行房轿车)的生产正在进行中,并计划于2020年初开始交付。……大众推出新的AtlasCrossSportSuvCoupe去年,AtlasCrossSport的量产版仅在纽约国际汽车展上作为概念车展出,现已在查塔努加(Chattanooga)亮相。成功的北美,俄罗斯和中东SUV现已提供轿跑车……全新的顶级齿轮杂志现在就出去ldquo;顶级齿轮rdquo;杂志的主编查理middot;特纳(CharlieTurner)对这个月的新一期ldquo;打屁股rdquo;发表了几句话,现在一切都结束了!在我……夏天,半身裙最正确的打开方式搭配这几款上衣,气质有女人味夏季怎么能少得了一条半身裙呢?时髦的半身裙是每个女生的季节必备,它可以为你的造型带来更多的美感,在穿搭能够将女性韵味很好地展示出来。气质女性的衣橱里离不开它,更应该搭配好……西单有什么好玩的(西单大悦城周边有什么好玩的)位于北京市西城区的西单,商贾云集,历史悠久,文化底蕴深厚,与王府井、大栅栏并称三大传统商业区。在老北京城人们俗称的西单牌楼得名,它位于北京市中心,是西城区著名的商业街区。……马自达凭借其迷人的新款CX30微型SUV重新确立了自己作为经马自达凭借其迷人的新款CX30微型SUV,重新确立了自己作为经济型汽车的首选品牌的地位。CX30的一切都是正确的,从它干净,引人注目的外观设计,尖锐的处理和价格,证明了一……墨西哥是大众汽车的关键州奥迪在重启时踩下刹车墨西哥城墨西哥普埃布拉州(Puebla)周五表示,由于大流行,该国汽车行业不存在重新启动活动的条件,这给汽车制造商在那里重新启动业务踩了刹车。大众集团(Volkswage……
友情链接:易事利快生活快传网聚热点七猫云快好知快百科中准网快好找文好找中准网快软网