Redis集群方案基于分而治之的思想。Redis中数据都是以Key-Value形式存储的,而不同Key的数据之间是相互独立的。因此可以将Key按照某种规则划分成多个分区,将不同分区的数据存放在不同的节点上。这个方案类似数据结构中哈希表的结构。在Redis集群的实现中,使用哈希算法(公式是CRC16(Key) mod 16383)将Key映射到0~16383范围的整数。这样每个整数对应存储了若干个Key-Value数据,这样一个整数对应的抽象存储称为一个槽(slot)。每个Redis Cluster的节点——准确讲是master节点——负责一定范围的槽,所有节点组成的集群覆盖了0~16383整个范围的槽。
据说任何计算机问题都可以通过增加一个中间层来解决。槽的概念也是这么一层。它介于数据和节点之间,简化了扩容和收缩操作的难度。数据和槽的映射关系由固定算法完成,不需要维护,节点只需维护自身和槽的映射关系。
在正常集群分布时,需要对redis的key进行负载均衡,
简单方案,:
1.假设现在有4台服务器
2.对key进行hash并取余,得到对应的数值找到对应的服务器。
存在问题:如果线上4台服务器无法满足需求时,增加一台变成5台,这时候取余的结果跟之前的数值就存在区别,导致对应的key找不到数据,而且这个可能影响大部分的key都找不到。
上面的方案只是解决了性能扩展的问题,集群的故障容错能力并没有提升。提高容错能力的方法一般为使用某种备份/冗余手段。负责一定数量的槽的节点被称为master节点。为了增加集群稳定性,每个master节点可以配置若干个备份节点——称为slave节点。Slave节点一般作为冷备份保存master节点的数据,在master节点宕机时替换master节点。在一些数据访问压力比较大的情况下,slave节点也可以提供读取数据的功能,不过slave节点的数据实时性会略差一下。而写数据的操作则只能通过master节点进行。
当Redis节点接收到对某个key的命令时,如果这个key对应的槽不在自己的负责范围内,则返回MOVED重定向错误,通知客户端到正确的节点去访问数据。
如果频繁出现重定向错误,势必会影响访问的性能。由于从key映射到槽的算法是固定公开的,客户端可以在内部维护槽到节点的映射关系,访问数据时可以自己通过key计算出槽,然后找到正确的节点,减少重定向错误。目前大部分开发语言的Redis客户端都会实现这个策略。这个地址https://redis.io/clients可以查看主流语言的Redis客户端。
尽管不同节点存储的数据相互独立,这些节点仍然需要相互通信以同步节点状态信息。Redis集群采用P2P的Gossip协议,节点之间不断地通信交换信息,最终所有节点的状态都会达成一致。常用的Gossip消息有下面几种:
当某个节点出现问题时,需要一定的传播时间让多数master节点认为该节点确实不可用,才能标记标记该节点真正下线。Redis集群的节点下线包括两个环节:主观下线(pfail)和客观下线(fail)。
主观下线:当节点A在cluster-node-timeout时间内和节点B通信(ping-pong消息)一直失败,则节点A认为节点B不可用,标记为主观下线,并将状态消息传播给其他节点。
客观下线:当一个节点被集群内多数master节点标记为主观下线后,则触发客观下线流程,标记该节点真正下线。
故障恢复
一个持有槽的master节点客观下线后,集群会从slave节点中选出一个提升为master节点来替换它。Redis集群使用选举-投票的算法来挑选slave节点。一个slave节点必须获得包括故障的master节点在内的多数master节点的投票后才能被提升为master节点。假设集群规模为3主3从,则必须至少有2个主节点存活才能执行故障恢复。如果部署时将2个主节点部署到同一台服务器上,则该服务器不幸宕机后集群无法执行故障恢复。
默认情况下,Redis集群如果有master节点不可用,即有一些槽没有负责的节点,则整个集群不可用。也就是说当一个master节点故障,到故障恢复的这段时间,整个集群都处于不可用的状态。这对于一些业务来说是不可忍受的。可以在配置中将cluster-require-full-coverage配置为no,那么master节点故障时只会影响访问它负责的相关槽的数据,不影响对其他节点的访问。
一致性 Hash 算法通过构建环状的 Hash 空间替线性 Hash 空间的方法解决了这个问题,整个 Hash 空间被构建成一个首位相接的环。
如上图,上方两个图为普通hash环,其具体的构造过程为:
1.先构造一个长度为 2^32 的一致性 Hash 环
2.计算每个缓存服务器的 Hash 值,并记录,这就是它们在 Hash 环上的位置
3.对于每个图片,先根据 key 的 hashcode 得到它在 Hash 环上的位置,然后在 Hash 环上顺时针查找距离这个 Key 的 Hash 值最近的缓存服务器节点,这就是该图片所要存储的缓存服务器。
这时,当服务器扩容时,少部分的缓存会命中到新的节点上,其他的并不会受影响。反之,当服务器节点被删除时,只会影响之前映射到当前节点的key,转移到顺时针方向的下一个节点。
但是以上的普通hash环会存在一个问题,当节点移除时,原有节点的数据会全量打到一个节点上,导致那个节点的数据倍增,同理,如果节点新增时,只会影响其中一个节点,其他节点的压力并不会移除。
这时,引入了包含虚拟节点的hash环,扩展整个环上的节点数量,可以将每台物理缓存服务器虚拟为一组虚拟缓存服务器,使得 Hash 环在空间上的分割更加均匀。
如图中下方图,node1x代表节点1,依次类推,通过寻找虚拟节点,然后找到对应的真实节点,只要虚拟节点足够多,节点会更均匀的分布在环上。
此时,新增一个节点,所影响的key会均匀的从其他节点转移到新的节点上,同理删除一个节点,节点上的缓存会均匀的转移到其他节点上。
一致性hash也有一些不足的地方:
1.集群方案,hash slot
Redis Cluster 通过分片的方式将整个缓存划分为 16384 个槽,每个缓存节点就相当于 Hash 环上的一个节点,接入集群时,所有实例都将均匀占有这些哈希槽,当需要查询一个 Key 是,首先根据 Key 的 hashcode 对 16384 取余来得到 Key 属于哪个槽,并映射到缓存实例上。
把16384个槽抽象成20个哈希槽位,如果有4个节点,分配的哈希槽为0-4,5-9,10-14,15-19,当新增一个节点时,将每个节点的哈希槽的一部分取出来到第五个节点上,取出0,5,10,15形成新的节点哈希槽。删除也同理
2.中心化
公司使用sentinel的形式,把sentinel当做中心来对redis进行调度
3.去中心化
每个节点都保存完整的哈希槽-节点的映射表。
这样的话,无论向那个节点发出寻找缓存的请求,都会转移到对应的节点上。
hashslot 减少了频繁寻址