在我们做负载均衡,分布式部署,数据分表时,需要用到一些分布式路由算法将请求路由到真实的节点上。最常用的有哈希取模,一致性哈希算法等。这里对他们的做一些介绍及提供PHP实现。
场景举例:分布式缓存服务器路由
1、哈希取模算法
对缓存key进行hash得到结果取余数 (hash() mod N):对机器编号从0到N-1,按照自定义的hash()算法,对每个请求的hash()值按N取模,得到余数i,然后将请求分发到编号为i的机器。
优点:实现简单
缺点:新增节点、删除节点引起的hash值颠簸,所有节点缓存需要重新计算迁移
function hashmod($key,$mod)
{
$code =crc32($key);
return abs($code) % $mod;
}
测试代码:
for($i = 1;$i<100;$i++){
$key = 'cachekey_'.$i;
$node = hashmod($key,10);
$arr[$node][] = $key;
}
foreach($arr as $key=>$val){
echo '节点'.$key.'数据量:'.count($val).PHP_EOL;
}
结果:
2、一致性哈希算法(Consistent Hashing)
由于hash算法结果一般为unsigned int型,因此对于hash函数的结果应该均匀分布在[0,-1]间,如果我们把一个圆环用个点来进行均匀切割,首先按照hash(key)函数算出服务器(节点)的哈希值, 并将其分布到0~-1的圆上。
用同样的hash(key)函数求出需要存储数据的键的哈希值,并映射到圆上。然后从数据映射到的位置开始顺时针查找,将数据保存到找到的第一个服务器(节点)上。
Consistent Hashing原理示意图
新增一个节点的时候,只有在圆环上新增节点逆时针方向的第一个节点的数据会受到影响。删除一个节点的时候,只有在圆环上原来删除节点顺时针方向的第一个节点的数据会受到影响,因此通过Consistent Hashing很好地解决了负载均衡中由于新增节点、删除节点引起的hash值颠簸问题。
Consistent Hashing添加服务器示意图
虚拟节点(virtual nodes):之所以要引进虚拟节点是因为在服务器(节点)数较少的情况下(例如只有3台服务器),通过hash(key)算出节点的哈希值在圆环上并不是均匀分布的(稀疏的),仍然会出现各节点负载不均衡的问题。虚拟节点可以认为是实际节点的复制品(replicas),本质上与实际节点实际上是一样的(key并不相同)。引入虚拟节点后,通过将每个实际的服务器(节点)数按照一定的比例(例如200倍)扩大后并计算其hash(key)值以均匀分布到圆环上。在进行负载均衡时候,落到虚拟节点的哈希值实际就落到了实际的节点上。由于所有的实际节点是按照相同的比例复制成虚拟节点的,因此解决了节点数较少的情况下哈希值在圆环上均匀分布的问题。
虚拟节点对Consistent Hashing结果的影响
从上图可以看出,在节点数为10个的情况下,每个实际节点的虚拟节点数为实际节点的100-200倍的时候,结果还是很均衡的。
class ConsistentHashing
{
// 圆环
// hash -> 节点
private $_ring = array();
// 所有节点
// 节点 -> hash
public $nodes = array();
// 每个节点的虚拟节点
public $virtual = 64;
/**
* 构造
* @param array $nodes 初始化的节点列表
*/
public function __construct($nodes = array())
{
if (!empty($nodes)) {
foreach ($nodes as $value) {
$this->addNode($value);
}
}
}
/**
* 获取圆环内容
* @return array $this->_ring
*/
public function getRing()
{
return $this->_ring;
}
/**
* time33 函数
* @param string $str
* @return 32位正整数
* @author 大神们
*/
public function time33($str)
{
$hash = 0;
$s = md5($str);
$len = 32;
for ($i = 0; $i < $len; $i++) {
$hash = ($hash * 33 + ord($s{$i})) & 0x7FFFFFFF;
}
return $hash;
}
/**
* 增加节点
* @param string $node 节点名称
* @return object $this
*/
public function addNode($node)
{
if (in_array($node, array_keys($this->nodes))) {
return;
}
for ($i = 1; $i <= $this->virtual; $i++) {
$key = $this->time33($node . '-' . $i);
$this->_ring[$key] = $node;
$this->nodes[$node][] = $key;
}
ksort($this->_ring, SORT_NUMERIC);
return $this;
}
/**
* 删除节点
* @param string $node 节点名称
*/
public function removeNode($node)
{
//删除虚拟节点
foreach ($this->nodes[$node] as $k => $v) {
unset($this->_ring[$v]);
}
//删除节点
unset($this->nodes[$node]);
return $this;
}
/**
* 获取字符串的HASH在圆环上面映射到的节点
* @param string $key
* @return string $node
*/
public function getNode($key)
{
$node = current($this->_ring);
$hash = $this->time33($key);
foreach ($this->_ring as $key => $value) {
if ($hash <= $key) {
$node = $value;
break;
}
}
return $node;
}
}
用以下代码测试:
$ch = new ConsistentHashing(['node1','node2','node3','node4','node5','node6','node7','node8','node9','node10']);
echo '创建节点:node1 - node10'.PHP_EOL;
for($i=1;$i<=10;$i++){
echo 'cachekey'.$i.' 分布节点:'.$ch->getNode('cachekey'.$i).PHP_EOL;
}
echo '_____________'.PHP_EOL;
$ch->addNode('node11');
echo '增加节点:node11'.PHP_EOL;
for($i=1;$i<=10;$i++){
echo 'cachekey'.$i.' 分布节点:'.$ch->getNode('cachekey'.$i).PHP_EOL;
}
echo '_____________'.PHP_EOL;
$ch->removeNode('node8');
echo '删除节点:node8'.PHP_EOL;
for($i=1;$i<=10;$i++){
echo 'cachekey'.$i.' 分布节点:'.$ch->getNode('cachekey'.$i).PHP_EOL;
}
结果如下:
————————————————
版权声明:本文为CSDN博主「青季」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/skypeng57/article/details/81222300