一致性hash算法

一致性哈希算法原理和實現(xiàn)

在做服務(wù)器負(fù)載均衡時候可供選擇的算法有很多,包括: 輪循算法(Round Robin)、哈希算法(HASH)、最少連接算法(Least Connection)、響應(yīng)速度算法(Response Time)、加權(quán)法(Weighted )等。其中哈希算法是最為常用的算法,比如在nginx+ats / haproxy+squid等CDN架構(gòu)中,nginx/haproxy所使用的負(fù)載均衡算法便是一致性哈希。不僅如此在分布式系統(tǒng)中哈希算法也得到了廣泛應(yīng)用,研究過memcached緩存數(shù)據(jù)庫的人都知道,memcached服務(wù)器端本身不提供分布式cache的一致性,而是由客戶端來提供,使用一致性哈希的好處在于,增減集群的緩存服務(wù)器時,只有少量的緩存會失效,回源量較小。

1.問題的背景

假設(shè)一個分布式任務(wù)調(diào)度系統(tǒng)(負(fù)載均衡、分布式緩存服務(wù)器的常見該場景),執(zhí)行任務(wù)的節(jié)點有n臺機器,現(xiàn)有m個job在這n臺機器上運行,這m個Job需要逐一映射到n個節(jié)點中一個,這時候可以選擇一種簡單的Hash算法來讓m個Job可以均勻分布到n個節(jié)點中,比如:

hash(Job) % n

替換成分布式Cache也一樣的有上述場景:有n個 cache 服務(wù)器(后面簡稱 cache ),那么如何將一個對象 object 映射到n個 cache 上呢
上面的公式看上去很完美,但考慮如下兩種情形:

  • n個節(jié)點中有一個宕掉了,這時候節(jié)點數(shù)量變更為n-1,此時的映射公式變成 hash(Job) % (n-1)
  • 由于Job數(shù)量增加,需要新增機器,此時的映射公式變成 hash(Job) % (n+1)

兩種情形可以看到,基本上所有的Job會被重新分配到跟節(jié)點變動前不同的節(jié)點上,意味著需要遷移幾乎所有正在運行的Job,想想這樣會給系統(tǒng)帶來多大的復(fù)雜性和性能損耗。緩存失效相當(dāng)于少了一個調(diào)節(jié)池,對于服務(wù)器而言也是一場災(zāi)難,洪水般的訪問都會直接沖向后臺服務(wù)器;再來考慮第三個問題,由于硬件能力越來越強,你可能想讓后面添加的節(jié)點多做點活,顯然上面的 hash 算法也做不到。有什么方法可以改變這個狀況呢,一致性哈希的解決方案就出來了,它用于盡可能地降低節(jié)點變動帶來的數(shù)據(jù)遷移開銷。

2.一致性Hash性質(zhì)

考慮到分布式系統(tǒng)每個節(jié)點都有可能失效,并且新的節(jié)點很可能動態(tài)的增加進(jìn)來,如何保證當(dāng)系統(tǒng)的節(jié)點數(shù)目發(fā)生變化時仍然能夠?qū)ν馓峁┝己玫姆?wù),這是值得考慮的,尤其實在設(shè)計分布式緩存系統(tǒng)時,如果某臺服務(wù)器失效,對于整個系統(tǒng)來說如果不采用合適的算法來保證一致性,那么緩存于系統(tǒng)中的所有數(shù)據(jù)都可能會失效(即由于系統(tǒng)節(jié)點數(shù)目發(fā)生動態(tài)變化時,客戶端在請求某一對象時需要重新計算其hash值,由于hash值已經(jīng)改變,所以很可能找不到保存該對象的服務(wù)器節(jié)點),因此一致性hash就顯得至關(guān)重要,良好的分布式cahce系統(tǒng)中的一致性hash算法應(yīng)該滿足以下幾個方面:

  • 平衡性(Balance)
    平衡性是指哈希的結(jié)果能夠盡可能分布到所有的緩沖中去,這樣可以使得所有的緩沖空間都得到利用。

  • 單調(diào)性(Monotonicity)
    該性質(zhì)是最需要考量的因素,單調(diào)性是指如果已經(jīng)有一些內(nèi)容通過哈希分派到了相應(yīng)的緩沖中,又有新的緩沖區(qū)加入到系統(tǒng)中,那么哈希的結(jié)果應(yīng)能夠保證原有已分配的內(nèi)容可以被映射到新的緩沖區(qū)中去,而不會被映射到舊的緩沖集合中的其他緩沖區(qū)(簡單理解:增加新節(jié)點,原有的部分內(nèi)容可以映射過來)。簡單的哈希算法往往不能滿足單調(diào)性的要求,哈希結(jié)果的變化意味著當(dāng)緩沖空間發(fā)生變化時,所有的映射關(guān)系需要在系統(tǒng)內(nèi)全部更新,因此會帶來極大計算和傳輸負(fù)荷。

  • 分散性(Spread)
    好的哈希算法應(yīng)能夠盡量避免不一致的情況發(fā)生,也就是盡量降低分散性。

  • 負(fù)載(Load)
    負(fù)載問題實際上是從另一個角度看待分散性問題。既然不同的終端可能將相同的內(nèi)容映射到不同的緩沖區(qū)中,那么對于一個特定的緩沖區(qū)而言,也可能被不同的用戶映射為不同的內(nèi)容。與分散性一樣,這種情況也是應(yīng)當(dāng)避免的,因此好的哈希算法應(yīng)能夠盡量降低緩沖的負(fù)荷。

  • 平滑性(Smoothness)
    平滑性是指緩存服務(wù)器的數(shù)目平滑改變和緩存對象的平滑改變是一致的。

參考: 一致性哈希算法原理

3.一致性哈希算法原理

一致性哈希將整個哈希值空間組織成一個虛擬的圓環(huán),如假設(shè)某哈希函數(shù)H的值空間為0 ~ 232-1(即哈希值是一個32位無符號整形),整個空間按順時針方向組織,0和232-1在零點中方向重合,整個哈??臻g環(huán)如下:

image

下一步將各個服務(wù)器使用Hash進(jìn)行一個哈希,具體可以選擇服務(wù)器的ip或主機名作為關(guān)鍵字進(jìn)行哈希,這樣每臺機器就能確定其在哈希環(huán)上的位置,這里假設(shè)將上文中四臺服務(wù)器使用ip地址哈希后在環(huán)空間的位置如下:

image

接下來使用如下算法定位數(shù)據(jù)訪問到相應(yīng)服務(wù)器:將數(shù)據(jù)key使用相同的函數(shù)Hash計算出哈希值,并確定此數(shù)據(jù)在環(huán)上的位置,從此位置沿環(huán)順時針“行走”,第一臺遇到的服務(wù)器就是其應(yīng)該定位到的服務(wù)器。例如有Object A、Object B、Object C、Object D四個數(shù)據(jù)對象,經(jīng)過哈希計算后,在環(huán)空間上的位置如下:

image

根據(jù)一致性哈希算法,數(shù)據(jù)A會被定為到Node A上,B被定為到Node B上,C被定為到Node C上,D被定為到Node D上。

下面分析一致性哈希算法的容錯性和可擴展性?,F(xiàn)假設(shè)Node C不幸宕機,可以看到此時對象A、B、D不會受到影響,只有C對象被重定位到Node D。一般的,在一致性哈希算法中,如果一臺服務(wù)器不可用,則受影響的數(shù)據(jù)僅僅是此服務(wù)器到其環(huán)空間中前一臺服務(wù)器(即沿著逆時針方向行走遇到的第一臺服務(wù)器)之間數(shù)據(jù),其它不會受到影響。
下面考慮另外一種情況,如果在系統(tǒng)中增加一臺服務(wù)器Node X,如下圖所示:

image

此時對象Object A、B、D不受影響,只有對象C需要重定位到新的Node X 。一般的,在一致性哈希算法中,如果增加一臺服務(wù)器,則受影響的數(shù)據(jù)僅僅是新服務(wù)器到其環(huán)空間中前一臺服務(wù)器(即沿著逆時針方向行走遇到的第一臺服務(wù)器)之間數(shù)據(jù),其它數(shù)據(jù)也不會受到影響。

綜上所述,一致性哈希算法對于節(jié)點的增減都只需重定位環(huán)空間中的一小部分?jǐn)?shù)據(jù),具有較好的容錯性和可擴展性。另外,一致性哈希算法在服務(wù)節(jié)點太少時,容易因為節(jié)點分部不均勻而造成數(shù)據(jù)傾斜問題。例如系統(tǒng)中只有兩臺服務(wù)器,其環(huán)分布如下:

image

此時必然造成大量數(shù)據(jù)集中到Node A上,而只有極少量會定位到Node B上。為了解決這種數(shù)據(jù)傾斜問題,一致性哈希算法引入了虛擬節(jié)點機制,即對每一個服務(wù)節(jié)點計算多個哈希,每個計算結(jié)果位置都放置一個此服務(wù)節(jié)點,稱為虛擬節(jié)點。具體做法可以在服務(wù)器ip或主機名的后面增加編號來實現(xiàn)。例如上面的情況,可以為每臺服務(wù)器計算三個虛擬節(jié)點,于是可以分別計算 “Node A#1”、“Node A#2”、“Node A#3”、“Node B#1”、“Node B#2”、“Node B#3”的哈希值,于是形成六個虛擬節(jié)點:

image

4.一致性Hash的java實現(xiàn)

1.確定哈希值空間
考慮通常的 hash 算法都是將 value 映射到一個 32 為的 key 值,也即是 0~232-1 次方的數(shù)值空間

2.計算節(jié)點哈希
將節(jié)點Node向這個值空間映射,取Node的Hash值,選取一個可以固定標(biāo)識一個Node的屬性值進(jìn)行Hashing,假設(shè)以字符串形式輸入,可以取Node標(biāo)識的md5值,然后截取其中32位作為映射值。md5取值如下:

    private byte[] md5(String value) {
        MessageDigest md5;
        try {
            md5 = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
        md5.reset();
        byte[] bytes;
        try {
            bytes = value.getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
        md5.update(bytes);
        return md5.digest();
    }

因為映射值只需要32位即可,所以可以利用以下方式計算最終值(number取0即可):

    private long hash(byte[] digest, int number) {
        return (((long) (digest[3 + number * 4] & 0xFF) << 24)
                | ((long) (digest[2 + number * 4] & 0xFF) << 16)
                | ((long) (digest[1 + number * 4] & 0xFF) << 8)
                | (digest[0 + number * 4] & 0xFF))
                & 0xFFFFFFFFL;
    }

3.有序緩存節(jié)點信息
把n個節(jié)點Node通過以上方式取得hash值,映射到環(huán)形值空間。算法中,將以有序Map的形式在內(nèi)存中緩存每個節(jié)點的Hash值對應(yīng)的物理節(jié)點信息。緩存于這個內(nèi)存變量中:

private final TreeMap<Long, String> virtualNodes ;

4.數(shù)據(jù)向值空間映射
數(shù)據(jù)Job取hash的方式跟節(jié)點Node的方式一模一樣,可以使用上述md5 => hash的方式同樣將所有Job取得Hash映射到這個環(huán)中。

5.數(shù)據(jù)和節(jié)點映射
當(dāng)節(jié)點和數(shù)據(jù)都被映射到這個環(huán)上后,可以設(shè)定一個規(guī)則把哪些數(shù)據(jù)hash值放在哪些節(jié)點Node Hash值上了,規(guī)則就是:沿著順時針方向,數(shù)據(jù)hash值向后找到第一個Node Hash值即認(rèn)為該數(shù)據(jù)hash值對應(yīng)的數(shù)據(jù)映射到該Node上。至此,這一個從數(shù)據(jù)到節(jié)點的映射關(guān)系就確定了。順時針找下一個Node Hash值算法如下:

   public String select(Trigger trigger) {
        String key = trigger.toString();
        byte[] digest = md5(key);
        String node = selectForKey(hash(digest, 0));
        return node;
    }

    //根據(jù)數(shù)據(jù)的哈希值計算出所歸屬的節(jié)點Node
    private String selectForKey(long hash) {
        String node;
        Long key = hash;
        if (!virtualNodes.containsKey(key)) {
            SortedMap<Long, String> tailMap = virtualNodes.tailMap(key);
            if (tailMap.isEmpty()) {
                key = virtualNodes.firstKey();
            } else {
                key = tailMap.firstKey();
            }
        }
        node = virtualNodes.get(key);
        return node;
    }

Trigger是對Job一次觸發(fā)任務(wù)的抽象,這里可忽略關(guān)注,重寫了toString方法返回一個標(biāo)記一個Job的唯一標(biāo)志,計算Hash值,從節(jié)點Hash值中按規(guī)則尋找。

6.算法優(yōu)化-虛擬節(jié)點
上述算法過程,會想到兩個問題,第一,數(shù)據(jù)對象會不會分布不均勻,特別是新增節(jié)點或者減少節(jié)點時;第二,前文提到的如果想讓部分節(jié)點多映射到一些數(shù)據(jù)對象,如何處理。虛擬節(jié)點這是解決這個問題。將一個物理節(jié)點虛擬出一定數(shù)量的虛擬節(jié)點,分散到這個值空間上,需要盡可能地隨機分散開。

假設(shè)有4個物理節(jié)點Node,環(huán)上的每個色塊代表一個虛擬節(jié)點涵蓋的hash值區(qū)域,每種顏色代表一個物理節(jié)點。當(dāng)物理節(jié)點較少時,虛擬節(jié)點數(shù)需要更高來確保更好的一致性表現(xiàn)。經(jīng)測試,在物理節(jié)點為個位數(shù)時,虛擬節(jié)點可設(shè)置為160個,此時可帶來較好的表現(xiàn)(后文會給出測試結(jié)果,160*n個總節(jié)點數(shù)情況下,如果發(fā)生一個節(jié)點變動,映射關(guān)系變化率基本為1/n,達(dá)到預(yù)期)。

image

具體做算法實現(xiàn)時,已知物理節(jié)點,虛擬節(jié)點數(shù)設(shè)置為160,可將這160*n的節(jié)點計算出Hash值,以Hash值為key,以物理節(jié)點標(biāo)識為value,以有序Map的形式在內(nèi)存中緩存,作為后續(xù)計算數(shù)據(jù)對象對應(yīng)的物理節(jié)點時的查詢數(shù)據(jù)。代碼如下,virtualNodes中緩存著所有虛擬節(jié)點Hash值對應(yīng)的物理節(jié)點信息。

   public ConsistentHash(List<String> nodes) {
        this.virtualNodes = new TreeMap<>();
        this.identityHashCode = identityHashCode(nodes);
        this.replicaNumber = 160;
        for (String node : nodes) {
            for (int i = 0; i < replicaNumber / 4; i++) {
                byte[] digest = md5(node.toString() + i);
                for (int h = 0; h < 4; h++) {
                    long m = hash(digest, h);
                    virtualNodes.put(m, node);
                }
            }
        }
    }

參考:深入一致性哈希(Consistent Hashing)算法原理

總結(jié):

  • 一致性哈希算法解決了負(fù)載均衡和分布式緩存中動態(tài)分配問題,使得節(jié)點的動態(tài)變化(服務(wù)器宕機、新增服務(wù)器節(jié)點)所造成的代價降到最低。
  • 計算機的任何問題都可以通過增加一個虛擬層來解決,計算機硬件、網(wǎng)絡(luò)和軟件都是如此,網(wǎng)絡(luò)的7層協(xié)議,每一層都可以看做是下一層的虛擬層,操作系統(tǒng)可以看做是計算機硬件的虛擬層,java虛擬機可以看做是操作系統(tǒng)的虛擬層。上述的解決節(jié)點負(fù)載不均衡的問題也采用了虛擬化的思路,將每臺物理緩存服務(wù)器虛擬為緩存服務(wù)器,將虛擬服務(wù)器的hash值放在hash環(huán)上,數(shù)據(jù)key首先去尋找虛擬服務(wù)器節(jié)點,再映射得到真正的物理服務(wù)器的信息,這個思想可以在解決計算機問題上多多借鑒。

粘貼拷貝整理,如有侵權(quán),請聯(lián)系我刪除

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容