当我们拥有了多台存储服务器之后,现在有多个key,希望可以将这些个key均匀的缓存到这些服务器上,可以使用哪些方案呢?
1. 普通哈希取模法
1.1 直接哈希取模
这是一种最容易想到的方法,使用取模算法hash(key)% N,对key进行hash运算后取模,N是机器的数量。key进行hash后的结果对3取模,得到的结果一定是0、1或者2,正好对应服务器node0、node1、node2,存取数据直接找对应的服务器即可,简单粗暴,完全可以解决上述的问题
1.2 普通哈希取模法缺陷
取模算法虽然使用简单,但对机器数量取模,在集群扩容和收缩时却有一定的局限性,因为在生产环境中根据业务量的大小,调整服务器数量是常有的事;而服务器数量N发生变化后hash(key)% N计算的结果也会随之变化。
比如:一个服务器节点挂了,计算公式从hash(key)% 3变成了hash(key)% 2,结果会发生变化,此时想要访问一个key,这个key的缓存位置大概率会发生改变,那么之前缓存key的数据也会失去作用与意义,大量缓存在同一时间失效,造成缓存的雪崩,进而导致整个缓存系统的不可用,这基本上是不能接受的。
2. 一致性哈希算法
一致性哈希的出现就是为了解决上面的问题:
一致性hash算法本质上也是一种取模算法,不过,不同于上边按服务器数量取模,一致性hash是对固定值2^32取模。
2.1 Hash环
我们可以将这2 ^ 32个值抽象成一个圆环,圆环的正上方的点代表0,顺时针排列,以此类推,1、2、3、4、5、6……直到2^32-1,而这个由2的32次方个点组成的圆环统称为hash环.
那么这个hash环和一致性hash算法又有什么关系嘞?我们以三台服务器编号0、1、2为例:
2.2 服务器映射到 Hash环
这个时候计算公式就从hash(key)% N 变成了hash(服务器ip)% 232,使用服务器IP地址进行hash计算,用哈希后的结果对232取模,结果一定是一个0到2^32-1之间的整数,而这个整数映射在hash环上的位置代表了一个服务器,依次将node0、node1、node2三个服务器映射到hash环上
2.3 key值映射到Hash环
接着在将需要缓存的key对象也映射到hash环上,hash(key)% 2^32
2.4 对象key映射到服务器
服务器节点和要缓存的key对象都映射到了hash环,那对象key具体应该缓存到哪个服务器上呢?
key->server映射规则:从缓存对象key的位置开始,沿顺时针方向遇到的第一个服务器,便是当前对象将要缓存到的服务器。
2.5 一致性哈希应对扩容与缩容
我们简单了解了一致性hash的原理,那它又是如何优化集群中添加节点和缩减节点,普通取模算法导致的缓存服务,大面积不可用的问题呢?
2.5.1 集群添加节点
先来看看扩容的场景,假如业务量激增,系统需要进行扩容增加一台服务器node-4,刚好node-4被映射到node-1和node-2之间。
此时key1 按照映射规则被重定向到node_4节点,其他节点的映射服务器不发生变化
2.5.1 集群缩减节点
与增加节点基本一致,假设缩减掉node_0,此时key_3被重定向到node_2,其他key不受影响
从上边的两种情况发现,当集群中服务器的数量发生改变时,一致性hash算只会影响少部分的数据,保证了缓存系统整体还可以对外提供服务的。
2.6 数据偏斜问题
在服务器节点数量太少的情况下,很容易因为节点分布不均匀而造成数据倾斜问题,如下图被存储的对象大部分在node-1服务器上,导致其他节点资源浪费,系统压力大部分集中在node-4节点上,这样的集群是非常不健康的
解决数据倾斜的办法也简单,我们就要想办法让节点映射到hash环上时,相对分布均匀一点。
一致性Hash算法引入了一个虚拟节点机制,即对每个服务器节点计算出多个hash值,它们都会映射到hash环上,映射到这些虚拟节点的对象key,最终会缓存在真实的节点上。
虚拟节点的hash计算通常可以采用,对应节点的IP地址加数字编号后缀 hash(i+10.24.23.227) 的方式,举个例子,node-1节点IP为10.24.23.227,计算node-1的hash值:
- hash(110.24.23.227)% 2^32
假设我们给node-1设置三个虚拟节点,node-1#1、node-1#2、node-1#3,对它们进行hash后取模。
-
hash(110.24.23.227)% 2^32
-
hash(210.24.23.227)% 2^32
-
hash(310.24.23.227)% 2^32
需要注意一点,分配的虚拟节点个数越多,映射在hash环上才会越趋于均匀,节点太少的话很难看出效果
2.7 一致哈希的应用
一致性hash在分布式系统中应该是实现负载均衡的首选算法,它的实现比较灵活,既可以在客户端实现,也可以在中间件上实现,比如日常使用较多的缓存中间件memcached和redis集群都有用到它
- 练习题:基于上文讲解的一致性哈希原理,实现一套一致性哈希方法
3. 哈希槽
3.1 哈希槽定义
学习完哈希槽,我们会发现,一致性哈希算法的缺点在于对于数据分布,节点位置控制并不友好。
因此有人又提出了 “哈希槽”的概念,我们平时经常接触的Redis cluster就选择了哈希槽(slot)来实现流量分发。
同样,Dsjourney提供的分布式系统中,也是选择了这一方案,同学们可以在体验页面进行自己的哈希槽配置:
为了简化同学们的操作,我们在DsJourney中预定义定义了15个槽,槽中的数字代表了存储服务器组(分片服务器组)的组号(gid)。
首先哈希槽其实是两个概念,第一个是哈希算法。redis cluster 的 hash 算法不是简单的 hash(),而是 crc16 算法,一种校验算法。另外一个就是槽位的概念,空间分配的规则。其实哈希槽的本质和一致性哈希算法非常相似,不同点就是对于哈希空间的定义。一致性哈希的空间是一个圆环,节点分布是基于圆环的,无法很好的控制数据分布。而 redis cluster 的槽位空间是自定义分配的,类似于 windows 盘分区的概念。这种分区是可以自定义大小,自定义位置的。
3.2 哈希槽在实际生产中的应用—Redis Cluster
redis cluster 包含了16384个哈希槽,每个 key 通过计算后都会落在具体一个槽位上,而这个槽位是属于哪个存储节点的,则由用户自己定义分配。例如机器硬盘小的,可以分配少一点槽位,硬盘大的可以分配多一点。如果节点硬盘都差不多则可以平均分配。所以哈希槽这种概念很好地解决了一致性哈希的弊端
另外在容错性和扩展性上,表象与一致性哈希一样,都是对受影响的数据进行转移。而哈希槽本质上是对槽位的转移,把故障节点负责的槽位转移到其他正常的节点上。扩展节点也是一样,把其他节点上的槽位转移到新的节点上。
但一定要注意的是,对于槽位的转移和分派,redis 集群是不会自动进行的,而是需要人工配置的。所以 redis 集群的高可用是依赖于节点的主从复制与主从间的自动故障转移。