哪种一致性哈希算法才是解决分布式缓存问题的王者?

一致性哈希是由Karger等人于1997年提出的一种特殊的哈希算法,目的是解决分布式缓存的问题,现在在分布式系统中有着广泛的应用。本文将对ketama、jump consistent hash、rendezvous hash和maglev hash四种算法进行对比分析。

一、一致性哈希的特性

  • 平衡性

不同key通过算法映射后,可以比较均衡地分布到所有的后端节点上。

  • 单调性

当有新的节点上线后,系统中原有的key要么还是映射到原来的节点上,要么映射到新加入的节点上,不会出现从一个老节点重新映射到另一个老节点。

  • 稳定性

当服务发生扩缩容的时候,发生迁移的数据量尽可能少。

二、问题背景

假设我们有N个cache服务器节点,那如何将数据映射到这N个节点上呢,最简单的方法就是用数据计算出一个hash值,然后用hash值对N取模,如:hash(data) % N,这样只要计算出来的hash值比较均匀,那数据也就能比较均匀地映射到N个节点上了。但这带来的问题就是,如果发生扩缩容,节点的数量发生了变化,那很多数据的映射关系都会发生变化。显然这种方法虽然简单,但并不太能解决我们的需求。

三、四种常见一致性哈希算法

下面分别介绍对比四种比较常见的一致性哈希算法,看看一致性哈希算法是怎么解决这问题的。

1、经典一致性哈希

经典的一致性哈希算法也就是我们常说的割环法 ,大家应该都比较熟悉。简单来说就是,我们把节点通过hash的方式,映射到一个范围是[0,2^32]的环上,同理,把数据也通过hash的方式映射到环上,然后按顺时针方向查找第一个hash值大于等于数据的hash值的节点,该节点即为数据所分配到的节点。而更好点的做法是带虚拟节点的方法,我们可以为每个物理节点分配若干个虚拟节点,然后把虚拟节点映射到hash环,分配给每个物理节点虚拟节点数量对应每个物理节点的权重,如下图1所示。这样还是按顺时针的方法查找数据所落到的虚拟节点,再看该虚拟节点是属于哪个物理节点就可以知道数据是分配给哪个物理节点了。

这种割环法的实现多种,下面以比较有名的Ketama Hash实现为例进行对比分析。Ketama Hash的关键源码如下:

#服务器节点例子,第一列为地址,第二列为内存
#------ Server --------Mem-#
#255.255.255.255:6553566666#
10.0.1.1:11211600
10.0.1.2:11211300
10.0.1.3:11211200
10.0.1.4:11211350
10.0.1.5:112111000
10.0.1.6:11211800
10.0.1.7:11211950
10.0.1.8:11211100typedef struct
{unsigned int point;  // point on circlechar ip[22];
} mcs;typedef struct
{char addr[22];unsigned long memory;
} serverinfo;typedef struct
{int numpoints;void* modtime;void* array; //array of mcs structs
} continuum;typedef continuum* ketama_continuum;/** \brief Generates the continuum of servers (each server as many points on a circle).* \param key Shared memory key for storing the newly created continuum.* \param filename Server definition file, which will be parsed to create this continuum.* \return 0 on failure, 1 on success. */
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;serverinfo* slist;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. */if ( numservers < 1 ){set_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;}
#ifdef DEBUGsyslog( LOG_INFO, "Server definitions read: %u servers, total memory: %lu.\n",numservers, memory );
#endif/* Continuum will hold one mcs for each point on the circle: */mcs continuum[ numservers * 160 ];unsigned int i, k, cont = 0;for( i = 0; i < numservers; i++ ){float pct = (float)slist[i].memory / (float)memory;// 按内存权重计算每个物理节点需要分配多少个虚拟节点,正常是160个unsigned int ks = floorf( pct * 40.0 * (float)numservers );
#ifdef DEBUGint 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 );
#endiffor( k = 0; k < ks; k++ ){/* 40 hashes, 4 numbers per hash = 160 points per server */char ss[30];unsigned char digest[16];// 在节点的addr后面拼上个序号,然后以该字符串去计算hash值sprintf( ss, "%s-%d", slist[i].addr, k );ketama_md5_digest( ss, digest );/* Use successive 4-bytes from hash as numbers* for the points on the circle: */int h;// 16字节,每四个字节作为一个虚拟节点的hash值for( h = 0; h < 4; h++ ){continuum[cont].point = ( digest[3+h*4] << 24 )| ( digest[2+h*4] << 16 )| ( digest[1+h*4] <<  8 )|   digest[h*4];memcpy( continuum[cont].ip, slist[i].addr, 22 );cont++;}}}free( slist );// 排序,方便二分查找/* Sorts in ascending order of "point" */qsort( (void*) &continuum, cont, sizeof( mcs ), (compfn)ketama_compare );. . .return 1;
}

Ketama Hash的实现简单来说可以分成以下几步:

1)从配置文件中读取服务器节点列表,包括节点的地址及内存,其中内存参数用来衡量一个节点的权重;

2)对每个节点按权重计算需要生成几个虚拟节点,基准是每个节点160个虚拟节点,每个节点会生成10.0.1.1:11211-1、10.0.1.1:11211-2到10.0.1.1:11211-40共40个字符串,并以此算出40个16字节的hash值(其中hash算法采用的md5),每个hash值生成4个4字节的hash值,总共40*4=160个hash值,对应160个虚拟节点;

3)把所有的hash值及对应的节点地址存到一个continuum存组中,并按hash值排序方便后续二分查找计算数据所属节点。

这种割环法的平衡性在虚拟节点数较多且搭配较好的hash函数的情况下,可以具备较好的平衡性和稳定性 ,实际应用中可以采用比Ketama算法默认160更多的虚拟节点数,hash算法也可以采用其他的算法。在算法的复杂度方面,Ketama算法的复杂度是O(log(vn)),其中n是节点数,v是节点的虚拟节点数。Ketama算法也能很好地满足单调性,当发生节点数量发生伸缩的时候,相当于只是在环上增加或者去掉相应的虚拟节点,也就只会导致变化的节点上的数据发生重新映射,因些能很好满足单调性。

2、Rendezvous hash

这个算法比较简单粗暴,没有什么构造环或者复杂的计算过程,它对于一个给定的Key,对每个节点都通过哈希函数h()计算一个权重值wi,j = h(Keyi, Nodej),然后在所有的权重值中选择最大一个Max{wi,j}。显而易见,算法挺简单,所需存储空间也很小,但算法的复杂度是O(n)。从wiki上摘抄的python实现的核心代码如下:

def determine_responsible_node(nodes, key):"""Determines which node, of a set of nodes of various weights, is responsible for the provided key."""highest_score, champion = -1, Nonefor node in nodes:score = node.compute_weighted_score(key)if score > highest_score:champion, highest_score = node, scorereturn champion

当发生扩缩的时候,相当于增加了一次计算hash的机会,如果计算出来的hash值超过原来的最大值,则该部分key分配到新的节点,缩容的时候则相当于把该节点上的key迁移到该key原本计算出来的hash值次高的节点上。可见,当节点变化的时候,rendezvous hash只会影响到最大权重值落到变化的节点的key,也就是说只有变化的节点上的数据需要重新映射,因些也很符合单调性的要求。 而Rendezvous hash算法的平衡性和稳定性则取决于哈希函数的随机特性 。

在wiki上还提供了优化方法(Skeleton-based variant)来降低算法的复杂度,如下图2所示,把原始节点分成若干个虚拟组,虚拟组一层一层组成一个“骨架”,然后在虚拟组中按照Rendezvous hash一样的方法计算出最大的节点,从而得到下一层的虚拟组,再在下一层的虚拟组中按同样的方法计算,直到找到最下方的真实节点,最终可以把算法复杂度降低到O(log n)。

3.Jump consistent hash

Jump consistent hash是Google于2014年发表的论文中提出的一种一致性哈希算法, 它占用内存小且速度很快,并且只有大概5行代码,比较适合用在分shard的分布式存储系统中 。其完整的代码如下,其输入是一个64位的key及桶的数量,输出是返回这个key被分配到的桶的编号。

int32_t JumpConsistentHash(uint64_t key, int32_t num_buckets) {int64_t b = ­-1, j = 0;while (j < num_buckets) {b = j;key = key * 2862933555777941757ULL + 1;j = (b + 1) * (double(1LL << 31) / double((key >> 33) + 1));}return b;
}

下面根据论文内容简单介绍下其原理:

  • 记ch(key, num_buckets)为桶数量为num_buckets时的hash函数
  • 当num_buckets = 1时,显而易见,所有key都会分配给仅有的一个桶,即ch(key, 1) == 0
  • 当num_buckets = 2时,为了使用key分布均匀,应该有1/2的key保留在0号桶中,而有1/2的key应该迁移到1号桶中
  • 由此可以发现,当num_buckets由n变为n+1时,ch(key, n+1)的结果中应该有n/n+1结果保持不变,而有1/n+1的结果发生怕了跳变,变成了n

Jump consistent hash的这种思路看上去其实挺简单,就是num_buckets变化的时候,有些key的计算结果会发生变化。假如这里我们取一个随机数来决定每次要不要跳变,并且这个随机数只跟key有关,那么我们得到的初步算法如下:

int ch(int key, int num_buckets) {random.seed(key) ;int b = 0; // This will track ch(key, j +1) .for (int j = 1; j < num_buckets; j ++) {if (random.next() < 1.0/(j+1) ) b = j ; //这个不会经常执行}  return b;
}

从代码可以看出,算法的复杂度是O(n),而且大家会发现,大多数情况下不会发生跳变,也就是b=j并不会执行,并且随着j越来越大,跳变的可能越来越小, 那么有没有什么办法来进行优化,让我们能通过一个随机数来直接得到下一次跳变的j,降低算法的复杂度呢? 论文也在此基础上给出了优化后的算法并推理论证:

  • 把ch(key, num_buckets)看做是一个随机变量,对于特定的key k,jump consistent hash跟踪了其桶编号的跳变
  • 假设b是最后一次跳变的桶编号,也就是ch(k, b) != ch(k, b+1) 且ch(k, b+1) = b
  • 假设下一次跳变的结果是j,也就是(b, j)之间每一次增加桶的结果都不应该发生跳变,对于(b,j)区间内的任意的i,j是下一次跳变的概率可以记为:P(j ≥ i) = P( ch(k, i) = ch(k, b+1) )
  • 幸运的是P( ch(k, i) = ch(k, b+1) )的结果很好算,我们注意到P( ch(k, 10) = ch(k, 11) ) = 10/11, P( ch(k, 11) = ch(k, 12) ) = 11/12, P( ch(k, 10) = ch(k, 12) ) = 10/11 * 11/12 = 10/12。总的来说,如果n ≥ m, P( ch(k, n) = ch(k, m) ) = m / n. 因此对于任意的 i > b有:P(j ≥ i) = P( ch(k, i) = ch(k, b+1) ) = (b+1) / i,也就是j ≥ i的概率是(b+1) / i。
  • 此时我们取一个[0,1]区间的随机数r,规定r < (b+1) / i就有j ≥ i,也就是i ≤ (b+1) / r,这样我们就得到了i的上界是(b+1) / r,而对于任意的i都有j ≥ i,所以j = floor((b+1) / r),这样我们就用一个随机数r来算出了j。

所以上面的算法可以优化成以下实现:

int ch(int key, int num_buckets) {random. seed(key) ;int b = -1; //  bucket number before the previous jumpint j = 0; // bucket number before the current jumpwhile(j<num_buckets){b=j;double r=random.next(); //  0<r<1.0< span="">j = floor( (b+1) /r);}return b;
}</r<1.0<>

这里算法的复杂度变成了O(ln n),代码里的随机函数需要是一个均匀的随机数生成器,论文中这里采用了一个64位的线性同余随机数生成器,所以对于key本来就是64位整数的,也不需要再对key进行hash计算了。Jump consistent hash的平衡性也取决于线性同余随机数生成器,因此也有着比较好的平衡性,论文中也与Karger经典的一致性哈希算法进行了对比,下图3为其对比结果,从标准差来看,jump consistent hash的平衡性比经典的一致性哈希算法好很多。而当节点数量发生变化的时候,jump consistent hash会发生跳变的key的数量已经是理论上的最小值1/n了。但jump consistent hash也有一个比较明显的缺点,它只能在尾部增删节点,而不太好在中间增删,对于那种节点随机故障需要剔除的情况,如果用这个算法就需要再采用其他方法来处理了。

4、Maglev hash

Maglev hash是Google于2016年发表的一篇论文中提出来的一种新的一致性哈希算法。Maglev hash的基本思路是建立一张一维的查找表,如图4所示,一个长度为M的列表,记录着每个位置所属的节点编号B0…BN,当需要判断某个key被分配到哪个节点的时候,只需对key计算hash,然后对M取模看所落到的位置属于哪个节点。

如何查找看上去很简单,问题是如何产生这个查找表 。接下来以图5为例介绍下如何生成查找表,假设我们有三个节点,B0、B1、B2,我们为每个节点生成长度为M(图5中M=7)的permutation list(偏好序列),序列是(0,M-1)的随机数(如何生成这个序列我们下面解释),如图5所示,B0的偏好序列是[3,0,4,1,5,2,6]。另外我们准备一个长度为M的待填充的查找表Entry。然后我们按B0、B1、B2的顺序,根据每个节点的偏好序列中的数字来填充查找表Entry。我们把偏好序列中的数字作为查找表中的目标位置,把该序列的节点编号填充到查找表的目标位置上,如果目标位置已经被占用,则继续往下查看偏好序列的下一个数字。以图5举例,具体步骤如下:

1)首先从B0的开始,B0偏好序列的第一个数字是3,所以查找表的3号位置填上B0

2)按顺序轮到B1,其偏好序列的第一个数字是0,所以查找表的0号位置填上B1

3)接着轮到B2,发现第一个数字是3,而查找表的3号位置已经被占用,那继续看B2的偏好序列的第二个数字是4,所以查找表的4号位置填上B2

4)然后又回到B0,这时发现B0的第二个数字是0已经被占用,往下看偏好序列的第三个数字是4,也被占用了,则继续往下看第四个数字是1,查找表的1号位置没被占用,所以查找表的1号位置填上B0

5)按此规则继续处理B1、B2,直到把查找表的所有位置都填满

论文中也给出了查找表生成过程的伪代码,如下图6所示:

介绍完查找表是如何生成的, 还剩下一个问题就是各节点的偏好序列又是如何生成的 。论文中给出的方法是取两个不相关的hash函数,然后以各个节点的名字使用两个hash函数分别计算,得到一个offset和一个skip,如下图7所示,有了offset和skip,对于i号节点的偏好序列的第j个数,则通过(offset + j * skip) mod M就可以得到。论文中还强调了,这里的M必须是一个素数,这是为了让每个skip值跟M互质。另外,算法的复杂度是O(MlogM),最坏情况是O(M*M),这种情况发生在节点数量N=M,并且每个节点生成的序列都是一样的情况下,为了避免这种情况,一般建议选择一个值远大于N的M。

论文中说明了,按这种方法生成的查找表,每个节点分到的槽位基本是M/N个,最多只有1的差别,其中N是指所有节点的数量,由此可以看到,Maglev hash有很好的平衡性。下图8是论文中给出的,Maglev hash与经典一致性hash及Rendezvous在均衡性方面的实现对比结果,其中的small跟large分别代码查找表大小为65537和655373,而min跟max分别代表最小及最大的槽位占比。从图中可以看到,无论是在small还是large的情况下,Maglev hash的均衡性都是最好的,而在small的情况下经典一致性hash及Rendezvous的最小及最大的槽位占比相差还是挺大的,当然这种情况可以随着查找表的增大而有所下降。

接下来看下 当节点发生增删的时候,对生成的查找表有什么影响 。以下图9为例,我们在图5原来的基础上假设B1节点出现故障被淘汰掉了,这必然导致查找表里的一些槽位编号发生变化,从图9可以看到,当B1节点删除后,有3个槽位发生了变化,其中0号跟2号位置,由于B1节点的删除被重新分配给了B0,这符合一致性hash的单调性,比较好理解,但还发生了一个从B0到B2的重新映射,这是不符合一致性哈希算法的单调性要求的,论文中也指出了这种情况的存在。

在稳定性方面,经典一致性哈希、Rendezvous和Jump consistent hash都做到了在后端节点数量发生变化的时候的最小重新映射,而从图9删除节点的情况来看,Maglev hash并没有做到最小重新映射。针对这个问题,论文中也对Maglev hash对后端节点数量变化的容忍性做了测试实验,下图10是其测试结果,展示了相同后端节点数量、不同查找表大小的情况下,槽位映射结果发生变化的百分比与后端节点故障的百分比的关系。从图10可以看到, 随着后端节点故障百分比的增加,槽位映射结果发生变化的百分比也在增加,但是在查找表大小比较大的情况下,Maglev hash对后端节点的增删有更好的容忍性 。

但尽管是这样,Google仍然只采用65537的查找表大小,据说是觉得后端节点同时故障的概率小,而且还有其他保护机制,另外是当查找表大小从65537增加到655373的时候,查找表的生成时间从1.8ms增加到22.9ms,所以查找表也不能无限扩大。

四、总结

下面简单地以一个表格对以上四种一致性哈希算法进行对比总结:

算法 扩容 缩容 平衡性 单调性 稳定性 时间复杂度
Ketama 较好 较好 O(log vn)
Rendezvous 较好 较好 O(n)
Jump consistent hash 需要额外处理 O(ln n)
Maglev hash 较好 较好 较好 较好 O(MlogM),最坏O(M*M)

最后:

我们身为技术人员,最怕的就是安于现状,一直在原地踏步,那么你可能在30岁就会迎来自己的职业危机,因为你工作这么久提升的只有自己的年龄,技术还是万年不变!

如果你想在未来能够自我突破,圆梦大厂,那或许以上这份Java学习资料,你需要阅读阅读,希望能够对你的职业发展有所帮助。

获取方式: 只需你**点赞+关注**后,进[Java架构资源交流群] ,找管理员获取哦-!


*点赞+关注**后,进[Java架构资源交流群] ,找管理员获取哦-!

哪种一致性哈希算法才是解决分布式缓存问题的王者?相关推荐

  1. 一致性哈希算法(用于解决服务器均衡问题)

    tencent2012 笔试题附加题 问题描述:  例如手机朋友网有n个服务器,为了方便用户的访问会在服务器上缓存数据,因此用户每次访问的时候最好能保持同一台服务器.已有的做法是根据ServerIPI ...

  2. 【重难点】【Java基础 01】一致性哈希算法、sleep() 和wait() 的区别、强软弱虚引用

    [重难点][Java基础 01]一致性哈希算法.sleep() 和wait() 的区别.强软弱虚引用 文章目录 [重难点][Java基础 01]一致性哈希算法.sleep() 和wait() 的区别. ...

  3. 图解什么是一致性哈希算法

    1. 写在前面 周末就像太阳,总会到来,也总会离开. 此刻,没错,是周六呀!还是双休那种! 昨晚在B站看了几个长视频,导致2点才睡觉,早上一觉醒来已经10点了. 在这里温馨提示各位盆友们,虽然我们都是 ...

  4. [图文详解]什么是一致性哈希算法,墙裂推荐收藏

    1. 写在前面 周一就像太阳,总会到来,也总会离开. 此刻,没错,是周一呀! 昨晚在B站看了几个长视频,导致2点才睡觉,早上是真的起不来. 在这里温馨提示各位盆友们,虽然我们都是年轻人,但还是要规律作 ...

  5. 一次失败的面试,复习一次一致性哈希算法

    来自公众号:孤独烟 本文故事绝对真实,如有雷同,绝对不是巧合! 于是呢,烟哥提前十分钟在公司里头找了一个厕所的坑位,然后进去随手一锁门-.(以下省略10000字)- 唉- 我竟然又带薪上厕所了,而且上 ...

  6. 一文搞懂负载均衡中的一致性哈希算法

    一致性哈希算法在很多领域有应用,例如分布式缓存领域的 MemCache,Redis,负载均衡领域的 Nginx,各类 RPC 框架.不同领域场景不同,需要顾及的因素也有所差异,本文主要讨论在负载均衡中 ...

  7. 负载均衡一致性哈希算法实现 | nginx 负载均衡一致性哈希源码分析 | ngx_http_upstream_consistent_hash_module 源码分析

    这是本学期分布式计算/系统课程负载均衡节的课后作业,理解七层反向代理的负载均衡 Nginx 中使用的的一致性哈希算法.开头只是讲一些没用的东西,后面主要是分析 Nginx 的 O(1) 时间复杂度的一 ...

  8. 哈希分布与一致性哈希算法简介

    前言 在我们的日常web应用开发当中memcached可以算作是当今的标准开发配置了.相信memcache的基本原理大家也都了解过了,memcache虽然是分布式的应用服务,但分布的原则是由clien ...

  9. 一致性哈希算法以及其PHP实现

    在做服务器负载均衡时候可供选择的负载均衡的算法有很多,包括:  轮循算法(Round Robin).哈希算法(HASH).最少连接算法(Least Connection).响应速度算法(Respons ...

最新文章

  1. docker 批量删除 镜像或容器 删除所有容器
  2. python语言必背代码-Python新手必须知道的25条知识点
  3. 【Flutter】Image 组件 ( 内存加载 Placeholder | transparent_image 透明图像插件 )
  4. auto cad 打印颜色变浅_CAD制图软件中如何设置CAD打印样式表(CTB)?
  5. 纯CSS实现的3D简洁按钮设计
  6. 1024 Palindromic Number (25 分)大整数相加+会问+reverse倒置
  7. nProtect GameGuard 的破解
  8. journalctl用法详解
  9. 米家app扫描不到石头机器人_石头扫地机器人T7评测:能驾驭豪宅的高端旗舰?...
  10. 有道笔记Markdown快捷键
  11. android 小米8底部有一大片黑色
  12. Mac恢复被修改的文档
  13. 莫生气,一切对镜皆是考验,对面若不识,还需从头练
  14. 无人驾驶路径规划(三)局部路径规划-Frenet坐标系下的动态轨迹规划
  15. 【存储】文件存储、块存储、对象存储的区别
  16. 用html实现电子时钟
  17. Python代码使用easyocr识别身份证号码
  18. 【ML复习】什么是 监督学习,什么是 非监督学习?二者的区别是什么?列举常见的 监督学习算法 和 非监督学习算法。
  19. Android 批量插入删除手机联系人
  20. 贿赂囚犯 Bribe the prisoners DP

热门文章

  1. 关于大数据的视频资料
  2. 将图像DN值定标维热辐射强度之后,可用Planck函数求解出星上亮度温度
  3. (ASCII代码 )密码破译
  4. SSAO By Computer Shader(三)
  5. 返回上一步编辑或者上几步或者想恢复删除的代码
  6. ZZULIOJ:按身高站队(实验课)
  7. qtablewidget 复选框选中_QTableWidget中checkBox如何判断复选框是否被选中
  8. 你真的会用三目运算符吗?
  9. Win7怎么连接手机热点
  10. 网络安全三同步怎么实施