Ketama一致性哈希算法整理

为方便自己后期自己查看,把网上Hash映射做了整理:

那节点是怎样放入这个环中的呢?


         
    //对所有节点,生成nCopies个虚拟结点  
            for(Node node : nodes) {  
                //每四个虚拟结点为一组,为什么这样?下面会说到  
                for(int i=0; i<nCopies / 4; i++) {  
                    //getKeyForNode方法为这组虚拟结点得到惟一名称  
                    byte[] digest=HashAlgorithm.computeMd5(getKeyForNode(node, i));  
                /** Md5是一个16字节长度的数组,将16字节的数组每四个字节一组, 
                            分别对应一个虚拟结点,这就是为什么上面把虚拟结点四个划分一组的原因*/  
                    for(int h=0;h<4;h++) {  
                      //对于每四个字节,组成一个long值数值,做为这个虚拟节点的在环中的惟一key  
                        Long k = ((long)(digest[3+h*4]&0xFF) << 24)  
                            | ((long)(digest[2+h*4]&0xFF) << 16)  
                            | ((long)(digest[1+h*4]&0xFF) << 8)  
                            | (digest[h*4]&0xFF);  
                          
                        allNodes.put(k, node);  
                    }  
                }  
            }  


上面的流程大概可以这样归纳:四个虚拟结点为一组,以getKeyForNode方法得到这组虚拟节点的name,Md5编码后,每个虚拟结点对应Md5码16个字节中的4个,组成一个long型数值,做为这个虚拟结点在环中的惟一key。第12行k为什么是Long型的呢?呵呵,就是因为Long型实现了Comparator接口。

处理完正式结点在环上的分布后,可以开始key在环上寻找节点的游戏了。
对于每个key还是得完成上面的步骤:计算出Md5,根据Md5的字节数组,通过Kemata Hash算法得到key在这个环中的位置。


 final Node rv;  
        byte[] digest = hashAlg.computeMd5(keyValue);  
        Long key = hashAlg.hash(digest, 0);  
        //如果找到这个节点,直接取节点,返回  
        if(!ketamaNodes.containsKey(key)) {  
        //得到大于当前key的那个子Map,然后从中取出第一个key,就是大于且离它最近的那个key  
            SortedMap<Long, Node> tailMap=ketamaNodes.tailMap(key);  
            if(tailMap.isEmpty()) {  
                key=ketamaNodes.firstKey();  
            } else {  
                key=tailMap.firstKey();  
            }  
            //在JDK1.6中,ceilingKey方法可以返回大于且离它最近的那个key  
            //For JDK1.6 version  
//          key = ketamaNodes.ceilingKey(key);  
//          if (key == null) {  
//              key = ketamaNodes.firstKey();  
//          }  
        }  
          
          
        rv=allNodes.get(key);  


引文中已详细描述过这种取节点逻辑:在环上顺时针查找,如果找到某个节点,就返回那个节点;如果没有找到,则取整个环的第一个节点。
上文为简单的节点如何放到环上,和节点获取逻辑讲解,下文为某作者实际用例讲解:

http://blog.chinaunix.net/uid-20498361-id-4303232.html

Ketama的Hash算法,以虚拟节点的思想,解决Memcached的分布式问题。

写完memcached集群轻客户端有一段时间了,使用了ketama的第3方一致性hash算法库。这里分析一下它的实现。


1,简介
    若我们在后台使用NoSQL集群,必然会涉及到key的分配问题,集群中某台机器宕机时如何key又该如何分配的问题。
    若我们用一种简单的方法,n = hash( key)%N来选择n号服务器,一切都运行正常,若再考虑如下的两种情况;  
(1) 一个 cache 服务器 m down 掉了(在实际应用中必须要考虑这种情况),这样所有映射到 cache m 的对象都会失效,怎么办,需要把 cache m 从 cache 中移除,这时候 cache 是 N-1 台,映射公式变成了 hash(object)%(N-1) ;  
(2) 由于访问加重,需要添加 cache ,这时候 cache 是 N+1 台,映射公式变成了 hash(object)%(N+1) ;  
1 和 2 意味着什么?这意味着突然之间几乎所有的 cache 都失效了。对于服务器而言,这是一场灾难,洪水般的访问都会直接冲向后台服务器; 
(3) 再来考虑一个问题,由于硬件能力越来越强,你可能想让后面添加的节点多做点活,显然上面的 hash 算法也做不到。
以上三个问题,可以用一致性hash算法来解决。关于一致性hash算法的理论网上很多,这里分析几种一致性hash算法的实现。

2,ketama实现分析
2.1 实现流程介绍
ketama对一致性hash算法的实现思路是:
(1) 通过配置文件,建立一个服务器列表,其形式如:(1.1.1.1:11211, 2.2.2.2:11211,9.8.7.6:11211...)
(2) 对每个服务器列表中的字符串,通过Hash算法,hash成几个无符号型整数。
    注意:如何通过hash算法来计算呢?
(3) 把这几个无符号型整数放到一个环上,这个换被称为continuum。(我们可以想象,一个从0到2^32的钟表)
(4) 可以建立一个数据结构,把每个数和服务器的ip地址对应在一起,这样,每个服务器就出现在这个环上的这几个位置上。
    注意:这几个数,不能随着服务器的增加和删除而变化,这样才能保证集群增加/删除机器后,以前的那些key都映射到同样的ip地址上。后面将会详细说明怎么做。
(5) 为了把一个key映射到一个服务器上,先要对key做hash,形成一个无符号型整数un,然后在环continuum上查找大于un的下一个数值。若找到,就把key保存到这台服务器上。
(6) 若你的hash(key)值超过continuum上的最大整数值,就直接回饶到continuum环的开始位置。
    这样,添加或删除集群中的结点,就只会影响一少部分key的分布。
    注意:这里说的会影响一部分key是相对的。其实影响的key的多少,由该ip地址占的权重大小决定的。在ketama的配置文件中,需要指定每个ip地址的权重。权重大的在环上占的点就多。

2.2 源码分析
在github上下载源码后,解压,进入ketama-master/libketama目录。一致性hash算法的实现是在ketama.c文件中。
在该文件中,还用到了共享内存,这里不分析这一部分,只分析一致性hash算法的核心实现部分。

2.2.1 数据结构

// 服务器信息,主要记录服务器的ip地址和权重值
typedef struct
{
    char addr[22];                   //服务器ip地址
    unsigned long memory;   // 权重值
} serverinfo;

// 以下数据结构就是continuum环上的结点,换上的每个点其实代表了一个ip地址,该结构把点和ip地址一一对应起来。
// 环上的结点
typedef struct
{
    unsigned int point;          //在环上的点,数组下标值
    char ip[22];                       // 对应的ip地址
} mcs;
2.2.2 一致性hash环的创建
该函数是创建continuum的核心函数,它先从配置文件中读取集群服务器ip和端口,以及权重信息。创建continuum环,并把这些服务器信息和环上的数组下标对应起来。

// 其中key是为了访问共享内存而设定的,在使用时可以把共享内存部分去掉。
static int
ketama_create_continuum( key_t key, char* filename )
{
    // 若不使用共享内存,可以不管
    if (shm_ids == NULL) {
        init_shm_id_tracker();
    }
   // 共享内存相关,用不着时,可以去掉
    if (shm_data == NULL) {
        init_shm_data_tracker();
    }
    int shmid;
    int* data;                                              /* Pointer to shmem location */
    // 该变量来记录共从配置文件中共读取了多少个服务器
    unsigned int numservers = 0;
    // 该变量是配置文件中所有服务器权重值得总和
    unsigned long memory;
    // 从配置文件中读取到的服务器信息,包括ip地址,端口,权重值
    serverinfo* slist;

    // 从配置文件filename中读取服务器信息,把服务器总数保存到变量numservers中,把所有服务器的权重值保存到memory中。
    slist = read_server_definitions( filename, &numservers, &memory );

    /* Check numservers first; if it is zero then there is no error message
     * and we need to set one. */
    // 以下几行是检查配置文件内容是否正确
    // 若总服务器数量小于1,错误。
    if ( numservers < 1 )
    {
        sprintf( k_error, "No valid server definitions in file %s", filename );
        return 0;
    }
    else if ( slist == 0 )  // 若服务器信息数组为空,错误
    {
        /* read_server_definitions must've set error message. */
        return 0;
    }

    // 以下代码开始构建continuum环
    /* Continuum will hold one mcs for each point on the circle: */

    // 平均每台服务器要在这个环上布160个点,这个数组的元素个数就是服务器个数*160。
    // 具体多少个点,需要根据事情的服务器权重值进行计算得到。
    // 为什么要选择160个点呢?主要是通过md5计算出来的是16个整数,把这个整数分成4等分,每份是4位整数。
    // 而每进行一次hash计算,我们可以获得4个点。
    mcs continuum[ numservers * 160 ];
    unsigned int i, k, cont = 0;
    // 遍历所有服务器开始在环上部点
    for( i = 0; i < numservers; i++ )
    {
        // 计算服务器i在所有服务器权重的占比
        float pct = (float)slist[i].memory / (float)memory;
        // 由于计算一次可以得到4个点,所有对每一台机器来说,总的计算只需要计算40*numservers次。
        // 按权重占比进行划分,就是以下的计算得到的次数
        unsigned int ks = floorf( pct * 40.0 * (float)numservers );

#ifdef DEBUG
        int hpct = floorf( pct * 100.0 );
        syslog( LOG_INFO, "Server no. %d: %s (mem: %lu = %u%% or %d of %d)\n",
            i, slist[i].addr, slist[i].memory, hpct, ks, numservers * 40 );
#endif

        // 计算出总次数,每次可以得到4个点
        for( k = 0; k < ks; k++ )
        {
            /* 40 hashes, 4 numbers per hash = 160 points per server */
            char ss[30];
            unsigned char digest[16];
            
            // 通过计算hash值来得到下标值,该hash值是字符串:"-n",其中的n是通过权重计算出来的该主机应该部点的总数/4。
            sprintf( ss, "%s-%d", slist[i].addr, k );
            // 计算其字符串的md5值,该值计算出来后是一个unsigned char [16]的数组,也就是可以保存16个字节
            ketama_md5_digest( ss, digest );

            /* Use successive 4-bytes from hash as numbers for the points on the circle: */
            // 通过对16个字节的每组4个字节进行移位,得到一个0到2^32之间的整数,这样环上的一个结点就准备好了。
            int h;
            // 共有16个字节,可以处理4次,得到4个点的值
            for( h = 0; h < 4; h++ )
            {
                // 把计算出来的连续4位的数字,进行移位。
                // 把第一个数字一道一个整数的最高8位,后面的一次移动次高8位,后面一次补零,这样就得到了一个32位的整数值。移动后
                continuum[cont].point = ( digest[3+h*4] << 24 )
                                      | ( digest[2+h*4] << 16 )
                                      | ( digest[1+h*4] << 8 )
                                      | digest[h*4];
                // 复制对应的ip地址到该点上
                memcpy( continuum[cont].ip, slist[i].addr, 22 );
                cont++;
            }
        }
    }
    free( slist );
    
    // 以下代码对计算出来的环上点的值进行排序,方便进行查找
    // 这里要注意:排序是按照point的值(计算出来的整数值)进行的,也就是说原来的数组下标顺序被打乱了。
    /* Sorts in ascending order of "point" */
    qsort( (void*) &continuum, cont, sizeof( mcs ), (compfn)ketama_compare );
    
    // 到这里算法的实现就结束了,环上的点(0^32整数范围内)都已经建立起来,每个点都是0到2^32的一个整数和ip地址的结构。
    // 这样查找的时候,只是需要hash(key),并在环上找到对应的数的位置,取得该节点的ip地址即可。

2.2.3 在环上查找元素
* 计算key的hash值的实现
unsigned int ketama_hashi( char* inString ) 
{
    unsigned char digest[16];
    // 对key的值做md5计算,得到一个有16个元素的unsigned char数组
    ketama_md5_digest( inString, digest );
    // 取数组中的前4个字符,并移位,形成一个整数作为hash得到的值返回
    return (unsigned int)(( digest[3] << 24 )
                        | ( digest[2] << 16 )
                        | ( digest[1] << 8 )
                        | digest[0] );
}

* 在环上查找相应的结点
mcs* ketama_get_server( char* key, ketama_continuum cont ) 
{
    // 计算key的hash值,并保存到变量h中
    unsigned int h = ketama_hashi( key );
    // 该变量cont->numpoints是总的数组埋点数
    int highp = cont->numpoints;
    // 数组结点的值
    mcs (*mcsarr)[cont->numpoints] = cont->array;
    int lowp = 0, midp;
    unsigned int midval, midval1;
    // divide and conquer array search to find server with next biggest
    // point after what this key hashes to
    while ( 1 )
    {
        // 从数组的中间位置开始找
        // 注意此时的数组是按照point的值排好序了
        midp = (int)( ( lowp+highp ) / 2 );
        // 若中间位置等于最大点数,直接绕回到0位置
        if ( midp == cont->numpoints )
            return &( (*mcsarr)[0] ); // if at the end, roll back to zeroth
       
        // 取的中间位置的point值
        midval = (*mcsarr)[midp].point;
        // 再取一个值:若中间位置下标为0,直接返回0,若中间位置的下标不为0,直接返回上一个结点的point值
        midval1 = midp == 0 ? 0 : (*mcsarr)[midp-1].point;
        // 把h的值和取的两个值point值进行比较,若在这两个point值之间说明h值应该放在较大的那个point值的下标对应的ip地址上
        if ( h <= midval && h > midval1 )
            return &( (*mcsarr)[midp] );
        // 否则继续2分
        if ( midval < h )
            lowp = midp + 1;
        else
            highp = midp - 1;
       // 若没有找到,直接返回0位置的值,这种情况应该很少
        if ( lowp > highp )
            return &( (*mcsarr)[0] );
    }
<strong>}</strong>

2.2.4 添加删除机器时会怎样
    先说明一下删除机器的情况。机器m1被删除后,以前分配到m1的key需要重新分配,而且最好是均匀分配到现存的机器上。
    我们来看看,ketama是否能够做到?
    当m1机器宕机后,continuum环需要重构,需要把m1的ip对应的点从continuum环中去掉。
我们来回顾一下环的创建过程:
    按每个ip平均160个点,可以计算出总数t。按每个ip的权重值占比和总数t的乘积得到该ip应该在该环上部的点数。若一台机器宕机,那么每台机器的权重占比增加,在该环上部的点数也就相应的增加,当然这个增加也是按每台机器的占比来的,占比多的增加的点数就多,占比少的增加的点数就少。但,每个ip的点数一定是增加的。
    创建环上的点值的过程是:
        先计算hash值:      
           
    for( k = 0; k < ks; k++ )     {    //其中ks是每个ip地址对应的总点数
                    ...
                    sprintf( ss, "%s-%d", slist[i].addr, k );  
                    ketama_md5_digest( ss, digest );
                    ... 
                }


        循环移位hash值:
  continuum[cont].point = ( digest[3+h*4] << 24 )
                                      | ( digest[2+h*4] << 16 )
                                      | ( digest[1+h*4] << 8 )
                                      | digest[h*4];

 由于此时每个ip的占比增加,ks就增加了:
 
       float pct = (float)slist[i].memory / (float)memory;   // 此时这个值增加
        unsigned int ks = floorf( pct * 40.0 * (float)numservers );  //该值也增加
这样,每个ip地址对应的point值就多了,但以前的point值不会变。依然在这个环上相同的点值上。也就是说把影响平均分摊到现有的各台机器上。
当然,删除的情况和添加的情况相似,都是把影响平均分摊到现有的各个机器上了。

小结:
(1) 环上的点是通过对ip地址加一个整数(形如:-N)作为一个字符串做hash,然后移位得到4个点数。
(2) 排序后,通过2分查找进行查询,效率较高。
(3) 这样,添加ip时,环上以前部的点不会变化,而且把影响分摊到现有的各个ip上。

问题:
这里我也对该算法提出了两点疑问,
问题1:创建环和在环上查找,都是使用的hash值4位取数的办法,那么是否存在查找某个key时,计算的值在环上不存在?当然这里也做了处理(找不到直接返回0号位置的ip地址: return &( (*mcsarr)[0] );),但若这种情况比较多时,误差可能比较大。
    通过测试发现,这种情况出现的概率并不大,几乎没有。

问题2:其实当ip地址有变动时,还是又可能使原来的key对应的ip地址有变化,只是这种情况概率比较小?那么能不能使得原来的key对应的ip地址不变化?还有待改进。

代码来源: https://github.com/RJ/ketama
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值