一致性哈希算法和Go语言实现
一致性哈希算法,当我第一次听到这个名字的时候,感觉特别高深。而它往往会和分布式系统相关,准确的说,是分布式缓存。
在Web服务中,缓存是介于数据库和服务端程序之间的一个东西。在网站的业务还不是很大的时候,一般不会需要这个东西,每次都可以从数据库中查询获得数据,但是随着网站的访问量增大,每次请求都访问数据库的话,数据库的压力就会非常大,导致响应变慢,于是就需要一种缓存数据库,它们的存储结构一般比较简单,而且它们的数据都是存储在内存中。典型的缓存数据库有Redis,Memcache。有了缓存之后,服务端的程序可以先从缓存中读取数据,如果发现没有数据,那么从数据库中读取,并且将它放入缓存中,局部性原理,它们下次被访问的概率会很大,因此,当再次访问该数据的时候,就可以直接从缓存中读取数据。由于磁盘和内存的性能差别是很大的,因此,命中缓存和不命中缓存的性能差距也是很大的。要增加缓存的命中概率,很显然,增加机器是一种简单有效的解决办法,但是一台机器的内存是非常有限的,因此,我们需要多台机器一起组成一个缓存集群。然后将数据分散到各个机器上,在这个分散过程中,我就需要一种hash算法。如果不加额外考虑,我们可以根据机器的台数n,给机器编号成0到n-1,使用模n的方法分散到各个机器中,根据概率的分布原理,每台机器获得的数据大致是相同的。
随着网站规模的增大,n台机器已经不足以支撑现有的数据缓存,这个时候,需要增加机器,如果还是按照取模的办法,假如现在的机器增加到n+1台,那么缓存的命中率会变成原来的1/(n+1),这个命中率实在是太低了。根据理论计算,要保证最大的负载均衡的情况下,我们增加一台机器后的最大命中率是可以达到原来的n/(n+1)的。就相当于从原来的每台机器上拿出1/(n+1)的数据,然后放到新的机器里面。根据数学归纳法可以求得在n台机器上增加m台机器,在达到最大化负载均衡的情况下,缓存命中率最高可以达到原来的n/(n+m)。那么怎样的一种哈希算法才可以使得扩容后的命令率接近这个最高理论值呢?这就是一致性哈希算法。
现在,我们要做的其实就是设计一个满足要求的哈希算法。我们的期望是这样的,当加入新的成员(缓存机器)的时候,我们可以分别从原来的机器中抽取一小部分的数据到我们新加入的机器中,这样就保证了原有机器的数据不变化。举个形象的例子,大家一起喝可乐,开始八个人,每个人一小满杯,刚好倒完。又来了两个人,这个时候,我们不可能把所有可乐倒回一个大瓶子,然后分给大家吧,最好最公平的方式就是大家都匀出一小点给新的两个空杯子。这只是个例子,不一定实际,但是道理是一样的。在这种分布式缓存系统中,我们应该怎么做呢?在量子力学理论中,能力是一份一份的,而不是连续的。受这个启发,我们可以把机器分成一小份一小份,然后每一小份负责一个哈希范围。这样一台机器就负责多个哈希范围。这些一小份一小份,我们称之为虚拟节点。再来考虑在n台机器基础上增加m台机器的情况。其实只要把原来机器每台总份数的m/(n+m)分给新的机器就可以了。至于整个哈希范围分成多少份,这个问题需要实践和经验来获得,显然,分多了的话,管理和操作的效率下降,而分少了呢,随着机器增加容易造成机器的负载不均匀。
首先,我们把整个哈希范围设想成一条线段,一般用一个无符号的32位整型来表示整个范围。在线段上,我们可以将它分成一段一段的,每一个小线段由头和尾两个坐标来表示,同时为了避免相邻两个线段交界处重复,我们可以预先定义每一个小线段都是左闭右开。然后每台机器都会管理多个小线段。当有新的机器加入的时候,我们根据加入的机器的数量,根据最大限度负载均衡原则,计算出需要从每个现有的机器中拿出多少份给新的机器。也就是说每个机器都会维护一个自己的数据结构,里面包括自己需要管理的区间的集合,而每一次机器的变动操作都会更新这个数据机构。而当要查询一个key的数据对应哪一台机器的时候,为了加快查询速度,需要建立一张倒排索引,以便快速查到对应的机器。这种方法是我自己根据一致性哈希达到的目的而想的。它可以实现一致性哈希的要求,但是明显管理起来比较麻烦,而且具体分成多少份也是一个难定夺的值。下面,对它进行简化。
上面,每一台机器管理一条线段,很明显这样子比较复杂,对于一段区间,需要维护一个包含两个值的元组。事实上,只需要维护一个点即可,我们可以定义这个点只管辖它本身以及顺时针方向直到下一个点(不包括下一个点)的区间。但是对于最后一个点和第一个点会有点麻烦,于是,把原来的线段两头相接,这样就形成了一个环。这样,每个点都可以往顺时针方向寻找,并且能够寻找到相应的点。因此,一台机器只要维护他自己的点的列表就可以了。在上面,当增加机器的时候,我们选择的是将已有的份移向新的机器,这样需要维护一个算法,保证最大负载均衡,而实际上可以在已有的环上增加点来达到分割已有机器数据的目的。例如在在线段一中加上一个点,则线段一为分割成两个线段,这样就巧妙的把数据分割问题解决了,当然这么做之后,导致总的份数不再是确定值(其实我们根本不在意总的份数),总的份数由机器数量以及每个机器占有的份数决定。每个机器占用多少份比较合理呢?有人给出的答案是150。当然这也是个经验值。现在,现在的一致性哈希算法变成了这样:每个机器有用固定个数的点,这些点分布在一个由32位无符号整形组成的环上。每个点管理本身和顺时针方向直到下一个点的区域。如何生成这些点呢?随机。使用哈希函数对机器名字进行哈希,得到一个无符号32位整型。在Golang中使用crc32.ChecksumIEEE
函数生成。因此对于一个key,需要查询它对应的机器的时候,只需要查找它逆时针方向的第一个点,然后找到这个点对应的机器即可。
下面是Golang语言的一个简单实现,仅供参考。主要借鉴了这份代码的实现。其实对于节点的查询,这里使用排序后二分查询(Golang自带sort包)。复杂度为logN
,但是插入和删除的复杂度是nlogN
。如果使用红黑树实现,两者的复杂度都是nlogN
,从算法角度来讲,使用红黑树比较合适,但是实际上一致性哈希使用最多的是查找,而对节点的改动相对较少,因此差距并不是很大。
import (
"hash/crc32"
"sort"
"strconv"
"sync"
) type errorString struct {
s string
} func (e *errorString) Error() string {
return "ConsistentError: " + e.s
} // 定义错误类型
func ConsistentError(text string) error {
return &errorString{text}
} // 定义环类型
type Circle []uint32 func (c Circle) Len() int {
return len(c)
} func (c Circle) Less(i, j int) bool {
return c[i] < c[j]
} func (c Circle) Swap(i, j int) {
c[i], c[j] = c[j], c[i]
} type Hash func(date []byte) uint32 type Consistent struct {
hash Hash // 产生uint32类型的函数
circle Circle // 环
virtualNodes int // 虚拟节点个数,文中所说150
virtualMap map[uint32]string // 点到主机的映射
members map[string]bool // 主机列表
sync.RWMutex
} func NewConsisten() *Consistent {
return &Consistent{
hash: crc32.ChecksumIEEE,
circle: Circle{},
virtualNodes: ,
virtualMap: make(map[uint32]string),
members: make(map[string]bool),
}
} // generate a string key for an element with an index
func (c *Consistent) eltKey(key string, idx int) string {
return key + "|" + strconv.Itoa(idx)
} func (c *Consistent) updateCricle() {
c.circle = Circle{}
for k := range c.virtualMap {
c.circle = append(c.circle, k)
}
sort.Sort(c.circle)
} func (c *Consistent) Members() []string {
c.RLock()
defer c.RUnlock() m := make([]string, len(c.members)) var i =
for k := range c.members {
m[i] = k
i++
} return m
} func (c *Consistent) Get(key string) string {
hashKey := c.hash([]byte(key))
c.RLock()
defer c.RUnlock() i := c.search(hashKey) return c.virtualMap[c.circle[i]]
} // search nearly vnode around key
// sort.Search uses binary search to find key
// every vnode cover its self and clockwise area
func (c *Consistent) search(key uint32) int {
f := func(x int) bool {
return c.circle[x] >= key
} i := sort.Search(len(c.circle), f)
i = i -
if i < {
i = len(c.circle) -
}
return i
} // this function is beautiful
func (c *Consistent) ForceSet(keys ...string) {
mems := c.Members()
for _, elt := range mems {
var found = false FOUNDLOOP:
for _, k := range keys {
if k == elt {
found = true
break FOUNDLOOP
}
}
if !found {
c.Remove(elt)
}
} for _, k := range keys {
c.RLock()
_, ok := c.members[k]
c.RUnlock() if !ok {
c.Add(k)
}
}
} func (c *Consistent) Add(elt string) {
c.Lock()
defer c.Unlock() if _, ok := c.members[elt]; ok {
return
} c.members[elt] = true for idx := ; idx < c.virtualNodes; idx++ {
c.virtualMap[c.hash([]byte(c.eltKey(elt, idx)))] = elt
} c.updateCricle()
} func (c *Consistent) Remove(elt string) {
c.Lock()
defer c.Unlock() if _, ok := c.members[elt]; !ok {
return
} delete(c.members, elt) for idx := ; idx < c.virtualNodes; idx++ {
delete(c.virtualMap, c.hash([]byte(c.eltKey(elt, idx))))
} c.updateCricle()
}
转载:http://cloudaice.com/consistent-hash/
一致性哈希算法和Go语言实现的更多相关文章
- .net的一致性哈希实现
最近在项目的微服务架构推进过程中,一个新的服务需要动态伸缩的弹性部署,所有容器化示例组成一个大的工作集群,以分布式处理的方式来完成一项工作,在集群中所有节点的任务分配过程中,由于集群工作节点需要动态增 ...
- 五分钟理解一致性哈希算法(consistent hashing)
转载请说明出处:http://blog.csdn.net/cywosp/article/details/23397179 一致性哈希算法在1997年由麻省理工学院提出的一种分布式哈希(DHT)实现算法 ...
- 每天进步一点点——五分钟理解一致性哈希算法(consistent hashing)
转载请说明出处:http://blog.csdn.net/cywosp/article/details/23397179 一致性哈希算法在1997年由麻省理工学院提出的一种分布式哈希(DHT) ...
- memcache 的内存管理介绍和 php实现memcache一致性哈希分布式算法
1 网络IO模型 安装memcached需要先安装libevent Memcached是多线程,非阻塞IO复用的网络模型,分为监听主线程和worker子线程,监听线程监听网络连接,接受请求后,将连接描 ...
- 经典算法和OJ网站(开发者必备-转)
一. Online Judge简介: Online Judge系统(简称OJ)是一个在线的判题系统.用户可以在线提交程序多种程序(如C.C++.Pascal)源代码,系统对源代码进行编译和执行,并通过 ...
- 一致性哈希算法以及其PHP实现
在做服务器负载均衡时候可供选择的负载均衡的算法有很多,包括: 轮循算法(Round Robin).哈希算法(HASH).最少连接算法(Least Connection).响应速度算法(Respons ...
- BM算法和Sunday快速字符串匹配算法
BM算法研究了很久了,说实话BM算法的资料还是比较少的,之前找了个资料看了,还是觉得有点生涩难懂,找了篇更好的和算法更好的,总算是把BM算法搞懂了. 1977年,Robert S.Boyer和J St ...
- 一致性哈希算法(consistent hashing)【转】
一致性哈希算法 来自:http://blog.csdn.net/cywosp/article/details/23397179 一致性哈希算法在1997年由麻省理工学院提出的一种分布式哈希 ...
- 一致性哈希算法——算法解决的核心问题是当slot数发生变化时,能够尽量少的移动数据
一致性哈希算法 摘自:http://blog.codinglabs.org/articles/consistent-hashing.html 算法简述 一致性哈希算法(Consistent Hashi ...
随机推荐
- java script sleep synchronous
function sleep(milliseconds) { var start = new Date().getTime(); for (var i = 0; i < 1e7; i++) { ...
- 上传 第三方jar包 nexus
手动上传第三方jar包到nexus的步奏: 1)按下图方式进行 2)按下图完成上传 3)点击保存完成
- oracle,PL/SQL新建表
创建用户 -- Create the user create user U_HQ_JAVA default tablespace USERS temporary tablespace TEMP pro ...
- 第八章 高级搜索树 (b1)B-树:动机
- python的协程和异步io【select|poll|epoll】
协程又叫做微线程,协程是一种用户态的轻量级的线程,操作系统根本就不知道协程的存在,完全由用户来控制,协程拥有自己的的寄存器的上下文和栈,协程调度切换时,将寄存器上下文和栈保存到其他地方,在切换回来后, ...
- Notepad++正则表达式格式 Editplus使用正则表达式[转]
使用正则表达式可以很好地完成很多繁琐耗时的工作,以下抄录editplus正则表达式的使用,同样适用于notepad++:表达式 说明 \t 制表符. \n 新行. . 匹配任意字符. | ...
- SQL某时间段记录查询
--查询当天: select * from info where DateDiff(dd,datetime,getdate())=0 --查询24小时内的: select * from info wh ...
- spring-boot基础概念与简单应用
1.spring家族 2.应用开发模式 2.1单体式应用 2.2微服务架构 微服务架构中每个服务都可以有自己的数据库 3.微服务架构应当注意的细节 3.1关于"持续集成,持续交付,持续部署 ...
- php5.3 php-fpm 开启 关闭 重启
自php5.3开始,php源码中包含了php-fpm,不需要单独通过补丁的方式安装php-fpm,在源码安装的时候直接 configure 中增加参数 –enable-fpm 即可. 所以启动.关 ...
- LFS(Linux From Scratch)学习
一.简介 LFS──Linux from Scratch,就是一种从网上直接下载源码,从头编译LINUX的安装方式.它不是发行版,只是一个菜谱,告诉你到哪里去买菜(下载源码),怎么把这些生东西( ra ...