libmemcached一致性hash算法详解(1)----php-memcached客户端一致性哈希与crc算法共用产生的bug分析
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字段的操作, 然后测试用例做响应修改即可.
-------
author: selfimpr
blog: http://blog.csdn.net/lgg201
mail: lgg860911@yahoo.com.cn
上一篇文档见
libmemcached一致性hash算法详解(1)----php-memcached客户端一致性哈希与crc算法共用产生的bug分析
这里就不废话了, 直接上代码:
- #include <stdio.h>
- #include <stdlib.h>
- #include <stdint.h>
- #include <string.h>
- /*---------------------------以下部分摘自libmemcached/crc.c--------------------------*/
- static const uint32_t crc32tab[256] = {
- 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba,
- 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
- 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
- 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
- 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de,
- 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
- 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec,
- 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,
- 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
- 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
- 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940,
- 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
- 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116,
- 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
- 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
- 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,
- 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a,
- 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
- 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818,
- 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
- 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
- 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,
- 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c,
- 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
- 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2,
- 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,
- 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
- 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
- 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086,
- 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
- 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4,
- 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,
- 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
- 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683,
- 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
- 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
- 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe,
- 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,
- 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
- 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
- 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252,
- 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
- 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60,
- 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,
- 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
- 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,
- 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04,
- 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
- 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a,
- 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
- 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
- 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21,
- 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e,
- 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
- 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c,
- 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
- 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
- 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,
- 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0,
- 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
- 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6,
- 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,
- 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
- 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d,
- };
- uint32_t hash_crc32(const char *key, size_t key_length)
- {
- uint32_t x;
- uint32_t crc= UINT32_MAX;
- for (x= 0; x < key_length; x++)
- crc= (crc >> 8) ^ crc32tab[(crc ^ (key[x])) & 0xff];
- return ~crc;
- }
- /*---------------------------以上部分摘自libmemcached/crc.c--------------------------*/
- //libmemcached中使用crc求hash值的方法
- uint32_t memcached_hash_crc32(const char *key, size_t key_len) {
- return ((hash_crc32(key, key_len) >> 16) & 0x7fff);
- }
- /**
- * 模拟libmemcached的一致性hash算法中的点
- * libmemcached中, 假设有A,B两台服务器, 则其一致性算法为:
- * 1. 假设每台服务器100个点
- * 2. 每个点是一个类似下面的server_t的结构, 一个字段存放server的索引, 一个字段存放值
- * 3. 每个服务器构造自己对应的100个点时, 对host:port-i求hash值, 其中host为服务器地址, port为端口, i为0-99的数
- * 4. 将两个服务器总共产生的200个点放入一个数组, 按照点的value(即上面求得的hash值)排序
- * 下面是拿到一个key进行分布时的策略
- * 1. 对key求一个hash值
- * 2. 从上面得到的服务器的200个点的数组中, 二分查找离这个key产生的hash值最近的点
- * 3. 拿到这个点对应的服务器
- */
- typedef struct _server {
- char host[128];
- uint32_t value;
- } server_t;
- //libmemcached中点的比较函数
- int server_cmp(const void *s1, const void *s2) {
- server_t *cs1 = (server_t *)s1;
- server_t *cs2 = (server_t *)s2;
- if ( cs1->value == cs2->value ) return 0;
- else if ( cs1->value > cs2->value ) return 1;
- else return -1;
- }
- //打印从servers开始的n个点
- void print_servers(const server_t *servers, int n) {
- int i = 0;
- while ( i < n) {
- printf("server: %s, value: %d\n", (servers + i)->host, (servers + i)->value);
- i ++;
- }
- }
- //构造示例性的2台服务器产生的点, libmemcached中的算法简化了就是如此
- server_t *construct_servers() {
- server_t *servers = malloc(sizeof(server_t) * 200);
- int i = 0, j;
- int server_length = 0;
- char server[128];
- while ( i < 2 ) {
- for ( j = 0; j < 100; j ++ ) {
- server_length = sprintf(server, "127.0.0.1:%d-%d", i ? 11211 : 11212, j);
- strcpy((servers + i * 100 + j)->host, server);
- (servers + i * 100 + j)->value = memcached_hash_crc32(server, server_length);
- }
- i ++;
- }
- qsort(servers, 200, sizeof(server_t), server_cmp);
- //print_servers(servers, 200);
- return servers;
- }
- //给定一个key, 查找对应的服务器, libmemcached的查找算法简化板
- void search(server_t *servers, int num, char *key, int key_len) {
- server_t *begin, *left, *middle, *right, *end;
- begin = left = servers;
- end = right = servers + num;
- uint32_t hash;
- hash = memcached_hash_crc32(key, key_len);
- while (left < right) {
- middle= left + (right - left) / 2;
- if (middle->value < hash) left= middle + 1;
- else right= middle;
- }
- if (right == end) right= begin;
- printf("key: %s, server: %s\n", key, right->host);
- }
- //下面是模拟的一个分布测试程序
- void main(void) {
- server_t *servers;
- uint32_t hash;
- char key[128];
- int i = 0, key_len;
- servers = construct_servers();
- while ( i < 100 ) {
- key_len = sprintf(key, "item-%d", i ++);
- search(servers, 200, key, key_len);
- }
- }
文章来源:http://blog.csdn.net/lgg201/article/details/6856112