一致性哈希算法
普通哈希
在分布式场景下,往往涉及到负载均衡,比如分布式缓存这种应用,我们希望如果获取相同的key,尽量映射到同一台机器上,这样可以在内存中最快速的获取,如果相同的key总是打到不同的机器上,可能由于缓存淘汰导致该缓存值被替换掉,进而需要读取磁盘,这样性能就会大打折扣。
基于此,我们可以获取key值的哈希值,该哈希值对应的就是机器节点,比如key和节点个数做取余运算(key%n),这样每次相同的key必然会打到同一台机器上。
但是问题又出现了,分布式环境中最常见的问题就是节点宕机,如果一台节点下线了,那么之前的映射关系将全部失效。那么大部分的内存缓存将会失效,数据库读取磁盘的压力将会瞬间达到极限,造成雪崩。
所以普通的哈希不能适应动态的节点变化,怎么让哈希在节点增加或减少时仍然有效呢?一致性哈希算法诞生了。
一致性哈希
一致性哈希是对普通哈希的改进,普通的哈希是的本质是将一些key散列到一条无限长的线上,而一致性哈希是将key映射到一条环上(抽象概念的环,实际存储还是存到切片中)。
现在有n个机器节点,对这些机器节点进行哈希(可以根据机器名,机器编号之类的属性进行哈希)。这样机器节点就散列在这个环上,此时请求key值,会对key进行哈希,然后选取该哈希值右边最近的那个节点进行请求。
比如该环上,小圆代表机器节点,小圆的值是节点hash之后的值。那么对于key的hash值14,它处于环的这个位置,它右边最近的节点是20,那么请求将达到节点20上。同理key45会打到节点1上。
这样我们增加节点,或是删除节点,只有一部分的key会受到影响,不会使得全部key都失效。
哈希环的要点
但是问题又出现了,哈希环还有他的缺点,那就是节点数量不能太少,如果节点太少,是不能均匀的哈希到环上的,会导致节点在一段区间上聚集。就比如上图中的节点1,它左边有一大部分的空间,这会导致节点1的压力大于其他节点。如果节点哈希的不均匀,就会导致某个节点压力过高而宕机,进而请求压力会转移至下个节点,进而节点逐个宕机。
解决问题的关键很简单:那就是增加节点个数,只有节点数量上来,才能更均匀的散列。但是我们节点的个数往往是有限的,如果我们只有3个节点,该怎么办呢?
虚拟节点
我们可以给一个真实节点创建n个虚拟节点,比如真实节点编号为1,那么他的3个虚拟几点编号一般命名为01,11,21。我们将虚拟节点哈希到哈希环上,当虚拟节点称为被请求的节点时,就将它映射到我们的实际节点上。
这样即使我们只有3个节点,我们可以为每个节点虚拟出100个虚拟节点,或者更多。这足以让节点们均匀散列。
一致性哈希实现
package consistent_hash
import (
"hash/crc32"
"sort"
"strconv"
)
type HashFunc func([]byte) uint32
type Map struct {
hash HashFunc // 求服务节点哈希值的算法
sortedKeys []int // 哈希环,存储服务节点的哈希值
vnode map[int]string // 虚拟节点映射
replicas int // 虚拟节点扩容倍数
}
func NewMap(replicas int, hash HashFunc) *Map {
m := &Map{
hash: hash,
sortedKeys: make([]int, 0),
vnode: make(map[int]string),
replicas: replicas,
}
if hash == nil {
m.hash = crc32.ChecksumIEEE // 默认使用crc
}
return m
}
// 添加n个服务节点
func (m *Map) Add(nodes ...string) {
for _, n := range nodes {
for i := 0; i < m.replicas; i++ {
h := int(m.hash([]byte(strconv.Itoa(i) + n))) // hashcode -> 0servername
m.sortedKeys = append(m.sortedKeys, h)
m.vnode[h] = n
}
}
sort.Ints(m.sortedKeys)
}
// 删除服务节点
func (m *Map) Remove(nodes ...string) {
for _, n := range nodes {
for i := 0; i < m.replicas; i++ {
h := int(m.hash([]byte(strconv.Itoa(i) + n)))
idx := sort.SearchInts(m.sortedKeys, h)
// 如果在哈希环找不到要删除的节点,可直接结束寻找
if idx == len(m.sortedKeys) || m.sortedKeys[idx] != h {
break
}
m.sortedKeys = append(m.sortedKeys[:idx], m.sortedKeys[idx+1:]...)
delete(m.vnode, h)
}
}
sort.Ints(m.sortedKeys)
}
// 获取一个key的存储位置
func (m *Map) Get(key []byte) string {
if len(key) == 0 || len(m.sortedKeys) == 0 {
return ""
}
h := int(m.hash(key))
// 当找不到h时,返回数组的长度
idx := sort.Search(len(m.sortedKeys), func(i int) bool {
return m.sortedKeys[i] >= h
})
return m.vnode[m.sortedKeys[idx%len(m.sortedKeys)]]
}