Golang 一致性 Hash 算法實現
❝
一致性 hash 算法是在分佈式應用中使用廣泛。其主要作用是爲了解決服務中的熱點問題。例如在分佈式數據存儲,比如 Redis 緩存集羣、有狀態的任務作業等,通過其解決請求的熱點問題,並且可以緩解當發生服務變動時出現的負載不平衡情況。
❞
本文將簡單介紹一致性 HASH 算法,並分析 golang 的一個開源實現包 (github.com/stathat/consistent
)
一致性 hash 算法原理
解決的問題
在分佈式存儲中,爲了保證不同機器緩存的數據均衡性以及負載的均衡性,一般會在代理層先做一次 hash。正常情況下,我們會做取模操作,然後將服務分配給不同的機器。但是這種情況下當某臺服務宕機或者有新機器加入時,會直接影響到所有機器的緩存。爲了解決這個問題,所以出現了一致性 hash 算法。
如何解決
一致性 hash 算法,主要是解決如何將 hash 後的值映射到既定的機器中。下面是 hash 步驟:
1,將 hash 值構造爲一個圓環,一般爲 [0,2^32 - 1] 2. 在圓環上均勻的挑選 hash 點,作爲機器的節點 3. 在訪問機器時,hash 到圓環的某個點上,然後順時針訪問到的第一個節點即爲需要訪問的機器。
這樣,當某臺機器宕機時,宕機機器的下一臺機器將承包宕機機器的存儲任務。如果 hash 環中的兩個機器節點中添加了一個機器節點,那原有的節點存儲任務將被分一部分存儲任務給新增加的節點。
昇華
正常情況下,如果使用上面說的一致性 hash 算法,出現宕機時,一臺機器將幹兩臺機器的活,這顯然是不科學的。因此還需要有虛擬節點的概念。將虛擬節點放在圓環上,然後真正的服務節點維護多個虛擬節點(並且需要保證一臺服務節點上的多個虛擬節點需要離散分佈)。這樣,當一個服務節點宕機後,其實是多個離散的虛擬節點從 hash 環中消失,這樣可以保證正常的機器在保留原有數據的基礎上,還能承載宕機對應的離散節點的負載。
一致性 hash 算法的 golang 實現
github.com/stathat/consistent
是 golang 實現的一個一致性 hash 算法。
數據結構
在實現中,增加了分佈式緩存中較爲實用的副本的概念。結構如下:
type Consistent struct {
circle map[uint32]string // 保存環
members map[string]bool // 成員標記
sortedHashes uints // []int32 類型 標記了環上的虛擬節點,有序的slice
NumberOfReplicas int // 副本數量
count int64 // 節點數量
scratch [64]byte // 未使用
UseFnv bool // 標記使用 FNV-1a hash
sync.RWMutex // 讀寫鎖標記
}
設置節點
初始化節點時,需要提供各個節點的名稱,以及副本的數量。初始化將進行如下操作:
-
將 hash 與 node 名的映射存入 circle 中
-
將 members 標記爲 true
-
將 hash 值放入有序的 sortedHashes 中
func (c *Consistent) Set(elts []string) {
c.Lock()
defer c.Unlock()
for k := range c.members { // 首先將清除不在elts 集合中的數據
found := false
for _, v := range elts {
if k == v {
found = true
break
}
}
if !found {
c.remove(k)
}
}
for _, v := range elts { // 之後依次添加
_, exists := c.members[v]
if exists {
continue
}
c.add(v)
}
}
func (c *Consistent) add(elt string) {
for i := 0; i < c.NumberOfReplicas; i++ {
c.circle[c.hashKey(c.eltKey(elt, i))] = elt
}
c.members[elt] = true
c.updateSortedHashes() // 更新sortedHashes 字段,保證有序
c.count++
}
訪問
當通過 name 映射一個 node 時,首先計算 hash 值,然後通過二分查找的方法從 sortedHashes slice 中拿到對應的 node 的 hash 值,最後通過 circle 映射出對應的 node name.
func (c *Consistent) Get(name string) (string, error) {
c.RLock()
defer c.RUnlock()
if len(c.circle) == 0 {
return "", ErrEmptyCircle
}
key := c.hashKey(name)
i := c.search(key)
return c.circle[c.sortedHashes[i]], nil
}
刪除
當某臺機器宕機時,可以刪除對應的 node 節點,以釋放快速調整服務請求。
func (c *Consistent) remove(elt string) {
// 代碼中有加鎖
for i := 0; i < c.NumberOfReplicas; i++ {
delete(c.circle, c.hashKey(c.eltKey(elt, i)))
}
delete(c.members, elt)
c.updateSortedHashes()
c.count--
}
總結
從代碼的實現中,我們可以看出:
-
從環中查找下一個節點,只需要將所有節點的 hash 值存放在一個有序的 slice 中,做二分查找即可。爲了保證每個節點數據的均衡性,一般會添加較多的虛擬節點,爲了保證訪問請求的速度,又不能過多。例如,在 Codis 中,虛擬節點默認一般有 1024 個(當然也可以配置更多的槽)
-
一致性 hash 對服務在做調整時可以做平滑過度
-
比較常見的應用, 例如分佈式緩存服務 Redis-Cluster, Codis;或者 服務代理 twemproxy 等
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/M65Zz5UBNsy4gnuyDfb17Q