一致性哈希算法是分布式系統中常用的算法,為什么要用這個算法?
比如:一個分布式存儲系統,要將數據存儲到具體的節點(服務器)上, 在服務器數量不發生改變的情況下,如果采用普通的hash再對服務器總數量取模的方法(如key%服務器總數量),如果期間有服務器宕機了或者需要增加服務器,問題就出來了。 同一個key經過hash之后,再與服務器總數量取模的結果跟之前的結果會不一樣,這就導致了之前保存數據的丟失。因此,引入了一致性Hash(Consistent Hashing)分布算法
把數據用hash函數(如md5,sha1),映射到一個圓環上,如上圖所示,數據在存儲時,先根據hash算法算出key的hash值,對應到這個環中的位置,如k1對應圖中所示的位置同,然后沿着順時針方向找到服務器節點B,然后把k1在存到B這個節點中。
如果B節點宕機了,則B上的數據就會落到C節點上,如下圖所示
這樣,只會影響C節點,對於其他節點A、D的數據不會造成影響。但是問題來了,這樣會造成C節點負載過重的情況,因為C節點承擔了B節點的數據,所以C節點容易宕機,這樣造成了分布不均勻。
為了解決這個問題,引入了“虛擬節點“的概念:即想象空上環上有很多”虛擬節點“,一個真實的服務器節點對應多個虛擬節點,數據存儲的時候沿着環的順時針方向找到虛擬節點,就找到了對應的真實服務器節點。如下圖
圖中的A1、A2、B1、B2、C1、C2、D1、D2都是虛擬節點,機器A負載存儲A1、A2的數據,機器B負載存儲B1、B2的數據,機器C負載存儲C1、C2的數據。由於這些虛擬節點數量很多,均勻分布,因此不會造成“雪崩”現象。
一致性哈希算法的PHP實現
下面給出一個接口
/**
* 一致性哈希實現接口
* Interface ConsistentHash
*/
interface ConsistentHash
{
//將字符串轉為hash值
public function cHash($str);
//添加一台服務器到服務器列表中
public function addServer($server);
//從服務器刪除一台服務器
public function removeServer($server);
//在當前的服務器列表中找到合適的服務器存放數據
public function lookup($key);
}
這個接口分別定義了4個方法,cHash(將字符串處理為hash值)、addServer(增加一台服務器)、removeServer(移除一台服務器)、lookup(找到一台服務器來存儲數據)
下面給出一個該接口的具體實現
/**
* 具體一致性哈希實現
* author chenqionghe
* Class MyConsistentHash
*/
class MyConsistentHash implements ConsistentHash
{
public $serverList = array(); //服務器列列表
public $virtualPos = array(); //虛擬節點的位置
public $virtualPosNum = 5; //每個節點對應5個虛節點
/**
* 將字符串轉換成32位無符號整數hash值
* @param $str
* @return int
*/
public function cHash($str)
{
$str = md5($str);
return sprintf('%u', crc32($str));
}
/**
* 在當前的服務器列表中找到合適的服務器存放數據
* @param $key 鍵名
* @return mixed 返回服務器IP地址
*/
public function lookup($key)
{
$point = $this->cHash($key);//落點的hash值
$finalServer = current($this->virtualPos);//先取圓環上最小的一個節點當成結果
foreach($this->virtualPos as $pos=>$server)
{
if($point <= $pos)
{
$finalServer = $server;
break;
}
}
reset($this->virtualPos);//重置圓環的指針為第一個
return $finalServer;
}
/**
* 添加一台服務器到服務器列表中
* @param $server 服務器IP地址
* @return bool
*/
public function addServer($server)
{
if(!isset($this->serverList[$server]))
{
for($i=0; $i<$this->virtualPosNum; $i++)
{
$pos = $this->cHash($server . '-' . $i);
$this->virtualPos[$pos] = $server;
$this->serverList[$server][] = $pos;
}
ksort($this->virtualPos,SORT_NUMERIC);
}
return TRUE;
}
/**
* 移除一台服務器(循環所有的虛節點,刪除值為該服務器地址的虛節點)
* @param $key
* @return bool
*/
public function removeServer($key)
{
if(isset($this->serverList[$key]))
{
//刪除對應虛節點
foreach($this->serverList[$key] as $pos)
{
unset($this->virtualPos[$pos]);
}
//刪除對應服務器
unset($this->serverList[$key]);
}
return TRUE;
}
}
然后, 我們來測試一下該算法
$hashServer = new MyConsistentHash();
$hashServer->addServer('192.168.1.1');
$hashServer->addServer('192.168.1.2');
$hashServer->addServer('192.168.1.3');
$hashServer->addServer('192.168.1.4');
$hashServer->addServer('192.168.1.5');
$hashServer->addServer('192.168.1.6');
$hashServer->addServer('192.168.1.7');
$hashServer->addServer('192.168.1.8');
$hashServer->addServer('192.168.1.9');
$hashServer->addServer('192.168.1.10');
echo "增加十台服務器192.168.1.1~192.168.1.10<br />";
echo "保存 key1 到 server :".$hashServer->lookup('key1') . '<br />';
echo "保存 key2 到 server :".$hashServer->lookup('key2') . '<br />';
echo "保存 key3 到 server :".$hashServer->lookup('key3') . '<br />';
echo "保存 key4 到 server :".$hashServer->lookup('key4') . '<br />';
echo "保存 key5 到 server :".$hashServer->lookup('key5') . '<br />';
echo "保存 key6 到 server :".$hashServer->lookup('key6') . '<br />';
echo "保存 key7 到 server :".$hashServer->lookup('key7') . '<br />';
echo "保存 key8 到 server :".$hashServer->lookup('key8') . '<br />';
echo "保存 key9 到 server :".$hashServer->lookup('key9') . '<br />';
echo "保存 key10 到 server :".$hashServer->lookup('key10') . '<br />';
echo '<hr />';
echo "移除一台服務器192.168.1.2<br />";
$hashServer->removeServer('192.168.1.2');
echo "保存 key1 到 server :".$hashServer->lookup('key1') . '<br />';
echo "保存 key2 到 server :".$hashServer->lookup('key2') . '<br />';
echo "保存 key3 到 server :".$hashServer->lookup('key3') . '<br />';
echo "保存 key4 到 server :".$hashServer->lookup('key4') . '<br />';
echo "保存 key5 到 server :".$hashServer->lookup('key5') . '<br />';
echo "保存 key6 到 server :".$hashServer->lookup('key6') . '<br />';
echo "保存 key7 到 server :".$hashServer->lookup('key7') . '<br />';
echo "保存 key8 到 server :".$hashServer->lookup('key8') . '<br />';
echo "保存 key9 到 server :".$hashServer->lookup('key9') . '<br />';
echo "保存 key10 到 server :".$hashServer->lookup('key10') . '<br />';
echo '<hr />';
echo "移除一台服務器192.168.1.6<br />";
$hashServer->removeServer('192.168.1.6');
echo "保存 key1 到 server :".$hashServer->lookup('key1') . '<br />';
echo "保存 key2 到 server :".$hashServer->lookup('key2') . '<br />';
echo "保存 key3 到 server :".$hashServer->lookup('key3') . '<br />';
echo "保存 key4 到 server :".$hashServer->lookup('key4') . '<br />';
echo "保存 key5 到 server :".$hashServer->lookup('key5') . '<br />';
echo "保存 key6 到 server :".$hashServer->lookup('key6') . '<br />';
echo "保存 key7 到 server :".$hashServer->lookup('key7') . '<br />';
echo "保存 key8 到 server :".$hashServer->lookup('key8') . '<br />';
echo "保存 key9 到 server :".$hashServer->lookup('key9') . '<br />';
echo "保存 key10 到 server :".$hashServer->lookup('key10') . '<br />';
echo '<hr />';
echo "移除一台服務器192.168.1.8<br />";
$hashServer->removeServer('192.168.1.8');
echo "保存 key1 到 server :".$hashServer->lookup('key1') . '<br />';
echo "保存 key2 到 server :".$hashServer->lookup('key2') . '<br />';
echo "保存 key3 到 server :".$hashServer->lookup('key3') . '<br />';
echo "保存 key4 到 server :".$hashServer->lookup('key4') . '<br />';
echo "保存 key5 到 server :".$hashServer->lookup('key5') . '<br />';
echo "保存 key6 到 server :".$hashServer->lookup('key6') . '<br />';
echo "保存 key7 到 server :".$hashServer->lookup('key7') . '<br />';
echo "保存 key8 到 server :".$hashServer->lookup('key8') . '<br />';
echo "保存 key9 到 server :".$hashServer->lookup('key9') . '<br />';
echo "保存 key10 到 server :".$hashServer->lookup('key10') . '<br />';
echo '<hr />';
echo "移除一台服務器192.168.1.2<br />";
$hashServer->removeServer('192.168.1.2');
echo "保存 key1 到 server :".$hashServer->lookup('key1') . '<br />';
echo "保存 key2 到 server :".$hashServer->lookup('key2') . '<br />';
echo "保存 key3 到 server :".$hashServer->lookup('key3') . '<br />';
echo "保存 key4 到 server :".$hashServer->lookup('key4') . '<br />';
echo "保存 key5 到 server :".$hashServer->lookup('key5') . '<br />';
echo "保存 key6 到 server :".$hashServer->lookup('key6') . '<br />';
echo "保存 key7 到 server :".$hashServer->lookup('key7') . '<br />';
echo "保存 key8 到 server :".$hashServer->lookup('key8') . '<br />';
echo "保存 key9 到 server :".$hashServer->lookup('key9') . '<br />';
echo "保存 key10 到 server :".$hashServer->lookup('key10') . '<br />';
echo '<hr />';
echo "增加一台服務器192.168.1.11<br />";
$hashServer->addServer('192.168.1.11');
echo "保存 key1 到 server :".$hashServer->lookup('key1') . '<br />';
echo "保存 key2 到 server :".$hashServer->lookup('key2') . '<br />';
echo "保存 key3 到 server :".$hashServer->lookup('key3') . '<br />';
echo "保存 key4 到 server :".$hashServer->lookup('key4') . '<br />';
echo "保存 key5 到 server :".$hashServer->lookup('key5') . '<br />';
echo "保存 key6 到 server :".$hashServer->lookup('key6') . '<br />';
echo "保存 key7 到 server :".$hashServer->lookup('key7') . '<br />';
echo "保存 key8 到 server :".$hashServer->lookup('key8') . '<br />';
echo "保存 key9 到 server :".$hashServer->lookup('key9') . '<br />';
echo "保存 key10 到 server :".$hashServer->lookup('key10') . '<br />';
echo '<hr />';
運行結果如下
增加十台服務器192.168.1.1~192.168.1.10 保存 key1 到 server :192.168.1.2 保存 key2 到 server :192.168.1.1 保存 key3 到 server :192.168.1.6 保存 key4 到 server :192.168.1.8 保存 key5 到 server :192.168.1.9 保存 key6 到 server :192.168.1.10 保存 key7 到 server :192.168.1.7 保存 key8 到 server :192.168.1.4 保存 key9 到 server :192.168.1.7 保存 key10 到 server :192.168.1.4 移除一台服務器192.168.1.2 保存 key1 到 server :192.168.1.7 保存 key2 到 server :192.168.1.1 保存 key3 到 server :192.168.1.6 保存 key4 到 server :192.168.1.8 保存 key5 到 server :192.168.1.9 保存 key6 到 server :192.168.1.10 保存 key7 到 server :192.168.1.7 保存 key8 到 server :192.168.1.4 保存 key9 到 server :192.168.1.7 保存 key10 到 server :192.168.1.4 移除一台服務器192.168.1.6 保存 key1 到 server :192.168.1.7 保存 key2 到 server :192.168.1.1 保存 key3 到 server :192.168.1.3 保存 key4 到 server :192.168.1.8 保存 key5 到 server :192.168.1.9 保存 key6 到 server :192.168.1.10 保存 key7 到 server :192.168.1.7 保存 key8 到 server :192.168.1.4 保存 key9 到 server :192.168.1.7 保存 key10 到 server :192.168.1.4 移除一台服務器192.168.1.8 保存 key1 到 server :192.168.1.7 保存 key2 到 server :192.168.1.1 保存 key3 到 server :192.168.1.3 保存 key4 到 server :192.168.1.10 保存 key5 到 server :192.168.1.9 保存 key6 到 server :192.168.1.10 保存 key7 到 server :192.168.1.7 保存 key8 到 server :192.168.1.4 保存 key9 到 server :192.168.1.7 保存 key10 到 server :192.168.1.4 移除一台服務器192.168.1.2 保存 key1 到 server :192.168.1.7 保存 key2 到 server :192.168.1.1 保存 key3 到 server :192.168.1.3 保存 key4 到 server :192.168.1.10 保存 key5 到 server :192.168.1.9 保存 key6 到 server :192.168.1.10 保存 key7 到 server :192.168.1.7 保存 key8 到 server :192.168.1.4 保存 key9 到 server :192.168.1.7 保存 key10 到 server :192.168.1.4 增加一台服務器192.168.1.11 保存 key1 到 server :192.168.1.7 保存 key2 到 server :192.168.1.1 保存 key3 到 server :192.168.1.11 保存 key4 到 server :192.168.1.10 保存 key5 到 server :192.168.1.9 保存 key6 到 server :192.168.1.10 保存 key7 到 server :192.168.1.7 保存 key8 到 server :192.168.1.4 保存 key9 到 server :192.168.1.7 保存 key10 到 server :192.168.1.4
可以,看到,使用一致性哈希后,無認是增加服務器還是減少服務器都最大程度的保證了數據的完整性、均勻性.
