从groupcache看一致性哈希
一致性哈希(consistenthash)
什么是一致性哈希
在分布式缓存中,假设我们有3台缓存服务器,我们有三万张图片要缓存数据要分配到这三台服务器上,常见的做法就是哈希,用图片名对服务器的个数取模,根据取模结果分配,这样就可以分配的很均匀。但是,如果3台机器不够,要加机器呢?这时候,机器数变了,那么之前key和服务器之间的对应关系就变了,这就意味着你的缓存在一定时间里就失效了。
因此基于传统哈希算法的缺陷,就很危险。于是就有了一致性哈希算法。一致性哈希算法可以保证当机器增加或者减少的时候,节点之间的数据迁移仅限于两个节点之间。
一致性哈希的原理
一致性哈希怎么做呢,他不对机器个数取模,而是对2^32次方取模,它的流程是这样的:
● 一致性哈希算法将整个哈希值空间按照顺时针组成一个换,叫哈希环,(就是一个有2^32个坑的环)
● 对服务器的标识(名字或者地址)做哈希,确定每个服务器在哈希环上的位置。
● 对数据的key做哈希,确定数据在哈希环上的位置,然后顺时针走,第一台遇到的服务器就是它定位到的服务器。
在这个时候,增减机器的时候,只会涉及到一部分数据的重定位,而不是大多数的数据。
优点:
○ 简单对服务器数量取模,增减机器的时候很可能会造成缓存的雪崩。而一致性哈希算法在增减机器的时候,只需重定位环空间一小部分的数据,只有部分缓存失效,具有较好的容错性和可扩展性。
扩展:缓存雪崩
缓存雪崩是指一段时间内缓存中大批量数据过期了,然后一大堆请求打到了后端数据库上,然后数据库扛不住了。
哈希环的倾斜和虚拟节点
当节点较少的时候,很可能出现哈希环的倾斜现象,这个现象是这样的:节点在哈希环上的位置都比较近,那么顺时针来看,第一个节点要负责环上一大部分数据,缓存的对象大部分集中到了一台服务器上,这样还是有可能造成数据不均匀的现象,再严重就是服务器的崩溃。
盗个图:
解决方法:虚拟节点
虚拟节点的本质就是让哈希环上的节点尽可能地多,可以对服务器地标识多次哈希,最终每个机器在换上都有多个节点,这样可以解决倾斜问题。
(后面又看到又chord算法,分布式哈希什么的, 越来越多了,后面还是专门写一篇文章吧)
GroupCache的实现
GroupCache的一致性哈希实现非常地简单,代码总共60多行。
type Hash func(data []byte) uint32
type Map struct {
hash Hash
replicas int
keys []int // Sorted
hashMap map[int]string
}
func New(replicas int, fn Hash) *Map {
m := &Map{
replicas: replicas,
hash: fn,
hashMap: make(map[int]string),
}
if m.hash == nil {
m.hash = crc32.ChecksumIEEE
}
return m
}
首先,定义了Map结构体,里面的hash是哈希函数,默认是go标准库里的crc.32CheckSumIEEE,keys存放排序后的节点的哈希值,hashMap存储哈希值和节点的映射。replicas代表一个服务器在哈希环上有几个节点。
接下来看它的存取过程:
加入:
// Add adds some keys to the hash.
func (m *Map) Add(keys ...string) {
for _, key := range keys {
for i := 0; i < m.replicas; i++ {
hash := int(m.hash([]byte(strconv.Itoa(i) + key)))
m.keys = append(m.keys, hash)
m.hashMap[hash] = key
}
}
sort.Ints(m.keys) // 必须得排序
}
可以看到第二重循环循环了replicas次,将一个服务器哈希多次,然后存到keys这个数组里面进行排序,通过hashMap建立映射。
取出:
// Get gets the closest item in the hash to the provided key.
func (m *Map) Get(key string) string {
if m.IsEmpty() {
return ""
}
hash := int(m.hash([]byte(key)))
// Binary search for appropriate replica.
// 二分查找
idx := sort.Search(len(m.keys), func(i int) bool { return m.keys[i] >= hash })
// Means we have cycled back to the first replica.
if idx == len(m.keys) {
idx = 0
}
return m.hashMap[m.keys[idx]]
}
查找就是将数据的key做一个哈希,然后根据这个哈希做二分查找,然后找到合适的去hashMap中取节点信息。
我细看了一下sort.Search,这个函数根据传入的函数做判断,会返回第一个判断结果为true的index。所注意这个函数是寻找左边界的二分查找(区间定为左闭右开,然后mid匹配上的时候,将mid-1作为右边界,最后返回左边界)。如果Search查不到符合要求的值,就会返回切片长度。因此上面的会这样写。这也是一个常见的循环数组查找的写法吧。