[Consistent Hashing] Go 實(shí)現(xiàn)基于虛擬節(jié)點(diǎn)的一致性哈希

一致性哈希 (Consistent Hashing) 算法, 主要應(yīng)用在分布式系統(tǒng)中, 尤其是緩存, 解決負(fù)載均衡碳蛋、熱點(diǎn)、數(shù)據(jù)傾斜等問(wèn)題.

一.目的

將服務(wù)器節(jié)點(diǎn) Node, 映射到一個(gè)較長(zhǎng)的環(huán)上, 當(dāng)查找數(shù)據(jù)時(shí), 在環(huán)上順時(shí)針查找, 該數(shù)據(jù)的 Key 所臨近的服務(wù)器節(jié)點(diǎn) Node, 最后再到服務(wù)器節(jié)點(diǎn)上去查找源數(shù)據(jù).

二.個(gè)人對(duì)算法的理解

1.取一個(gè)環(huán), 作為哈希環(huán).

長(zhǎng)度一般是 2 的 32 次方 (4,294,967,296), 即 int32 的數(shù)值范圍.

2.定一個(gè)哈希規(guī)則, 哈希結(jié)果的值范圍在要跟哈希環(huán)的長(zhǎng)度適配. 比如環(huán)長(zhǎng)度為 2 的 32 次方, 則選用 CRC32 規(guī)則; Redis-Cluster Slots 的個(gè)數(shù)是 2 的 14 次方 (16384), 使用的規(guī)則是 CRC16.

3.使用哈希規(guī)則對(duì)服務(wù)器節(jié)點(diǎn) Node 進(jìn)行哈希運(yùn)算. 比如對(duì)服務(wù)器節(jié)點(diǎn)的 IP须眷、HostName 等, 進(jìn)行 CRC32 哈希運(yùn)算.

4.將哈希運(yùn)算的結(jié)果, 對(duì)哈希環(huán)的長(zhǎng)度進(jìn)行取余運(yùn)算, 然后將余數(shù)映射到哈希環(huán)上.

5.對(duì)查找數(shù)據(jù)的 Key 進(jìn)行 CRC32 哈希運(yùn)算, 并對(duì)哈希環(huán)的長(zhǎng)度進(jìn)行哈希取余操作, 然后順時(shí)針查找臨近的 Node. (Key 比如 user 表的 user id)

三.主要缺點(diǎn)

1.無(wú)法滿(mǎn)足負(fù)載均衡.

如果節(jié)點(diǎn)個(gè)數(shù)比較少, 極端假設(shè)有兩個(gè)節(jié)點(diǎn), 一個(gè)節(jié)點(diǎn) A 的 Hash 值為 0, 另一個(gè)節(jié)點(diǎn) B 的 Hash 值為 10, 而一般哈希環(huán)的長(zhǎng)度一般都相當(dāng)長(zhǎng) (如 int32 范圍), 導(dǎo)致的結(jié)果是 A 負(fù)載 11 ~ int32最大數(shù)值, 而僅僅負(fù)載 1~10.

2.新增節(jié)點(diǎn)導(dǎo)致大量數(shù)據(jù)遷移.

如果新增一個(gè)節(jié)點(diǎn) C, Hash 值為 3,000,000,000, 那么 11 ~3,000,000,000 對(duì)應(yīng)的這么大量的數(shù)據(jù), 會(huì)從 A 遷移到 C 來(lái).

新增的節(jié)點(diǎn), 只為一個(gè)節(jié)點(diǎn)分?jǐn)倝毫? 并不是都為其他節(jié)點(diǎn)分?jǐn)傄徊糠值呢?fù)載壓力.

四.解決的關(guān)鍵思想

節(jié)點(diǎn)越多, 分布越均勻.

負(fù)載越近似均衡, 數(shù)據(jù)傾斜越少.

如果沒(méi)有實(shí)際節(jié)點(diǎn), 那就通過(guò)新增虛擬節(jié)點(diǎn).

于是, 基于虛擬節(jié)點(diǎn)的一致性哈希誕生.

五.用 go 語(yǔ)言實(shí)現(xiàn), 基于虛擬節(jié)點(diǎn)的一致性哈希

1.接口抽象和結(jié)構(gòu)體定義

根據(jù)迪米勒原則, 抽象以下接口

type IConsistentHash interface {
    AddNode(node Node) error
    RemoveNode(node Node) error
    GetNode(key string) Node
}

真實(shí)節(jié)點(diǎn) Node, 作為值類(lèi)型.

type Node struct {
    Name string // 服務(wù)器名稱(chēng)
    IP   string // IP
    Port int    // 端口
}

虛擬節(jié)點(diǎn), 將用 map 來(lái)與真實(shí)節(jié)點(diǎn)的關(guān)聯(lián), RingIndexs 切片的索引就是虛擬節(jié)點(diǎn)的 ID.

type virtualNode struct {
    // 節(jié)點(diǎn)的 Hash 在哈希環(huán)上的索引
    RingIndexs []uint32
}

基于虛擬節(jié)點(diǎn)的一致性哈希.

type ConsistentHash struct {
    virtualNodeAmount int                   // 虛擬節(jié)點(diǎn)個(gè)數(shù)
    nodeMap           map[Node]*virtualNode // 真實(shí)節(jié)點(diǎn) 映射 虛擬節(jié)點(diǎn)
    hashVals          []uint32              // 所用虛擬節(jié)點(diǎn)的 Hash 值, 主要用于檢索
    hashValMap        map[uint32]Node       // 虛擬節(jié)點(diǎn) Hash 值, 映射真實(shí)節(jié)點(diǎn)
}

2.具體實(shí)現(xiàn)

2.1 虛擬節(jié)點(diǎn) Key 的構(gòu)造

func (*ConsistentHash) node2Str(n Node) string {
    // fmt.Sprintf("%s<%s:%d>", n.Name, n.IP, n.Port)
    return strings.Join([]string{
        n.Name, "<", n.IP, ":", strconv.Itoa(n.Port), ">",
    }, "")
}

func (c *ConsistentHash) genKey(nodeStr string, i int) string {
    // nodeA#0, nodeA#1, nodeA#2, ...
    // nodeB#0, nodeB#1, nodeB#2, ...
    return strings.Join([]string{nodeStr, strconv.Itoa(i)}, "#")
}

2.2 哈希算法

因?yàn)樗惴?hash 范圍正是環(huán)的長(zhǎng)度范圍, 所以不對(duì) math.MaxInt32 取余.

func (c *ConsistentHash) hashAlg(str string) uint32 {
    var bytes = []byte(str)
    return crc32.ChecksumIEEE(bytes)
}

2.3 對(duì)所有虛擬節(jié)點(diǎn)的 Hash 值的排序

快排算法, 時(shí)間復(fù)雜度: O(NlogN), N 為虛擬節(jié)點(diǎn)的個(gè)數(shù)

func (c *ConsistentHash) sort() {
    hashVals := make([]uint32, 0, len(c.hashValMap))
  // concat all RingIndexs
    for _, vn := range c.nodeMap {
        hashVals = append(hashVals, vn.RingIndexs...)
    }
    sort.Slice(hashVals, func(i, j int) bool {
        return hashVals[i] < hashVals[j]
    })
    c.hashVals = hashVals
}

2.5 接口實(shí)現(xiàn)-新增和刪除節(jié)點(diǎn)

在新增節(jié)點(diǎn)時(shí), 自動(dòng)創(chuàng)建虛擬節(jié)點(diǎn), 在刪除節(jié)點(diǎn)時(shí), 將節(jié)點(diǎn)對(duì)應(yīng)的所有虛擬節(jié)點(diǎn)進(jìn)行拔除.

并在修改節(jié)點(diǎn)后, 重新對(duì)所有虛擬節(jié)點(diǎn)的 Hash 值進(jìn)行排序.

var (
    ErrNodeExisted  = errors.New("Node is Existed")
    ErrNodeNotFound = errors.New("Node is not Found")
)

func (c *ConsistentHash) AddNode(node Node) error {
    if _, ok := c.nodeMap[node]; ok {
        return ErrNodeExisted
    }
    s := c.node2Str(node)
    vn := &virtualNode{
        RingIndexs: make([]uint32, 0, c.virtualNodeAmount),
    }
    for i := 0; i < c.virtualNodeAmount; i++ {
        hash := c.hashAlg(c.genKey(s, i))
        vn.RingIndexs = append(vn.RingIndexs, hash)
        c.hashValMap[hash] = node
    }
    c.nodeMap[node] = vn
    c.sort()
    return nil
}

func (c *ConsistentHash) RemoveNode(node Node) error {
    if _, ok := c.nodeMap[node]; !ok {
        return ErrNodeNotFound
    }
    delete(c.nodeMap, node)
    s := c.node2Str(node)
    for i := 0; i < c.virtualNodeAmount; i++ {
        hash := c.hashAlg(c.genKey(s, i))
        if _, ok := c.hashValMap[hash]; ok {
            delete(c.hashValMap, hash)
        }
    }
    c.sort()
    return nil
}

2.6 接口實(shí)現(xiàn)-獲取節(jié)點(diǎn)

使用折半算法, 對(duì)節(jié)點(diǎn)進(jìn)行查找

func (c *ConsistentHash) GetNode(key string) Node {
    // c.sort()
    hash := c.hashAlg(key)
    l, r := 0, len(c.hashVals)-1
    for l < r {
        m := (l + r) / 2
        if hash > c.hashVals[m] {
            l = m + 1
        } else if hash < c.hashVals[m] {
            r = m - 1
        } else {
            ringIndex := c.hashVals[m]
            log.Printf("[ConsistentHash/GetNode] key-HashVal:%v, ringIndex:%v.\n", hash, ringIndex)
            return c.hashValMap[ringIndex]
        }
    }
    index := l
    if hash > c.hashVals[index] {
        index = (l + 1) % len(c.hashVals)
    }
    ringIndex := c.hashVals[index]
    log.Printf("[ConsistentHash/GetNode] key-HashVal:%v, ringIndex:%v.\n", hash, ringIndex)
    return c.hashValMap[ringIndex]
}

3.測(cè)試

3.1 測(cè)試代碼

主要驗(yàn)證獲取的節(jié)點(diǎn)是否正確, 以及對(duì)新增和刪除節(jié)點(diǎn)的操作是否正確.

  • 真實(shí)節(jié)點(diǎn)個(gè)數(shù): 10

  • 虛擬節(jié)點(diǎn)個(gè)數(shù): 16

import (
    "testing"
)

var nodes = []Node{
    {Name: "appA.service.order", IP: "192.168.1.100", Port: 5000},
    {Name: "appA.service.pay", IP: "192.168.1.101", Port: 5000},
    {Name: "appA.service.history", IP: "192.168.1.102", Port: 5000},
    {Name: "appA.service.detail", IP: "192.168.1.103", Port: 5000},
    {Name: "appA.service.comment", IP: "192.168.1.104", Port: 5000},
    {Name: "appA.service.mq", IP: "192.168.1.105", Port: 5000},
    {Name: "appA.service.mysql", IP: "192.168.1.106", Port: 5000},
    {Name: "appA.service.gateway", IP: "192.168.1.107", Port: 5000},
    {Name: "appB.service.geo", IP: "192.168.1.108", Port: 5000},
    {Name: "appC.service.cdn", IP: "192.168.1.109", Port: 5000},
}
var user_id = "123456"

func TestConsistentHash_GetNode(t *testing.T) {
    c := NewConsistentHash(16, nodes...)

    node := c.GetNode(user_id)

    t.Logf("GetNode: %+v\n", node)
    for node, vn := range c.nodeMap {
        t.Logf("%+v, %+v\n", node.IP, vn.RingIndexs)
    }
    t.Log(c.hashVals)
}

func TestConsistentHash(t *testing.T) {
    c := NewConsistentHash(16, nodes...)

    node := c.GetNode(user_id)
    t.Logf("GetNode: %+v\n", node)
    err := c.RemoveNode(node)
    t.Log("RemoveNode Err:", err)
    err = c.AddNode(node)
    t.Log("AddNode Err:", err)
    node = c.GetNode(user_id)
    t.Logf("GetNode: %+v\n", node)
}

3.2 測(cè)試結(jié)果

排序花颗、新增節(jié)點(diǎn)惠拭、刪除節(jié)點(diǎn)扩劝、獲取節(jié)點(diǎn), 貌似都 Okay.

=== RUN   TestConsistentHash_GetNode
2021/06/25 [ConsistentHash/GetNode] key-HashVal:158520161, ringIndex:256098299.
    consistenthash_test.go:26: GetNode: {Name:appA.service.pay IP:192.168.1.101 Port:5000}
    ...
    consistenthash_test.go:30: [9950081 24722963 133830552 137264098 147812920 256098299 262321697 ...]
--- PASS: TestConsistentHash_GetNode (0.00s)
=== RUN   TestConsistentHash
2021/06/25 [ConsistentHash/GetNode] key-HashVal:158520161, ringIndex:256098299.
    consistenthash_test.go:37: GetNode: {Name:appA.service.pay IP:192.168.1.101 Port:5000}
    consistenthash_test.go:39: RemoveNode Err: <nil>
    consistenthash_test.go:41: AddNode Err: <nil>
2021/06/25 [ConsistentHash/GetNode] key-HashVal:158520161, ringIndex:256098299.
    consistenthash_test.go:43: GetNode: {Name:appA.service.pay IP:192.168.1.101 Port:5000}
--- PASS: TestConsistentHash (0.00s)
PASS
ok

六.參考

理解 Consistent Hashing

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末庸论,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子棒呛,更是在濱河造成了極大的恐慌,老刑警劉巖鱼喉,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異扛禽,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)编曼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)麸恍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人抹沪,你說(shuō)我怎么就攤上這事∪谂罚” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵噪馏,是天一觀的道長(zhǎng)绿饵。 經(jīng)常有香客問(wèn)我欠肾,道長(zhǎng)拟赊,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任吸祟,我火速辦了婚禮,結(jié)果婚禮上屋匕,老公的妹妹穿的比我還像新娘。我一直安慰自己过吻,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布缘琅。 她就那樣靜靜地躺著粘都,像睡著了一般。 火紅的嫁衣襯著肌膚如雪刷袍。 梳的紋絲不亂的頭發(fā)上翩隧,一...
    開(kāi)封第一講書(shū)人閱讀 49,144評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音呻纹,去河邊找鬼堆生。 笑死,一個(gè)胖子當(dāng)著我的面吹牛雷酪,可吹牛的內(nèi)容都是我干的淑仆。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼蔗怠,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼吩跋!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起锌钮,我...
    開(kāi)封第一講書(shū)人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎梁丘,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體掏觉,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡值漫,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了惭嚣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡延旧,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出迁沫,到底是詐尸還是另有隱情芦瘾,我是刑警寧澤集畅,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站祷愉,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏二鳄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一订讼、第九天 我趴在偏房一處隱蔽的房頂上張望扇苞。 院中可真熱鬧欺殿,春花似錦鳖敷、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至山宾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間资锰,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工直秆, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人圾结。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓齿诉,卻偏偏與公主長(zhǎng)得像筝野,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子歇竟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容