一致性hash介紹


像Memcache以及其它一些內存K/V數據庫一樣,Redis本身不提供分布式支持,所以在部署多台Redis服務器時,就需要解決如何把數據分散到各個服務器的問題,並且在服務器數量變化時,能做到最大程度的不令數據重新分布。

通常使用的分布式方法是根據所要存儲數據的鍵的hash值與服務器數量N,按 hash % N 取模的算法來將數據分布到各個服務器。該算法的優點是足夠簡單,而且數據分布均勻。但是一旦服務器數量N發生變化的時候,緩存命中率會瞬間跌入谷底,因為 絕大多數的數據需要重新分布。而且對於大型網站來說,此時會有巨大的壓力涌向后端服務,可能會導致性能故障和服務故障,甚至宕機。

本文介紹什么是一致性哈希,為什么要使用以及如何使用一致性哈希算法實現Redis分布式部署,並且引入了虛擬節點以提高數據均勻分布的平衡性。最后還提供了一個故障轉移策略,保證在部分服務器故障的時候能迅速恢復命中率,提高Redis服務的可用性和穩定性。

一致性哈希

由於hash算法結果一般為unsigned int型,因此對於hash函數的結果應該均勻分布在[0,2^32-1]區間,如果我們把一個圓環用2^32 個點來進行均勻切割,首先按照hash(key)函數算出服務器(節點)的哈希值, 並將其分布到0~2^32的圓環上。
用同樣的hash(key)函數求出需要存儲數據的鍵的哈希值,並映射到圓環上。然后從數據映射到的位置開始順時針查找,將數據保存到找到的第一個服務器(節點)上。如圖所示:
104125_gk1O_182025

key1、key2、key3和server1、server2通過hash都能在這個圓環上找到自己的位置,並且通過順時針的方式來將key定位 到 server。按上圖來說,key1和key2存儲到server1,而key3存儲到server2。如果新增一台server,hash后在key1 和key2之間,則只會影響key1(key1將會存儲在新增的server上),其它則不變。

虛擬節點

在上圖中,很容易看出一個問題,沿順時針方向看,server2到server1之間的區間跨度大,而server1到server2的區間跨度 小,這就會導致一個問題:數據分布不均勻。大部分數據都分配到server1了,只有小部分數據分布在server2。在服務器數據很少的時候,數據不均 勻會表現的非常明顯。

解決這個問題的方法是使用虛擬節點,一個真實服務器對應多個虛擬節點,所有虛擬節點按hash值分布在一致性哈希圓環上。具體實現方法可以這樣做,為真實服務器設置副本數量,然后根據各真實服務器的IP和端口號再加上一個遞增的索引數計算hash值。

class RedisCache { public $servers = array(); //真實服務器 private $_servers = array(); //虛擬節點 const SERVER_REPLICAS = 10000; //服務器副本數量,提高一致性哈希算法的數據分布均勻程度 public function __construct( $servers ){ $this->servers = $servers; //Redis虛擬節點哈希表 foreach ($this->servers as $k => $server) { for ($i = 0; $i < self::SERVER_REPLICAS; $i++) { $hash = crc32($server['host'] . '#' .$server['port'] . '#'. $i); $this->_servers[$hash] = $k; } } ksort($this->_servers); // something else... } }

副本數量可以按真實服務器的數量調節,真實服務器 多則副本數量可以設置小一點,真實服務器少則副本數量需要設置多一點。在虛擬節點數量很大的時候,出於性能考慮所以不能使用循環的方法查找key對應的虛 擬節點,可以使用二分法快速查找。一個優化的算法是不論副本數量設置為10,100,還是10000的時候,對查找所需要的時間基本是沒有影響的。

故障轉移

使用一次性哈希實現Redis分布式部署了,還需要考慮系統的可用性和穩定性。需要做到,在某一台或者多台server故障的時候,程序能夠自動檢測到故障,並將數據重新定位到其它server。

我們可以考慮,根據key查找到的虛擬節點所對應的真實服務器故障的時候,我們在一次性哈希圓環上沿順時針方向順移一步,找到下一點虛擬節點對應的 真實服務器,將所要存儲的數據存放上去。但也很有可能下一個虛擬節點所對應的真實服務器與前一個虛擬節點相同,還是那台故障的服務器,而每次嘗試連接故障 的redis服務是一個很大的性能開銷。所以在第一次檢測到故障服務器的時候就需要記錄下來,然后在順移到下一個虛擬節點的時候先判斷是不是之前那一台故 障的服務器,如果是那就不要再嘗試進行連接,直接查找下一個虛擬節點,直到找到可用的服務器將數據存儲上去。

完整示例代碼

class RedisCache { public $servers = array(); //真實服務器 private $_servers = array(); //虛擬節點 private $_serverKeys = array(); private $_badServers = array(); // 故障服務器列表 private $_count = 0; const SERVER_REPLICAS = 10000; //服務器副本數量,提高一致性哈希算法的數據分布均勻程度 public function __construct( $servers ){ $this->servers = $servers; $this->_count = count($this-> servers); //Redis虛擬節點哈希表 foreach ($this ->servers as $k => $server) { for ($i = 0; $i < self::SERVER_REPLICAS; $i++) { $hash = crc32($server[ 'host'] . '#' .$server['port'] . '#'. $i); $this->_servers [$hash] = $k; } } ksort( $this->_servers ); $this->_serverKeys = array_keys($this-> _servers); } /** * 使用一致性哈希分派服務器,附加故障檢測及轉移功能 */ private function getRedis($key){ $hash = crc32($key); $slen = $this->_count * self:: SERVER_REPLICAS; // 快速定位虛擬節點 $sid = $hash > $this->_serverKeys [$slen-1] ? 0 : $this->quickSearch($this->_serverKeys, $hash, 0, $slen); $conn = false; $i = 0; do { $n = $this->_servers [$this->_serverKeys[$sid]]; !in_array($n, $this->_badServers ) && $conn = $this->getRedisConnect($n); $sid = ($sid + 1) % $slen; } while (!$conn && $i++ < $slen); return $conn ? $conn : new Redis(); } /** * 二分法快速查找 */ private function quickSearch($stack, $find, $start, $length) { if ($length == 1) { return $start; } else if ($length == 2) { return $find <= $stack[$start] ? $start : ($start +1); } $mid = intval($length / 2); if ($find <= $stack[$start + $mid - 1]) { return $this->quickSearch($stack, $find, $start, $mid); } else { return $this->quickSearch($stack, $find, $start+$mid, $length-$mid); } } private function getRedisConnect($n=0){ static $REDIS = array(); if (!$REDIS[$n]){ $REDIS[$n] = new Redis(); try{ $ret = $REDIS[$n]->pconnect( $this->servers [$n]['host'], $this->servers [$n]['port']); if (!$ret) { unset($REDIS[$n]); $this->_badServers [] = $n; return false; } } catch(Exception $e){ unset($REDIS[$n]); $this->_badServers [] = $n; return false; } } return $REDIS[$n]; } public function getValue($key){ try{ $getValue = $this->getRedis($key)->get($key); } catch(Exception $e){ $getValue = null; } return $getValue; } public function setValue($key,$value,$expire){ if($expire == 0){ try{ $ret = $this->getRedis($key)->set($key, $value); } catch(Exception $e){ $ret = false; } } else{ try{ $ret = $this->getRedis($key)->setex($key, $expire, $value); } catch(Exception $e){ $ret = false; } } return $ret; } public function deleteValue($key){ return $this->getRedis($key)->delete($key); } public function flushValues(){ //TODO return true; } } // Usage: $redis_servers = array( array( 'host' => '10.0.0.1', 'port' => 6379, ), array( 'host' => '10.0.0.2', 'port' => 6379, ), array( 'host' => '10.0.0.3', 'port' => 6379, ), array( 'host' => '10.0.0.3', 'port' => 6928, ), ); $redisCache = new RedisCache($redis_servers); $testKey = 'test_key'; $testValue = 'test_value_object'; $redisCache->setValue($testKey, $testValue, 3600); $value = $redisCache->getValue($testKey);


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM