golang consistent hashing

爲何須要一致性hash

假設你有10000個併發請求,同時請求單臺redis(又是redis :p ),此時redis是處理不了這麼多併發請求的。git

那麼如何提供系統的高可用性呢?

一個比較簡單的想法就是對於系統進行橫向擴展(也就是加機器),而且對於一些讀寫請求進行hash路由。github

好比,目前你有四臺redis服務器[0,1,2,3],此時client發起請求set("name","tom")golang

此時的路由變成redis

1.crc32('name') % 4  // 先找到寫哪臺redis
2.set("name","tom")  // 真實的寫操做
複製代碼

這樣看起來的確提高了系統的可用性,可是假設業務量暴漲,4臺redis也處理不過來了,那麼咱們此時的想法必定也是加機器(:p),可是加機器可能致使以前存儲在redis上的key失效,以加當前機器基礎上兩臺機器爲例:算法

crc32('name') % 6  != crc32('name') % 4
複製代碼

從上面的代碼能夠看出此時直接加機器的方式會致使key失效(可能會致使緩存雪崩,或者對於一些強依賴cache的服務,會形成部分數據丟失,服務不可用),此時就引入了一致性hash的概念數據庫

什麼是一致性hash

In computer science, consistent hashing is a special kind of hashing such that when a hash table is resized, only K/n keys need to be remapped on average, where K is the number of keys, and n is the number of slots.數組

上述是wikipad給出的示意,翻譯過來就是緩存

在計算機科學中,一致性hash是一種特殊的hash,這樣當調整hash表的大小時,平均只須要從新映射K/n個key,其中K是keys的數量,n是slot的數量。bash

實際理解起來仍是有點抽象,舉個例子,目前有10個key,3臺服務器,假設你加兩臺機器那麼須要變化的就是 10 / 5 = 2個key服務器

一致性Hash算法也是使用取模的方法,只是,剛纔描述的取模法是對服務器的數量進行取模,而一致性Hash算法是對2^32取模

哈希

如上圖是一致性hash實現,相似圓環

一致性hash數據傾斜問題

實際上還有一個問題,一致性Hash算法在服務節點太少時,容易由於節點分部不均勻而形成數據傾斜

網圖

如上圖,A節點(機器)附近的key比較多,而B節點只有一個key,那麼怎麼解決這種問題呢?

虛擬節點的概念被引入了

如圖,由於引入了虛擬節點,使得key分佈的更均勻了(NODEA#1,NODEA#2NODEA的虛擬節點。

golang consistent hasing實現

讓咱們來看一個golang consistent庫實現 eg:https://github.com/stathat/consistent

咱們先來看下一致性hash的結構體

type Consistent struct {
	circle           map[uint32]string  // 存儲crc32後該key的值
	members          map[string]bool    // 存儲鍵值
	sortedHashes     uints              // 排序後的數組
	NumberOfReplicas int                // 其實是虛擬節點
	count            int64              // 整個結構體的數量
	scratch          [64]byte           // 這個字段沒有用
	sync.RWMutex                        // 讀寫鎖
}
複製代碼

初始化

func New() *Consistent {
	c := new(Consistent)
	c.NumberOfReplicas = 20             // 默認每一個節點虛擬節點的數量
	c.circle = make(map[uint32]string)  // 初始化circle
	c.members = make(map[string]bool)   // 初始化members
	return c
}
複製代碼

新增機器

func (c *Consistent) Add(elt string) {
	c.Lock()
	defer c.Unlock()
	// 增長互斥鎖,防止併發新增
	c.add(elt)
}

func (c *Consistent) add(elt string) {
    // 遍歷,增長虛節點cache
	for i := 0; i < c.NumberOfReplicas; i++ {
		c.circle[c.hashKey(c.eltKey(elt, i))] = elt
		// output like: c.circle[1765504436] = cacheA
	}
	// 存儲鍵值
	c.members[elt] = true
    // 使hashkey有序
	c.updateSortedHashes()
	// 數量 + 1
	c.count++
}
// 對key進行string化
func (c *Consistent) eltKey(elt string, idx int) string {
	// return elt + "|" + strconv.Itoa(idx)

	// if string == cacheA
	/* output like 0cacheA 1cacheA 2cacheA */
	return strconv.Itoa(idx) + elt
}


func (c *Consistent) hashKey(key string) uint32 {
	// 若是傳進來的字符串小於64位,優化操做
	if len(key) < 64 {
		var scratch [64]byte
		copy(scratch[:], key)
		return crc32.ChecksumIEEE(scratch[:len(key)])
	}
	// 對於key進行crc32得出key的int值
	return crc32.ChecksumIEEE([]byte(key))
}
複製代碼

查找數據接口

func (c *Consistent) Get(name string) (string, error) {
	// 加讀鎖
	c.RLock()
	defer c.RUnlock()
	// 若是c.circle沒數據,返回error
	if len(c.circle) == 0 {
		return "", ErrEmptyCircle
	}
	// 把key hash化
	key := c.hashKey(name)
	// 搜索key
	i := c.search(key)

	return c.circle[c.sortedHashes[i]], nil
}

// 查找過程
func (c *Consistent) search(key uint32) (i int) {

	f := func(x int) bool {
		return c.sortedHashes[x] > key
	}
    // sort.Search其實是個基於f()函數進行search,找到c.sortedHashes[x] > key的位置而後進行返回
	i = sort.Search(len(c.sortedHashes), f)
    // 若是i>數組的長度,則默認i在0號位置上
	if i >= len(c.sortedHashes) {
		i = 0
	}

	return
}
複製代碼

從一致性hash內移除數據(機器)

func (c *Consistent) Remove(elt string) {
	c.Lock()
	defer c.Unlock()
	// 加鎖,防止併發刪除
	c.remove(elt)
}
// 移除數據
func (c *Consistent) remove(elt string) {
	for i := 0; i < c.NumberOfReplicas; i++ {
	    // 從map裏面刪除這個元素
		delete(c.circle, c.hashKey(c.eltKey(elt, i)))
	}
	delete(c.members, elt)
	// 從新排序
	c.updateSortedHashes()
	// 數量 - 1
	c.count--
}
複製代碼

一致性hash的應用

  • Partitioning component of Amazon's storage system Dynamo
  • Data partitioning in Apache Cassandra
  • Data Partitioning in Voldemort

一致性hash在這幾款數據庫都有應用

refrence

相關文章
相關標籤/搜索