1. 程式人生 > 遊戲 >動作冒險遊戲《HunterX》4月29日於Steam發售 支援中文

動作冒險遊戲《HunterX》4月29日於Steam發售 支援中文

一、傳統雜湊取模演算法的侷限性

簡單地說,雜湊就是一個鍵值對儲存,在給定鍵的情況下,可以非常高效地找到所關聯的值。

要了解一致性雜湊,首先我們必須瞭解傳統的雜湊及其在大規模分散式系統中的侷限性。

當資料太大而無法儲存在一個節點或機器上時,系統中需要多個這樣的節點或機器來儲存它。比如,使用多個 Web 快取中介軟體的系統。那如何確定哪個 key 儲存在哪個節點上?針對該問題,最簡單的解決方案是使用雜湊取模來確定。

給定一個 key,先對 key 進行雜湊運算,將其除以系統中的節點數,然後將該 key值 放入該節點。同樣,在獲取 key 時,對 key 進行雜湊運算,再除以節點數,然後轉到該節點並獲取值。上述過程對應的雜湊演算法定義如下:

node_number = hash(key) % N # 其中 N 為節點數。

下圖描繪了多節點系統中的傳統的雜湊取模演算法,基於該演算法可以實現簡單的負載均衡。

傳統雜湊取模演算法的侷限性:

舉例,semlinker、kakuqo 和 test 3 個鍵進行雜湊運算並取餘

  1. 節點減少的場景

在分散式多節點系統中,出現故障很常見。任何節點都可能在沒有任何事先通知的情況下掛掉,針對這種情況我們期望系統只是出現效能降低,正常的功能不會受到影響。 對於原始示例,當節點出現故障時會發生什麼?原始示例中有的 3 個節點,假設其中 1 個節點出現故障,這時節點數發生了變化,節點個數從 3 減少為 2,此時表格的狀態發生了變化:

很明顯節點的減少會導致鍵與節點的對映關係發生變化,這個變化對於新的鍵來說並不會產生任何影響,但對於已有的鍵來說,將導致節點對映錯誤,以 “semlinker” 為例,變化前系統有 3 個節點,該鍵對應的節點編號為 1,當出現故障時,節點數減少為 2 個,此時該鍵對應的節點編號為 0。

  1. 節點增加的場景

在分散式多節點系統中,對於某些場景比如節日大促,就需要對服務節點進行擴容,以應對突發的流量。 對於原始示例,當增加節點會發生什麼?原始示例中有的 3 個節點,假設進行擴容臨時增加了 1 個節點,這時節點數發生了變化,節點個數從 3 增加為 4 個,此時表格的狀態發生了變化:

很明顯節點的增加也會導致鍵與節點的對映關係發生變化,這個變化對於新的鍵來說並不會產生任何影響,但對於已有的鍵來說,將導致節點對映錯誤,同樣以 “semlinker” 為例,變化前系統有 3 個節點,該鍵對應的節點編號為 1,當增加節點時,節點數增加為 4 個,此時該鍵對應的節點編號為 2。

當叢集中節點的數量發生變化時,之前的對映規則就可能發生變化。如果叢集中每個機器提供的服務沒有差別,這不會有什麼影響。但對於分散式快取這種的系統而言,對映規則失效就意味著之前快取的失效,若同一時刻出現大量的快取失效,則可能會出現 “快取雪崩”,這將會造成災難性的後果。

要解決此問題,我們必須在其餘節點上重新分配所有現有鍵,這可能是非常昂貴的操作,並且可能對正在執行的系統產生不利影響。當然除了重新分配所有現有鍵的方案之外,還有另一種更好的方案即使用一致性雜湊演算法。

二、一致性雜湊演算法

一致性雜湊演算法在 1997 年由麻省理工學院提出,是一種特殊的雜湊演算法,在移除或者新增一個伺服器時,能夠儘可能小地改變已存在的服務請求與處理請求伺服器之間的對映關係。一致性雜湊解決了簡單雜湊演算法在分散式雜湊表中存在的動態伸縮等問題 。

一致性雜湊演算法優點

  • 可擴充套件性。一致性雜湊演算法保證了增加或減少伺服器時,資料儲存的改變最少,相比傳統雜湊演算法大大節省了資料移動的開銷 。
  • 更好地適應資料的快速增長。採用一致性雜湊演算法分佈資料,當資料不斷增長時,部分虛擬節點中可能包含很多資料、造成資料在虛擬節點上分佈不均衡,此時可以將包含資料多的虛擬節點分裂,這種分裂僅僅是將原有的虛擬節點一分為二、不需要對全部的資料進行重新雜湊和劃分。虛擬節點分裂後,如果物理伺服器的負載仍然不均衡,只需在伺服器之間調整部分虛擬節點的儲存分佈。這樣可以隨資料的增長而動態的擴充套件物理伺服器的數量,且代價遠比傳統雜湊演算法重新分佈所有資料要小很多。

一致性雜湊演算法與雜湊演算法的關係

一致性雜湊演算法是在雜湊演算法基礎上提出的,在動態變化的分散式環境中,雜湊演算法應該滿足的幾個條件:平衡性、單調性和分散性。

  • 平衡性:是指 hash 的結果應該平均分配到各個節點,這樣從演算法上解決了負載均衡問題。
  • 單調性:是指在新增或者刪減節點時,不影響系統正常執行。
  • 分散性:是指資料應該分散地存放在分散式叢集中的各個節點(節點自己可以有備份),不必每個節點都儲存所有的資料。

三、一致性雜湊演算法原理

一致性雜湊演算法通過一個叫作一致性雜湊環的資料結構實現。這個環的起點是 0,終點是 2^32 - 1,並且起點與終點連線,故這個環的整數分佈範圍是 [0, 2^32-1],如下圖所示:

  1. 將物件放置到雜湊環

假設我們有 "semlinker"、"kakuqo"、"lolo"、"fer" 四個物件,分別簡寫為 o1、o2、o3 和 o4,然後使用雜湊函式計算這個物件的 hash 值,值的範圍是 [0, 2^32-1]:

圖中物件的對映關係如下:

hash(o1) = k1; hash(o2) = k2;
hash(o3) = k3; hash(o4) = k4;

  1. 將伺服器放置到雜湊環

接著使用同樣的雜湊函式,我們將伺服器也放置到雜湊環上,可以選擇伺服器的 IP 或主機名作為鍵進行雜湊,這樣每臺伺服器就能確定其在雜湊環上的位置。這裡假設我們有 3 臺快取伺服器,分別為 cs1、cs2 和 cs3:

圖中伺服器的對映關係如下:

hash(cs1) = t1; hash(cs2) = t2; hash(cs3) = t3; # Cache Server

  1. 為物件選擇伺服器

將物件和伺服器都放置到同一個雜湊環後,在雜湊環上順時針查詢距離這個物件的 hash 值最近的機器,即是這個物件所屬的機器。 以 o2 物件為例,順序針找到最近的機器是 cs2,故伺服器 cs2 會快取 o2 物件。而伺服器 cs1 則快取 o1,o3 物件,伺服器 cs3 則快取 o4 物件。

  1. 伺服器增加的情況

假設由於業務需要,我們需要增加一臺伺服器 cs4,經過同樣的 hash 運算,該伺服器最終落於 t1 和 t2 伺服器之間,具體如下圖所示:

對於上述的情況,只有 t1 和 t2 伺服器之間的物件需要重新分配。在以上示例中只有 o3 物件需要重新分配,即它被重新到 cs4 伺服器。在前面我們已經分析過,如果使用簡單的取模方法,當新新增伺服器時可能會導致大部分快取失效,而使用一致性雜湊演算法後,這種情況得到了較大的改善,因為只有少部分物件需要重新分配。

  1. 伺服器減少的情況

假設 cs3 伺服器出現故障導致服務下線,這時原本儲存於 cs3 伺服器的物件 o4,需要被重新分配至 cs2 伺服器,其它物件仍儲存在原有的機器上。

  1. 虛擬節點

到這裡一致性雜湊的基本原理已經介紹完了,但對於新增伺服器的情況還存在一些問題。新增的伺服器 cs4 只分擔了 cs1 伺服器的負載,伺服器 cs2 和 cs3 並沒有因為 cs4 伺服器的加入而減少負載壓力。如果 cs4 伺服器的效能與原有伺服器的效能一致甚至可能更高,那麼這種結果並不是我們所期望的。

針對這個問題,我們可以通過引入虛擬節點來解決負載不均衡的問題。即將每臺物理伺服器虛擬為一組虛擬伺服器,將虛擬伺服器放置到雜湊環上,如果要確定物件的伺服器,需先確定物件的虛擬伺服器,再由虛擬伺服器確定物理伺服器。

圖中 o1 和 o2 表示物件,v1 ~ v6 表示虛擬伺服器,s1 ~ s3 表示物理伺服器。

四、一致性雜湊演算法實現

這裡我們只介紹不帶虛擬節點的一致性雜湊演算法實現:

import java.util.SortedMap;
import java.util.TreeMap;

public class ConsistentHashingWithoutVirtualNode {
    //待新增入Hash環的伺服器列表
    private static String[] servers = {"192.168.0.1:8888", "192.168.0.2:8888", 
      "192.168.0.3:8888"};

    //key表示伺服器的hash值,value表示伺服器
    private static SortedMap<Integer, String> sortedMap = new TreeMap<Integer, String>();

    //程式初始化,將所有的伺服器放入sortedMap中
    static {
        for (int i = 0; i < servers.length; i++) {
            int hash = getHash(servers[i]);
            System.out.println("[" + servers[i] + "]加入集合中, 其Hash值為" + hash);
            sortedMap.put(hash, servers[i]);
        }
    }

    //得到應當路由到的結點
    private static String getServer(String key) {
        //得到該key的hash值
        int hash = getHash(key);
        //得到大於該Hash值的所有Map
        SortedMap<Integer, String> subMap = sortedMap.tailMap(hash);
        if (subMap.isEmpty()) {
            //如果沒有比該key的hash值大的,則從第一個node開始
            Integer i = sortedMap.firstKey();
            //返回對應的伺服器
            return sortedMap.get(i);
        } else {
            //第一個Key就是順時針過去離node最近的那個結點
            Integer i = subMap.firstKey();
            //返回對應的伺服器
            return subMap.get(i);
        }
    }

    //使用FNV1_32_HASH演算法計算伺服器的Hash值
    private static int getHash(String str) {
        final int p = 16777619;
        int hash = (int) 2166136261L;
        for (int i = 0; i < str.length(); i++)
            hash = (hash ^ str.charAt(i)) * p;
        hash += hash << 13;
        hash ^= hash >> 7;
        hash += hash << 3;
        hash ^= hash >> 17;
        hash += hash << 5;

        // 如果算出來的值為負數則取其絕對值
        if (hash < 0)
            hash = Math.abs(hash);
        return hash;
    }

    public static void main(String[] args) {
        String[] keys = {"semlinker", "kakuqo", "fer"};
        for (int i = 0; i < keys.length; i++)
            System.out.println("[" + keys[i] + "]的hash值為" + getHash(keys[i])
                    + ", 被路由到結點[" + getServer(keys[i]) + "]");
    }

}

以上程式碼成功執行後,在控制檯會輸出以下結果:

[192.168.0.1:8888]加入集合中, 其Hash值為1326271016
[192.168.0.2:8888]加入集合中, 其Hash值為1132535844
[192.168.0.3:8888]加入集合中, 其Hash值為115798597

[semlinker]的hash值為1549041406, 被路由到結點[192.168.0.3:8888]
[kakuqo]的hash值為463104755, 被路由到結點[192.168.0.2:8888]
[fer]的hash值為1677150790, 被路由到結點[192.168.0.3:8888]

上面我們只介紹了不帶虛擬節點的一致性雜湊演算法實現,如果有的小夥伴對帶虛擬節點的一致性雜湊演算法感興趣,可以參考 一致性Hash(Consistent Hashing)原理剖析及Java實現 這篇文章。

五、總結

本文通過示例介紹了傳統的雜湊取模演算法在分散式系統中的侷限性,進而在針對該問題的解決方案中引出了一致性雜湊演算法。

一致性雜湊演算法在 1997 年由麻省理工學院提出,是一種特殊的雜湊演算法,在移除或者新增一個伺服器時,能夠儘可能小地改變已存在的服務請求與處理請求伺服器之間的對映關係。

在介紹完一致性雜湊演算法的作用和優點等相關知識後,我們以圖解的形式生動介紹了一致性雜湊演算法的原理,最後給出了不帶虛擬節點的一致性雜湊演算法的 Java 實現。