场景
单个节点的缓存容量达到上限,无法继续单点增加内存,如何解决?
单个节点支撑的QPS达到上限,如何解决?
初步方案
增加N个缓存节点,为了保证缓存数据的均匀,一般情况会采用对key值hash,然后取模的方式,然后根据结果,确认数据落到哪台节点上:如下:
hash(key)%N
很好,这个的确解决了上面的问题,实现了初步的分布式缓存,数据均匀分散到了各个节点上,流量请求也均匀的分散到了各个节点。
但是如果出现以下情况,会带来什么问题?
1、某台服务器突然宕机。缓存服务器从N变为N-1台。
2、缓存容量达到上限或者请求处理达到上限,需要增加缓存服务器,假定增加1台,则缓存服务器从N变为N+1
上面的情况带来的问题:
增加或者删除缓存服务器的时候,意味着大部分的缓存都会失效。这个是比较致命的一点,缓存失效,如果业务为缓存不命中,查询DB的话,会导致一瞬间DB的压力陡增。可能会导致整个服务不可用。
换种描述方式,我们需要解决怎么样的问题?或者需求是怎样的?
增删机器时,希望大部分key依旧在原有的缓存服务器上保持不变。举例来说:key1,key2,key3原先再Cache1机器上,现在增加一台缓存服务器,希望key1,key2,key3依旧在Cache1机器上,而不是在Cache2机器上。
改进方案(一致性Hash)
一致性哈希算法的简单背景介绍 (此段内容来自网络)
一致性哈希算法在1997年由麻省理工学院提出的一种分布式哈希(DHT)实现算法,设计目标是为了解决因特网中的热点(Hot spot)问题,初衷和CARP十分类似。一致性哈希修正了CARP使用的简单哈希算法带来的问题,使得分布式哈希(DHT)可以在P2P环境中真正得到应用。
一致性hash算法提出了在动态变化的Cache环境中,判定哈希算法好坏的四个定义:(来自百度百科)
-
平衡性(Balance):平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。
-
单调性(Monotonicity):单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到原有的或者新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。
-
分散性(Spread):在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。
-
负载(Load):负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同 的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。
所以通过上面的定义可以看到,简单的hash(key)%N的方式,违背了 单调性 的这个原则。原因如上面提到的,增删机器的时候,原有的缓存大部分会失效,也就违背了单调性的原则。
介绍:
大部分文章都提到环形的hash空间,但是没有讲为什么是环形的。后面我会聊下我的想法。
使用常见的hash算法可以把一个key值哈希到一个具有2^32个桶的空间中。也可以理解成,将key值哈希到 [0, 2^32) 的一个数字空间中。 我们假设这个是个首尾连接的环形空间。如下图:
假设我们现在有key1,key2,key3,key4 4个key值,我们通过一定的hash算法,将其对应到上面的环形hash空间中。
k1=hash(key1);
k2=hash(key2);
k3=hash(key3);
k4=hash(key4);
同样的,假设我们有3台cache服务器,把缓存服务器通过hash算法,加入到上述的环中。一般情况下是根据机器的IP地址或者唯一的计算机别名进行哈希。
c1=hash(cache1);
c2=hash(cache2);
c3=hash(cache3);
接下来就是数据如何存储到cache服务器上了,key值哈希之后的结果顺时针找上述环形hash空间中,距离自己最近的机器节点,然后将数据存储到上面, 如上图所示,k1 存储到 c3 服务器上, k4,k3存储到c1服务器上, k2存储在c2服务器上。用图表示如下:
增删机器的情况
假设cache3服务器宕机,这时候需要从集群中将其摘除。那么,之前存储再c3上的k1,将会顺时针寻找距离它最近的一个节点,也就是c1节点,这样,k1就会存储到c1上了,看一看下下面的图,比较清晰。
摘除c3节点之后,只影响到了原先存储再c3上的k1,而k3、k4、k2都没有受到影响,也就意味着解决了最开始的解决方案(hash(key)%N)中可能带来的雪崩问题。
增加节点原理和删除时差不多~
新增C4节点之后,原先存储到C1的k4,迁移到了C4,分担了C1上的存储压力和流量压力。
几个问题:
1、为什么需要想象成环形?
为了保证节点宕机摘除之后,原先存储在当前节点的key能找到可存储的位置。举个极端的例子,在不是环状hash空间下,刚好缓存的服务器处于0这个位置,那么0之后是没有任何节点信息的,那么当缓存服务器摘除的时候,以前存储在这台机器上的key便找不到顺时针距离它最近的一个节点了。但如果是环形空间,0之后的最近的一个节点信息有可能是2^32-1这个位置,他可以找到0之后的节点。如下图描述可能清晰一点。
2、为什么是2^32个桶空间?
没有搞清楚,个人理解是为了保证足够的灵活性,减少hash带来的key值冲突。也方便后续增删节点。
继续改进
上面的简单的一致性hash的方案在某些情况下但依旧存在问题:一个节点宕机之后,数据需要落到距离他最近的节点上,会导致下个节点的压力突然增大,可能导致雪崩,整个服务挂掉。
如下图所示,
当节点C3摘除之后,之前再C3上的k1就要迁移到C1上,这时候带来了两部分的压力:
1)、之前请求到C3上的流量转嫁到了C1上,会导致C1的流量增加,如果之前C3上存在热点数据,则可能导致C1扛不住压力挂掉。
2)、之前存储到C3上的key值转义到了C1,会导致C1的内容占用量增加,可能存在瓶颈。
当上面两个压力发生的时候,可能导致C1节点也宕机了。那么压力便会传递到C2上,又出现了类似滚雪球的情况,服务压力出现了雪崩,导致整个服务不可用。
如果解决上面的问题?
虚拟节点。歪果人的脑子真好使,想出这么一个牛逼的方式,虚拟节点。
如上描述,一个节点宕机之后可能会引起下个节点的存储及流量压力变大,这一点违背了最开始提到的四个原则中的 平衡性, 节点宕机之后,流量及内存的分配方式打破了原有的平衡。
虚拟节点,从名字可以看出来,这个节点是个虚拟的,每个实际节点对应多个虚拟节点。比较专业的说法如下:
“虚拟节点”( virtual node )是实际节点(机器)在 hash 空间的复制品( replica ),一实际个节点(机器)对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以hash值排列。
依旧用图片来解释,假设存在以下的真实节点和虚拟节点的对应关系。
Visual100—> Real1
Visual101—> Real1
Visual200—> Real2
Visual201—> Real2
Visual300—> Real3
Visual301—> Real3
同样的,hash之后的结果如下:
hash(Visual100)—> V100 —> Real1
hash(Visual101)—> V101 —> Real1
hash(Visual200)—> V200 —> Real2
hash(Visual201)—> V201 —> Real2
hash(Visual300)—> V300 —> Real3
hash(Visual301)—> V301 —> Real3
key值的hash结果如上,这里暂时不写了。
如图解释:
和之前介绍的不添加虚拟节点的类似,主要聊下如果宕机之后的情况。
假设Real1机器宕机,则会发生一下情况。
1、原先存储在虚拟节点V100上的k1数据将迁移到V301上,也就意味着迁移到了Real3机器上。
2、原先存储再虚拟节点V101上的k4数据将迁移到V200上,也就意味着迁移到了Real2机器上。
结果如下图:
这个就解决之前的问题了,某个节点宕机之后,存储及流量压力并没有全部转移到某台机器上,而是分散到了多台节点上。解决了节点宕机可能存在的雪崩问题。
当物理节点多的时候,虚拟节点多,这个的雪崩可能就越小。
PS:只有2个节点的时候,加入虚拟节点没有用。你懂的。