全面瞭解一致性雜湊演算法及PHP程式碼實現

跡憶客發表於2021-11-30

在設計一個分散式系統的架構時,為了提高系統的負載能力,需要把不同的資料分發到不同的服務節點上。因此這裡就需要一種分發的機制,其實就是一種演算法,來實現這種功能。這裡我們就用到了Consistent Hashing演算法。

在正式介紹Consistent Hashing演算法之前我們先來看一個簡單的hash演算法,就是用取餘數的方式來選擇節點。具體的步驟如下:

一、根據叢集服務的節點數建立一個雜湊表
二、然後根據鍵名計算出鍵名的整數雜湊值,用該雜湊值對節點數取餘。
三、最後根據餘數在雜湊表中取出節點。

假設在一個叢集中有n個伺服器節點,對這些節點編號為0,1,2,…,n-1 。然後,將一條資料(key,value)儲存到伺服器中。這時我們該如何來選擇伺服器節點呢?根據上面的步驟我們需要對key計算hash值,然後再對n(節點個數)取餘數。最後得到的值就是我們所要的節點。用一個公式來表示:num = hash(key) % n。hash()是一個計算hash值的函式,這裡對hash()函式還是有一定的要求的,如果我們使用的hash()函式很優化的話,那計算出的num是均勻分佈在0,1,2,…,n-1之間的,從而使盡可能多的伺服器節點都能被使用。而不是所有的資料都集中在一個或者幾個伺服器節點上面。具體的hash()實現不是本章討論的重點。

這種單純的取餘數的方式雖然簡單,但是如果將其應用到實際生產系統中會出現很大的問題。假設我們有23個服務節點。那麼根據上面的方式,一個key對映到每個節點的概率都是1/23。假設增加了一個服務節點的話,之前的hash(key) % n 就會變成hash(key) % (n+1) 。也就是說對於key來說有23/24的概率會被重新分配到新的節點。相反只會有1/24的概率會被分配到原節點。同樣,當你減少一個節點的時候,有22/23的概率會被重新分配到新的節點上去。

鑑於這種情況,就需要有一種方式來避免或者減少在橫向擴充套件的時候命中率降低的情況的發生。這種方法就是我們將要介紹的Consistent Hashing演算法,我們稱其為一致性hash演算法。
為了瞭解Consistent Hashing演算法是如何工作的,我們假設單位區間 [ 0 , 1 ) 依順時針的方向均勻的分佈在圓上。

 

假使有n個服務節點,為每個服務節點編號為0, 1, 2, …, n-1。然後我們需要有一個hash()函式來對服務節點計算hash值。如果選用的hash()函式返回值的取值範圍為[ 0, R ),那麼使用公式 v = hash(n) / R。這樣得到的v會分佈在單位區間[ 0, 1 )內。所以,通過這個方式就可以使我們的服務節點分佈在圓上面。

當然,以單位區間[ 0, 1 ) 畫圓只是一種方式,還有很多其他的畫圓方式,比如說:以區間[ 0, 2^32-1 ) 為圓,然後使用hash()函式對服務節點計算hash()值。選用的hash()函式產生的值當然也必須在0 – (2^32-1) 範圍之內了。

這裡我們還是以[ 0, 1 )為例來介紹。

我們以3個服務節點為例來進行說明

 

這三個節點隨機的分佈在這個圓上面。現在假設我們有一條資料(key,value)需要儲存,接下來要做的就是將這條資料通過同樣的方法對映到圓上面。

 

然後從key所坐落在圓上的位置開始順時針查詢服務節點所在的位置,找到的第一個服務節點即是要儲存的節點。所以說這條資料將要儲存在服務節點1上。

同理,當有其它的(key,value)對需要儲存的時候,也是按照上面的方式進行服務節點的選擇。

 

現在我們來看該方法對於我們剛開始提到的橫向擴充套件的問題是否能夠很好的解決呢?

假設我們需要增加一個服務節點3

 

通過上圖,我們可以看出,只有key1會改變其儲存服務節點。對於大部分的資料來說依然會找到原先的節點。因此,對於n個服務節點的叢集來說,當有服務節點增加的時候一條資料只有1/(n+1)的概率會改變其儲存的服務節點。這個概率遠比取餘數法所得的概率要小的多。同樣,減少一個服務節點和增加服務節點的原理是相同的,其每條資料重新選擇服務節點的概率為1/(n-1)。同樣這個概率也是很小的。

下面就用一段php程式碼來簡單的實現這個過程

$nodes = array('192.168.5.201','192.168.5.102','192.168.5.111');
$keys = array('onmpw', 'jiyi', 'onmpw_key', 'jiyi_key', 'www','www_key','key1');
$buckets = array(); //節點的hash字典
$maps = array(); //儲存key和節點之間的對映關係
/**
 * 生成節點字典 —— 使節點分佈在單位區間[0,1)的圓上
 */
foreach( $nodes as $key) {
    $crc = crc32($key)/pow(2,32);            // CRC値
    $buckets[] = array('index'=>$crc,'node'=>$key);
}

/*
 * 根據索引進行排序
 */
sort($buckets);
/*
 * 對每個key進行hash計算,找到其在圓上的位置
 * 然後在該位置開始依順時針方向找到第一個服務節點
 */
foreach($keys as $key){
    $flag = false; //表示是否有找到服務節點
    $crc = crc32($key)/pow(2,32);//計算key的hash值
    for($i = 0; $i < count($buckets); $i++){

        if($buckets[$i]['index'] > $crc){
        /*
         * 因為已經對buckets進行了排序
         * 所以第一個index大於key的hash值的節點即是要找的節點
         */
         $maps[$key] = $buckets[$i]['node'];
              $flag = true;
              break;
         }
    }
    if(!$flag){
         //沒有找到,則使用buckets中的第一個服務節點
         $maps[$key] = $buckets[0]['node'];
    }
}
foreach($maps as $key=>$val){
    echo $key.'=>'.$val,"<br />";
}

 

這段程式碼執行的結果如下

onmpw=>192.168.5.102
jiyi=>192.168.5.201
onmpw_key=>192.168.5.201
jiyi_key=>192.168.5.102
www=>192.168.5.201
www_key=>192.168.5.201
key1=>192.168.5.111

 

然後我們新增一個服務節點,修改程式碼如下

$nodes = array('192.168.5.201','192.168.5.102','192.168.5.111','192.168.5.11');

 

其它程式碼不變,繼續執行結果如下

onmpw=>192.168.5.102
jiyi=>192.168.5.201
onmpw_key=>192.168.5.11
jiyi_key=>192.168.5.102
www=>192.168.5.201
www_key=>192.168.5.201
key1=>192.168.5.111

 

我們看到,只有onmpw_key重新選擇了服務節點。其它的都是原先的節點。

到這裡我們看到,較之於取餘數法命中的概率提高了相當多了。那這裡是不是就解決了我們前面遇到的問題了呢?

其實,還沒有。因為這些值的分佈畢竟不是那麼的均勻。在系統中有可能這些服務節點分佈非常的集中,這可能導致的情況就是所有的key都對映到其中的一個或者幾個節點上面,剩下的服務節點都沒有被用到。雖然這並不是什麼很嚴重的問題,那為什麼我們要浪費哪怕只是一臺伺服器呢。

 

我們看,這種情況就造成了資料集中在一個服務節點上面,造成了其它服務節點的浪費。那如何解決這個問題呢?人們就又想出了一種新的方式:就是為每個節點建立虛擬的節點。什麼意思呢?就是說對於節點j,為其建立m個複製品。這m個複製出來的節點都通過hash()函式得出不同的hash值,但是每個虛擬節點儲存的節點資訊都是節點j的。然後這些虛擬節點都會隨機的分佈在圓上面。舉例子來說,我們有兩個服務節點。並且為每個節點都複製出三個虛擬節點。這些節點(包括虛擬節點都隨機的分佈在圓上面)

 

這樣看起來服務節點在圓上分佈還是比較均勻的了。其實,總結起來就是在上面的那種方式上稍微做了一下改進——給每個節點複製一些虛擬節點。

因此,我們的程式碼也不需要做過多的修改。為了看程式碼比較直觀,我在這裡還是將整段程式碼羅列在這。

$nodes = array('192.168.5.201','192.168.5.102','192.168.5.111');
$keys = array('onmpw', 'jiyi', 'onmpw_key', 'jiyi_key', 'www','www_key','key1');
//新增的變數  修改的地方
$replicas = 160;  //每個節點的複製的個數
$buckets = array(); //節點的hash字典
$maps = array(); //儲存key和節點之間的對映關係
/**
 * 生成節點字典 —— 使節點分佈在單位區間[0,1)的圓上
 */
foreach( $nodes as $key) {
        //修改的地方
        for($i=1;$i<=$replicas;$i++){
        $crc = crc32($key.'.'.$i)/pow(2,32);            // CRC値
        $buckets[] = array('index'=>$crc,'node'=>$key);
        }
}
/*
 * 根據索引進行排序
 */
sort($buckets);
/*
 * 對每個key進行hash計算,找到其在圓上的位置
 * 然後在該位置開始依順時針方向找到第一個服務節點
 */
foreach($keys as $key){
    $flag = false; //表示是否有找到服務節點
    $crc = crc32($key)/pow(2,32);//計算key的hash值
    for($i = 0; $i < count($buckets); $i++){
        if($buckets[$i]['index'] > $crc){
             /*
              * 因為已經對buckets進行了排序
              * 所以第一個index大於key的hash值的節點即是要找的節點
              */
              $maps[$key] = $buckets[$i]['node'];
              $flag = true;
              break;

        }

     }
     if(!$flag){
        //沒有找到,則使用buckets中的第一個服務節點
        $maps[$key] = $buckets[0]['node'];
     }
}
foreach($maps as $key=>$val){
    echo $key.'=>'.$val,"<br />";
}

 

有改動的地方在程式碼裡已經標註出來了。可以看到,修改的地方還是比較少的。

至此,相信大家對Consistent Hashing應該有了一個比較清晰的認識。hash演算法的用處還是很廣泛的,比如在memcache叢集,nginx負載等方面都有用到。

我們在 帶你深入瞭解Memcached中的分散式思想 這篇文章中用實際的案例介紹了一致性hash演算法在Memcache中的應用。這裡我們所有的程式碼都是用PHP實現的,如果對PHP不熟悉的有興趣的可以參考以下教程,PHP教程

所以,瞭解hash演算法對於我們是有很大的幫助的。

上述演算法過程的表述有不清楚或者不合適的地方,歡迎大家不吝賜教。

相關文章