groupcache 第二弹 consistenthash

801 阅读2分钟

关于一致性 hash网上有很多讲的非常好的教程,这个就不在此赘述了,就简单的说下。

普通的缓存场景如下:

  1. 如果有 N 太服务器用来做缓存,那么查询的数据key,则可以根据hash(key)%N来确定缓存的机器,并进行查询。
  2. 在机器挂了,或者要新增机器的时候,比较难以处理了。

在普通缓存场景比较棘手的时候,就出现了一致性 hash。该算法将所有的哈希值构成了一个环,其范围在 0 ~ 2^32-1。在查询 key 出现的时候,根据 key 算得 hash 在顺时针找到的第一个服务器上进行查询即可。

img

但是呢,服务器的分布可能是不均匀的,比如会出现如下这种情况,这个时候大多数 查询的 key 会在节点N1上进行查询。

img

所以就有了虚拟节点的概念,比如说虚拟节点的个数为2,那么每个节点会计算两次 hash,两次 hash 虽然占用了两个位置,但是都是指向的一个节点。

img

groupcache中的具体实现

源码中只定义了一个结构体Map如下:

type Hash func(data []byte) uint32

type Map struct {
	// hash 函数
	hash     Hash
	// 每个节点的副本数(每个节点在环上实际的节点个数是 replicas,而不是 replicas+1)
	replicas int
	// 所有节点的 hash 切片,实际中 keys 的长度并不是 2^32-1
	keys     []int // Sorted
	// 上面的 keys 切片中,每个 hash 实际对应的节点
	hashMap  map[int]string
}

第一个方法就是创建一个Map:

func New(replicas int, fn Hash) *Map {
	m := &Map{
		replicas: replicas,
		hash:     fn,
		hashMap:  make(map[int]string),
	}
    	// 如果函数的 fn 函数是 nil,则传入一个默认的函数
	if m.hash == nil {
		m.hash = crc32.ChecksumIEEE
	}
	return m
}

第二个方法是添加节点Add:

func (m *Map) Add(keys ...string) {
	for _, key := range keys {
         	// 每个 key 添加的 replicas 个副本
		for i := 0; i < m.replicas; i++ {
   			// 通过给每个 key 加一个前缀,来产生 hash 值
			hash := int(m.hash([]byte(strconv.Itoa(i) + key)))
             		// 所有的 hash 值都会存放到 keys 切片里面去
			m.keys = append(m.keys, hash)
             		// 虽然添加了多个,但是实际指向的是 key
			m.hashMap[hash] = key
		}
	}
	sort.Ints(m.keys)
}

第三个方式是获取查询的节点Get(忽略了IsEmpty方法):

func (m *Map) IsEmpty() bool {
	return len(m.keys) == 0
}

func (m *Map) Get(key string) string {
	if m.IsEmpty() {
		return ""
	}

	hash := int(m.hash([]byte(key)))

	// 通过二分搜索,查找 hash 在数组中应该插入的位置,就是应该查询的节点
	idx := sort.Search(len(m.keys), func(i int) bool { return m.keys[i] >= hash })

	// 如果索引的值等于 keys 的长度,说明到达环的边界,取值为0
	if idx == len(m.keys) {
		idx = 0
	}

	return m.hashMap[m.keys[idx]]
}

一致性 hash demo

package main
func main() {
	hash1 := consistenthash.New(2, nil)
	hash1.Add("www.baidu.com", "www.google.com")
	for i := 0; i < 10; i++ {
		fmt.Println(hash1.Get(strconv.Itoa(i)))
	}
	// Output:
	//www.baidu.com
	//www.google.com
	//www.baidu.com
	//www.google.com
	//www.baidu.com
	//www.google.com
	//www.baidu.com
	//www.google.com
	//www.baidu.com
	//www.google.com
}

这个 demo 中节点的副本数都是2,在查询09的值得时候,分布的是比较均匀的,百度和谷歌依次出现。

参考

  1. crossoverjie.top/2018/01/08/…
  2. www.zsythink.net/archives/11…