一致性哈希
1 为什么要用一致性哈希?
1.1 如何选择节点
对于分布式缓存来说,当一个节点接收到请求时,如果该节点没有存储缓存值,它面临的问题是:该从哪个节点获取数据?
假设现在存在 10
个分布式节点,当第一个节点接收到请求时,随机选择一个节点,由该节点从数据源获取数据,该节点从数据源获取数据的同时缓存该数据。当第二次接收到同样的key
时,只有1/10
的概率选择同一个节点,有9/10
的概率命中其他节点,这就意味着要再次从数据源获取数据。这样做,一是缓存效率低;二是各个节点上存储着重复数据,浪费了大量的存储空间。
如何能够对于给定的key
,每一次都选择同一个节点呢?使用hash
算法也许可以。
那么怎么设计hash
函数呢?
可以将key
的每一个字符的ASCII码
加起来,再对节点个数取模得到hash
值。这样的确可以解决上述问题。
1.2 节点数量变化
简单设计函数计算hash
值可以解决缓存性能的问题,但当节点数量发生变化时,所有的缓存值对应的节点都会发生改变,即几乎所有的缓存值都失效了。节点在接收到对应的请求时,都需要重新从数据源获取数据,容易引起缓存雪崩。
缓存雪崩:缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩。常因为缓存服务器宕机,或缓存设置了相同的过期时间引起。
如何解决这个问题呢?一致性哈希可以。
2. 一致性哈希原理
一致性哈希算法将key
映射到2^32
的空间中,将这个空间首尾相连,形成一个环。
- 计算节点/机器的哈希值,放置在环上。
- 计算
key
的哈希值,放置在环上,顺时针找到的第一个节点,就是该key
应选取的节点。
一致性哈希在增加/删除节点时,只需要重新定位该节点附近的一小部分数据,而不需要重新定位所有的节点,这就解决了上述的问题。
3. 数据倾斜问题
如果服务器的节点过少,容易导致大量的数据集中在某个节点上,负载不均,这就是数据倾斜问题。
为了解决这个问题,引入了虚拟节点的概念,一个真实节点对应多个虚拟节点。
- 第一步,计算虚拟节点的
Hash
值,放置在环上。 - 第二步,计算
key
的Hash
值,在环上顺时针寻找到应选取的虚拟节点,例如是peer2-1
,那么就对应真实节点peer2
。
虚拟节点扩充了节点的数量,解决了节点较少的情况下数据容易倾斜的问题。而且代价非常小,只需要增加一个字典(map)
维护真实节点与虚拟节点的映射关系即可。
package consistenthash
import (
"hash/crc32"
"sort"
"strconv"
)
type Hash func(data []byte) uint32
type Map struct {
hash Hash // Hash函数
replicas int // 虚拟节点倍数
keys []int // 哈希环
hashMap map[int]string // 虚拟节点和真实节点的映射表 key:虚拟节点hash值,value:真实节点名称
}
// New 允许自定义虚拟节点倍数及hash函数
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
}
// Add 添加真实节点/机器
func (m *Map) Add(peers ...string) {
// 对于每一个真实节点,生成对应的虚拟节点
for _, key := range peers {
for i := 0; i < m.replicas; i++ {
// hash值根据 序号+真实节点的key 生成
hash := int(m.hash([]byte(strconv.Itoa(i) + key)))
// 加入环
m.keys = append(m.keys, hash)
// 记录映射关系
m.hashMap[hash] = key
}
}
sort.Ints(m.keys)
}
// Get 选择节点:根据传入的值key计算hash,获取处理请求的虚拟节点
func (m *Map) Get(key string) string {
if len(m.keys) == 0 {
return ""
}
// 计算数据key的hash值
hash := int(m.hash([]byte(key)))
// 二分查找 顺时针寻找第一个匹配的节点位置
idx := sort.Search(len(m.keys), func(i int) bool {
return m.keys[i] >= hash
})
// 返回当前节点对应的真实节点
// 因为keys结构是个环,所以这里对idx取模更加严谨
// 实际上idx范围是[0, len(m.keys)),并不会溢出
return m.hashMap[m.keys[idx%len(m.keys)]]
}