一致性哈希算法

普通哈希

在分布式场景下,往往涉及到负载均衡,比如分布式缓存这种应用,我们希望如果获取相同的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)]]
}

posted @ 2021-12-02 18:38  moon_orange  阅读(65)  评论(0编辑  收藏  举报