author: selfimpr
blog: http://blog.csdn.net/lgg201
mail: lgg860911@yahoo.com.cn
事情的起源, 是同事使用下面的代码, 得到了一个诡异的结果, 而且是稳定的产生我们不期望的结果.
<?php
$mem = new Memcached;
$mem->addServers(array(array('10.8.8.32',11300,100),array('10.8.8.32',11301,0)));
$mem->setOption(Memcached::OPT_DISTRIBUTION, Memcached::DISTRIBUTION_CONSISTENT);
$mem->setOption(Memcached::OPT_HASH, Memcached::HASH_CRC);
for ($i=0;$i<10;$i++){
$key = "item_$i";
$arr = $mem->getServerByKey($key);
echo ($key.":\t".$arr['port']."\n");
}
print_r($mem->getServerList());
代码很简单, 就是创建了一个php的Memcached客户端, 增加了两台服务器, 设置了分布式算法为一致性哈希, Hash算法为CRC.
运行了很多次这个测试用例, 产生的输入都如下:
item_1: 11301
item_2: 11301
item_3: 11301
item_4: 11301
item_5: 11301
item_6: 11301
item_7: 11301
item_8: 11301
item_9: 11301
Array
(
[0] => Array
(
[host] => 10.8.8.32
[port] => 11300
[weight] => 100
)
[1] => Array
(
[host] => 10.8.8.32
[port] => 11301
[weight] => 0
)
)
从上面的输出中我们可以看出, 两台服务器都是OK的, 但是, 只能所有的key都被分布到了一台服务器上.
后来, 我尝试对测试用例做了以下修改:
1. 将Hash算法换做其他支持的算法
2. 将分布式算法换成普通的算法
我做的以上尝试, 输出的结果都是我们期望的, key被分布到不同的服务器上.
然后就是痛苦的问题跟踪, 最终发现问题出在php的memcached客户端对libmemcached的实现上
在libmemcached中, 用来代表一组服务器(针对同一个客户端服务)的结构(libmemcached/memcached.h中定义)是: struct memcached_st {};下面摘取其中的部分定义:
struct memcached_st {
uint8_t purging;
bool is_allocated;
uint8_t distribution;
uint8_t hash;
...
memcached_hash hash_continuum;
...
};
请记住hash和hash_continuum这两个字段.
然后我们看一个函数:
libmemcached/memcached_hash.c中的memcached_generate_hash函数, 进入这个函数的流程如下:
"php-memcached扩展php_memcached.c中getServeredByKey函数" 调用 "libmemcached的libmemcached/memcache_server.c中的memcached_server_by_key函数", 在其中又调用了 "libmemcached/memcached_hash.c中的memcached_generate_hash函数"
在这个函数中做了3件比较重要的事:
1. 生成要寻找的key的hash值
2. 如果需要, 更新服务器的一致性hash点集
3. 将key分布到服务器上
我们分别来看这3件事:
1. 生成key的hash值:
继续跟踪代码, 我们发现在generate_hash函数中有如下一句代码:
hash= memcached_generate_hash_value(key, key_length, ptr->hash);
查看memcached_generate_hash_value函数源代, 我们得知该函数是使用第3个参数指定的hash算法, 产生key的hash值, 这里使用的是ptr->hash
注: ptr就是前面提到的memcached_st结构
2. 更新服务器的一致性hash点集
这里, 我们需要说的是, 哪怕不需要, 在我们测试代码中的addServer调用时, 也会执行这个函数, 所以, 我们需要关注其中所做的事情
我们跟踪到update_continuum函数中, 分析源代码, 总结这个函数所做的事情, 用php代码描述如下:
<?php
$servers = array(
array('10.8.8.32', 11301),
array('10.8.8.32', 11300),
);
$points = array();
$index = 0;
foreach ( $servers as $server ) {
$i = 0;
while ( $i ++ < 100 ) { //libmemcached中100是两个常量求得的值
$points[] = array(
'index' => $index,
'value' => hash_value,
);
}
$index ++;
}
//这里再对$servers按照元素的'value'排序
也就是: 以$host:$port-$i作为key产生100个hash值, 所有服务器产生的这些hash值再排序
这里在libmemcached的update_continuum中, 我们需要找到下面这句代码:
value= memcached_generate_hash_value(sort_host, sort_host_length, ptr->hash_continuum);
也就是求每个点的hash值, 可以看到, 这里用了ptr中的hash_continuum字段给定的hash算法计算.
3. 将key分布到服务器上
这里的分布过程, 其实就是对上面产生的点集进行一个二分查找, 找到离key的hash值最近的点, 以其对应服务器作为key的服务器, 用php代码描述如下:
<?php
$points = array(); //之前取到的服务器产生的点集
$hash = 1; //要查找的key的hash值
$begin = $left = 0;
$end = $right = floor(count($points) / 2);
while ( $left < $right ) {
$middle = $left + floor(($left + $right) / 2);
if ( $points[$middle]['value'] < $hash ) $left = $middle + 1;
else $right = $middle;
}
//数组越界检查
if ( $right = $end ) $right = $begin;
//这里就得到了key分布到的服务器是所有服务器中的第$index个
$index = $servers[$right]['index'];
主要的过程分析完了, 对造成这个问题的关键点用红字标识了出来, 我们可以看到, 对key和对服务器求hash值的算法在memcached_st结构中是由不同的字段指定的.
那么, 问题就明了了, 我们取看看php-memcached中的setOption方法的实现, 它只是修改了ptr->hash字段.
因此, 测试用例的运行情况是:
对key使用crc算法求hash
对服务器点集使用默认算法求hash值
经过对两种算法比较, 默认的算法产生的hash数值都比较大, 而crc产生的hash值最大就是几万的样子(具体上限没有计算)
所以, 原因就找到了, 服务器点集的hash值都大于key产生的hash值, 所以查找时永远都落在点集的第一个点上.
至此, 问题的原因已经查明, 解决方法也就有了: 修改php的memcached扩展, 在setOption中增加修改ptr->hash_continuum字段的操作, 然后测试用例做响应修改即可.
下一篇文章将展示的是一个从libmemcached源代码提取出来的简化版一致性hash算法, 简单明了, 可以很容易说明libmemcached的一致性hash算法实现