libmemcached一致性hash算法详解(1)----php-memcached客户端一致性哈希与crc算法共用产生的bug分析

author: selfimpr

blog: http://blog.csdn.net/lgg201

mail: lgg860911@yahoo.com.cn

事情的起源, 是同事使用下面的代码, 得到了一个诡异的结果, 而且是稳定的产生我们不期望的结果.

  1. <?php  
  2. $mem = new Memcached;  
  3. $mem->addServers(array(array('10.8.8.32',11300,100),array('10.8.8.32',11301,0)));  
  4. $mem->setOption(Memcached::OPT_DISTRIBUTION, Memcached::DISTRIBUTION_CONSISTENT);  
  5. $mem->setOption(Memcached::OPT_HASH, Memcached::HASH_CRC);  
  6. for ($i=0;$i<10;$i++){  
  7.     $key = "item_$i";  
  8.     $arr = $mem->getServerByKey($key);  
  9.     echo ($key.":\t".$arr['port']."\n");  
  10. }  
  11. print_r($mem->getServerList());  

代码很简单, 就是创建了一个php的Memcached客户端, 增加了两台服务器, 设置了分布式算法为一致性哈希, Hash算法为CRC.

运行了很多次这个测试用例, 产生的输入都如下:

  1. item_1: 11301  
  2. item_2: 11301  
  3. item_3: 11301  
  4. item_4: 11301  
  5. item_5: 11301  
  6. item_6: 11301  
  7. item_7: 11301  
  8. item_8: 11301  
  9. item_9: 11301  
  10. Array  
  11. (  
  12.     [0] => Array  
  13.         (  
  14.             [host] => 10.8.8.32  
  15.             [port] => 11300  
  16.             [weight] => 100  
  17.         )  
  18.   
  19.     [1] => Array  
  20.         (  
  21.             [host] => 10.8.8.32  
  22.             [port] => 11301  
  23.             [weight] => 0  
  24.         )  
  25.   
  26. )  

从上面的输出中我们可以看出, 两台服务器都是OK的, 但是, 只能所有的key都被分布到了一台服务器上.

后来, 我尝试对测试用例做了以下修改:

1. 将Hash算法换做其他支持的算法

2. 将分布式算法换成普通的算法

我做的以上尝试, 输出的结果都是我们期望的, key被分布到不同的服务器上.

然后就是痛苦的问题跟踪, 最终发现问题出在php的memcached客户端对libmemcached的实现上

在libmemcached中, 用来代表一组服务器(针对同一个客户端服务)的结构(libmemcached/memcached.h中定义)是: struct memcached_st {};下面摘取其中的部分定义:

  1. struct memcached_st {  
  2.   uint8_t purging;  
  3.   bool is_allocated;  
  4.   uint8_t distribution;  
  5.   uint8_t hash;  
  6. ...  
  7.   memcached_hash hash_continuum;  
  8. ...  
  9. };  

请记住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代码描述如下:

  1. <?php  
  2. $servers    = array(  
  3.     array('10.8.8.32', 11301),   
  4.     array('10.8.8.32', 11300),   
  5. );  
  6. $points     = array();  
  7. $index      = 0;  
  8. foreach ( $servers as $server ) {  
  9.     $i  = 0;  
  10.     while ( $i ++ < 100 ) { //libmemcached中100是两个常量求得的值  
  11.         $points[]   = array(  
  12.             'index' => $index,   
  13.             'value' => hash_value,   
  14.         );  
  15.     }  
  16.     $index ++;  
  17. }  
  18. //这里再对$servers按照元素的'value'排序  

也就是: 以$host:$port-$i作为key产生100个hash值, 所有服务器产生的这些hash值再排序

这里在libmemcached的update_continuum中, 我们需要找到下面这句代码:

  1. value= memcached_generate_hash_value(sort_host, sort_host_length, ptr->hash_continuum);  

也就是求每个点的hash值, 可以看到, 这里用了ptr中的hash_continuum字段给定的hash算法计算.

3. 将key分布到服务器上

这里的分布过程, 其实就是对上面产生的点集进行一个二分查找, 找到离key的hash值最近的点, 以其对应服务器作为key的服务器, 用php代码描述如下:

  1. <?php  
  2. $points = array();  //之前取到的服务器产生的点集  
  3. $hash   = 1;        //要查找的key的hash值  
  4. $begin = $left = 0;  
  5. $end = $right = floor(count($points) / 2);  
  6. while ( $left < $right ) {  
  7.     $middle = $left + floor(($left + $right) / 2);  
  8.     if ( $points[$middle]['value'] < $hash ) $left = $middle + 1;  
  9.     else $right = $middle;  
  10. }  
  11. //数组越界检查  
  12. if ( $right = $end ) $right = $begin;  
  13. //这里就得到了key分布到的服务器是所有服务器中的第$index个  
  14. $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分析

这里就不废话了, 直接上代码:

  1. #include <stdio.h>  
  2. #include <stdlib.h>  
  3. #include <stdint.h>  
  4. #include <string.h>  
  5.   
  6. /*---------------------------以下部分摘自libmemcached/crc.c--------------------------*/  
  7. static const uint32_t crc32tab[256] = {  
  8.   0x00000000, 0x77073096, 0xee0e612c, 0x990951ba,  
  9.   0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,  
  10.   0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,  
  11.   0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,  
  12.   0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de,  
  13.   0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,  
  14.   0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec,  
  15.   0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,  
  16.   0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,  
  17.   0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,  
  18.   0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940,  
  19.   0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,  
  20.   0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116,  
  21.   0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,  
  22.   0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,  
  23.   0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,  
  24.   0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a,  
  25.   0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,  
  26.   0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818,  
  27.   0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,  
  28.   0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,  
  29.   0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,  
  30.   0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c,  
  31.   0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,  
  32.   0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2,  
  33.   0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,  
  34.   0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,  
  35.   0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,  
  36.   0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086,  
  37.   0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,  
  38.   0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4,  
  39.   0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,  
  40.   0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,  
  41.   0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683,  
  42.   0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,  
  43.   0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,  
  44.   0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe,  
  45.   0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,  
  46.   0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,  
  47.   0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,  
  48.   0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252,  
  49.   0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,  
  50.   0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60,  
  51.   0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,  
  52.   0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,  
  53.   0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,  
  54.   0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04,  
  55.   0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,  
  56.   0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a,  
  57.   0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,  
  58.   0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,  
  59.   0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21,  
  60.   0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e,  
  61.   0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,  
  62.   0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c,  
  63.   0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,  
  64.   0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,  
  65.   0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,  
  66.   0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0,  
  67.   0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,  
  68.   0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6,  
  69.   0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,  
  70.   0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,  
  71.   0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d,  
  72. };  
  73.   
  74. uint32_t hash_crc32(const char *key, size_t key_length)  
  75. {  
  76.   uint32_t x;  
  77.   uint32_t crc= UINT32_MAX;  
  78.   
  79.   for (x= 0; x < key_length; x++)  
  80.     crc= (crc >> 8) ^ crc32tab[(crc ^ (key[x])) & 0xff];  
  81.   
  82.   return ~crc;  
  83. }  
  84. /*---------------------------以上部分摘自libmemcached/crc.c--------------------------*/  
  85.   
  86. //libmemcached中使用crc求hash值的方法  
  87. uint32_t memcached_hash_crc32(const char *key, size_t key_len) {  
  88.     return ((hash_crc32(key, key_len) >> 16) & 0x7fff);  
  89. }  
  90.   
  91. /** 
  92.  * 模拟libmemcached的一致性hash算法中的点   
  93.  * libmemcached中, 假设有A,B两台服务器, 则其一致性算法为: 
  94.  *  1. 假设每台服务器100个点 
  95.  *  2. 每个点是一个类似下面的server_t的结构, 一个字段存放server的索引, 一个字段存放值 
  96.  *  3. 每个服务器构造自己对应的100个点时, 对host:port-i求hash值, 其中host为服务器地址, port为端口, i为0-99的数 
  97.  *  4. 将两个服务器总共产生的200个点放入一个数组, 按照点的value(即上面求得的hash值)排序 
  98.  * 下面是拿到一个key进行分布时的策略 
  99.  *  1. 对key求一个hash值 
  100.  *  2. 从上面得到的服务器的200个点的数组中, 二分查找离这个key产生的hash值最近的点 
  101.  *  3. 拿到这个点对应的服务器 
  102.  */  
  103. typedef struct _server {  
  104.     char host[128];  
  105.     uint32_t value;  
  106. } server_t;  
  107.   
  108. //libmemcached中点的比较函数  
  109. int server_cmp(const void *s1, const void *s2) {  
  110.     server_t *cs1 = (server_t *)s1;  
  111.     server_t *cs2 = (server_t *)s2;  
  112.     if ( cs1->value == cs2->value ) return 0;  
  113.     else if ( cs1->value > cs2->value ) return 1;  
  114.     else return -1;  
  115. }  
  116.   
  117. //打印从servers开始的n个点  
  118. void print_servers(const server_t *servers, int n) {  
  119.     int i = 0;  
  120.     while ( i < n) {  
  121.         printf("server: %s, value: %d\n", (servers + i)->host, (servers + i)->value);  
  122.         i ++;  
  123.     }  
  124. }  
  125. //构造示例性的2台服务器产生的点, libmemcached中的算法简化了就是如此  
  126. server_t *construct_servers() {  
  127.     server_t *servers = malloc(sizeof(server_t) * 200);  
  128.     int i = 0, j;  
  129.     int server_length = 0;  
  130.     char server[128];  
  131.     while ( i < 2 ) {  
  132.         for ( j = 0; j < 100; j ++ ) {  
  133.             server_length = sprintf(server, "127.0.0.1:%d-%d", i ? 11211 : 11212, j);  
  134.             strcpy((servers + i * 100 + j)->host, server);  
  135.             (servers + i * 100 + j)->value = memcached_hash_crc32(server, server_length);  
  136.         }  
  137.         i ++;  
  138.     }  
  139.     qsort(servers, 200, sizeof(server_t), server_cmp);  
  140.     //print_servers(servers, 200);  
  141.     return servers;  
  142. }  
  143. //给定一个key, 查找对应的服务器, libmemcached的查找算法简化板  
  144. void search(server_t *servers, int num, char *key, int key_len) {  
  145.     server_t *begin, *left, *middle, *right, *end;    
  146.     begin = left = servers;  
  147.     end = right = servers + num;  
  148.     uint32_t hash;  
  149.     hash = memcached_hash_crc32(key, key_len);  
  150.   
  151.     while (left < right) {  
  152.         middle= left + (right - left) / 2;  
  153.         if (middle->value < hash) left= middle + 1;  
  154.         else right= middle;  
  155.     }  
  156.     if (right == end) right= begin;  
  157.     printf("key: %s, server: %s\n", key, right->host);  
  158. }  
  159.   
  160. //下面是模拟的一个分布测试程序  
  161. void main(void) {  
  162.     server_t *servers;  
  163.     uint32_t hash;  
  164.     char key[128];  
  165.     int i = 0, key_len;  
  166.   
  167.     servers = construct_servers();  
  168.   
  169.     while ( i < 100 ) {  
  170.         key_len = sprintf(key, "item-%d", i ++);  
  171.         search(servers, 200, key, key_len);  
  172.     }  

文章来源:http://blog.csdn.net/lgg201/article/details/6856112

posted @ 2011-10-13 11:33  zhaoping  阅读(719)  评论(0编辑  收藏  举报