Google Maglev Hashing實現
背景
Maglev是Google開發的基於kernal bypass技術實現的4層負載均衡,它具有非常強大的負載效能,承載了Google據大部分接入流量。Maglev在負載均衡演算法上採用自行開發的一致性雜湊演算法被稱為Maglev Hashing,該雜湊演算法在節點變化時能夠儘量少的影響其他幾點,且儘可能的保證負載的均衡,是一個非常優秀的一致性雜湊演算法,Google的技術都是自帶光環哈!下面想用Golang做一個簡單的實現。
原理說明
Maglev Hashing的基本思想是為每一個節點生成一個優先填充表,列表的值就是該節點想要填充查詢表的位置.

lookup table
如Table1所示,節點B0,會按照順序3,0,4,...依次去嘗試填充查詢表。實際上,所有的節點會輪流按照各自優先列表的值填充查詢表。也就是說,每個節點都有幾乎均等的機會根據優先表來填充查詢表,直到查詢表被填滿。
當出現節點變化,如B1宕機時,查詢表會重新生成,因為節點的優先填充表不變,所以B0和B2原來的填充位置不變,B1宕機後確實的位置被B0和B2瓜分,按照輪流填充的機制,B0和B2基本也是均衡的。
演算法實現
設 M
為查詢表的大小。對與每一個節點 i
, permutation[i]
為優先填充表, permutation[i]
的取值是陣列 [0, M-1]
的一個隨機順序排列, permutation
是一個二維陣列。
下面介紹論文給出的高效生成 permutation[i]
的方法:
首先使用兩種雜湊函式來雜湊節點生成兩個數字, offset skip
. 論文中是計算節點名稱的雜湊值,為了簡單我就直接計算了節點的索引值,雜湊函式我用的是 演算法導論 裡提到的乘法雜湊法,程式碼如下:
func Hash1(k int) int { s := uint64(2654435769) p := uint32(14) tmp := (s * uint64(k)) % (1 << 32) return int(tmp / (1 << (32 - p))) } func Hash2(k int) int { s := uint64(1654435769) p := uint32(14) tmp := (s * uint64(k)) % (1 << 32) return int(tmp / (1 << (32 - p))) }
第二個雜湊函式我只是修改了一個引數值,雜湊演算法是一樣的。 offset skip
計算方式如下:
offset←h1(name[i]) mod M skip←h2(name[i]) mod (M−1)+1
從而得到permutation[i]中每一個值的計算方式:
permutation[ i ][ j ]←(offset+ j×skip) mod M 0<=j<= M-1
這裡要注意的是M必須為質數,這樣才能儘可能保證skip與M互斥。尋找合適的質數M我使用了簡單的篩選演算法:
func isPrime(n int) bool { if n < 2 { return false } end := int(math.Sqrt(float64(n))) for i := 2; i <= end; i++ { if n%i == 0 { return false } } return true } func findPrime(n int) int { //始終有大於n的質數 for { if isPrime(n) { return n } n++ } }
上面介紹了一些輔助函式,下面介紹演算法的具體實現流程:
type MaglevHash struct { m, nint permutation [][]int entry[]int nodeState[]bool } func NewMaglevHash(n int) *MaglevHash { m := findPrime(5 * n) permutation := make([][]int, n) entry := make([]int, m) nodeState := make([]bool, n) for idx, _ := range nodeState { nodeState[idx] = true } return &MaglevHash{ m:m, n:n, permutation: permutation, entry:entry, nodeState:nodeState, } }
定義一個結構MaglevHash和結構體生成函式,golang的標準實現。其中permutation為一個N*M的二維陣列,entry為長度N的查詢表,nodeState為長度N的記錄節點時候的下線的表。
接下來是生成permutation的函式,計算節點時實際上傳入的是節點索引值加一,避免傳入0,影響雜湊值的計算:
func (mh *MaglevHash) Permutate() { for idx, _ := range mh.permutation { mh.permutation[idx] = make([]int, mh.m) } for i := 0; i < mh.n; i++ { offset := Hash1(i+1) % mh.m skip := Hash2(i+1)%(mh.m-1) + 1 for j := 0; j < mh.m; j++ { mh.permutation[i][j] = (offset + j*skip) % mh.m } } }
生成好節點優先填充表之後,就可以根據該表填充查詢表:
func (mh *MaglevHash) Populate() { for idx, _ := range mh.entry { mh.entry[idx] = -1 } next := make([]int, mh.n) n := 0 for { for i := 0; i < mh.n; i++ { if !mh.nodeState[i] { continue } c := mh.permutation[i][next[i]] for mh.entry[c] >= 0 { next[i]++ c = mh.permutation[i][next[i]] } mh.entry[c] = i next[i]++ n++ if n == mh.m { return } } } }
在填充查詢表時,會檢查節點是否下線,若節點下線,則會忽略該節點。
func (mh *MaglevHash) DownNode(idx int) error { if idx > mh.n-1 { return errors.New("invalid idx") } mh.nodeState[idx] = false return nil }
節點下線時,需要呼叫該函式,然後再呼叫 Populate()
重新填充查詢表。
至此,Maglev hashing 一個簡單的實現就算完成了,後續希望使用生產環境的雜湊函式來替換本文用到雜湊函式,並考慮在nginx上實現該一致性雜湊演算法。