Redis集群模式之cluster

背景

使用Redis Sentinel 模式架构的缓存体系,在使用的过程中,随着业务的增加不可避免的要对Redis进行扩容,熟知的扩容方式有两种,一种是垂直扩容,一种是水平扩容。垂直扩容表示通过加内存方式来增加整个缓存体系的容量比如将缓存大小由2G调整到4G,这种扩容不需要应用程序支持;水平扩容表示表示通过增加节点的方式来增加整个缓存体系的容量比如本来有1个节点变成2个节点,这种扩容方式需要应用程序支持。垂直扩容看似最便捷的扩容,但是受到机器的限制,一个机器的内存是有限的,所以垂直扩容到一定阶段不可避免的要进行水平扩容,如果预留出很多节点感觉又是对资源的一种浪费因为对业务的发展趋势很快预测。Redis Sentinel 水平扩容一直都是程序猿心中的痛点,因为水平扩容牵涉到数据的迁移。迁移过程一方面要保证自己的业务是可用的,一方面要保证尽量不丢失数据所以数据能不迁移就尽量不迁移。针对这个问题,Redis Cluster就应运而生了

概念

Redis3.0推出了Redis-Cluster集群模式,采用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接,主从复制机制依旧是采用原有的机制(master-slave)。

原理

Redis cluster首要解决的就是需要将数据集按照分区规则,划分到不同的节点上,即每个节点是数据集的一个子集。Redis Cluster采用哈希分区规则中的虚拟槽分区。虚拟槽分区巧妙地使用了哈希空间,使用分散度良好的哈希函数把所有的数据映射到一个固定范围内的整数集合,整数定义为槽(slot)。Redis Cluster槽的范围是0 ~ 16383。槽是集群内数据管理和迁移的基本单位。采用大范围的槽的主要目的是为了方便数据的拆分和集群的扩展,每个节点负责一定数量的槽。Redis Cluster采用虚拟槽分区,所有的键根据哈希函数映射到0 ~ 16383,计算公式:slot = CRC16(key)&16383。每一个实节点负责维护一部分槽以及槽所映射的键值数据。

简单点总结就是:会先根据key,CRC16算法,计算出hash值,然后判断hash值在0~16383的哪个区间,对应划分到节点上面去。

关系图

客户端可以访问任意一个节点的地址,在连接该节点发送请求时,会经历以下步骤:

先用CRC16算法,根据key计算出hash值是否在当前节点的slot范围里面,

如果是的话直接取值返回,如果不是的话则返回MOVED信息,告知客户端路由信息,让客户端可以去对应的节点进行操作。

如果此时正在进行集群扩展或者缩空操作,当客户端向正确的节点发送命令时,槽及槽中数据已经被迁移到别的节点了,就会返回ask,这就是ask重定向机制

两者都是客户端重定向 moved异常:槽已经确定迁移,即槽已经不在当前节点 ask异常:槽还在迁移中

 

同Sentinel 一样,Redis Cluster 也具备一套完整的故障发现、故障状态一致性保证、主备切换机制

1.故障发现

依旧是分为主观下线客观下线2种。

主观下线:某节点标识另外某节点不可用。流程如下

①节点A发送ping命令给节点B。

②发送成功,表示节点B正常运行,节点A更新与节点B的最后通信时间点。

③发送失败,在下个定时任务周期时,依旧发送ping命令。

④最后通信时间超过配置时间时,则节点A把节点B标记为pfail状态。

客观下线:当半数以上持有槽的主节点都标记某节点主观下线时,可以保证判断的公平性,集群模式下,只有主节点(master)才有读写权限和集群槽的维护权限,从节点(slave)只有复制的权限。流程如下

①某个节点接收到其他节点发送的ping消息,如果接收到的ping消息中包含了其他pfail节点,这个节点会将主观下线的消息内容添加到自身的故障列表中,故障列表中包含了当前节点接收到的每一个节点对其他节点的状态信息。
②当前节点把主观下线的消息内容添加到自身的故障列表之后,会尝试对故障节点进行客观下线操作。

 

2.slave选举

假设B是A的master,并且B 已经被集群公认是FAIL 状态了,那么A 发起竞选,期望成为新的master。

如果B 有多个slave (A/E/F)都认知到B 处于FAIL 状态了,A/E/F 可能会同时发起竞选。当B的slave 个数 >= 3 时,很有可能产生多轮竞选失败。为了减少冲突的出现,优先级高的slave 更有可能发起竞选,从而提升成功的可能性。这里的优先级是slave的数据最新的程度,数据越新的(最完整的)优先级越高。

slave 通过向其他master发送FAILVOER_AUTH_REQUEST 消息发起竞选,master 收到后回复FAILOVER_AUTH_ACK 消息告知是否同意。slave 发送FAILOVER_AUTH_REQUEST 前会将currentEpoch 自增,并将最新的Epoch 带入到FAILOVER_AUTH_REQUEST 消息中,如果自己未投过票,则回复同意,否则回复拒绝。

3.结构变更通知

当slave 收到过半的master 同意时,会替代B 成为新的master。此时会以最新的Epoch 通过PONG 消息广播自己成为master,让Cluster 的其他节点尽快的更新拓扑结构。

当B 恢复可用之后,它手续爱你仍然认为自己是master,但逐渐的通过Gossip 协议得知A 已经替代了自己,然后降级为A的slave。

 

此外redis cluster还提供一些方法,提高性能和可用性,如单点保护机制

假设A,B两台master节点,都有各自的slave节点,当B的slave节点都挂了,若B也宕机了,将会造成B服务不可用,此时A会将自己其中一台slave节点进行迁移,变成B的slave节点,这样集群中每个master 至少有一个slave,使得Cluster 具有高可用。集群中只需要保持 2*master+1 个节点,就可以保持任一节点宕机时,故障转移后继续高可用。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

最后来讲讲扩充/缩容

在一个稳定的Redis Cluster 中,每个slot 对应的节点都是确定的。在某些情况下,节点和分片需要变更:

①新的节点作为master加入;

②某个节点分组需要下线;

③负载不均衡需要调整slot 分布。

假设A节点的某个槽迁移到B节点,步骤如下:

  1. 向节点B发送状态变更命令,将B的对应slot 状态置为importing
  2. 向节点A发送状态变更命令,将A对应的slot 状态置为migrating
  3. 针对A上的slot 的所有key,分别向A 发送migrate 命令,告知A 将对应的key 迁移到B。

当A节点的状态置为migrating 后,表示对应的slot 正在从A迁出,为保证该slot 数据的一致性。A此时提供的写服务和通常状态下有所区别,对于某个迁移中的slot:

  • 如果Client 访问的key 尚未迁出,则正常的处理该key;
  • 如果key已经迁出或者key不存在,则回复Client ASK 信息让其跳转到B处理;

当节点B 状态变成importing 后,表示对应的slot 正在向B迁入。即使B 能对外提供该slot 的读写服务,但是和通常情况下有所区别:

  • 当Client的访问不是从ask 跳转的,说明Client 还不知道迁移。有可能操作了尚未迁移完成的,处在A上面的key,如果这个key 在A上被修改了,则后续会产生冲突。
  • 所以对于该slot 上所有非ask 跳转的操作,B不会进行操作,而是通过moved 让Client 跳转至A执行。

这样的状态控制,保证了同一个key 在迁移之前总是在源节点执行。迁移后总是在目标节点执行,从而杜绝了双写的冲突。迁移过程中,新增加的key 会在目标节点执行,原节点不会新增key。使得迁移有界限,可以在某个确定的时刻结束。

单个key 的迁移过程可以通过原子化的migrate 命令完成。对于A/B的slave 节点,是通过主备复制,从而达到增删数据。

当所有key 迁移完成后,Client 通过 cluster setslot 命令设置B的分片信息,从而包含了迁入的slot。设置过程中会让Epoch自增,并且是Cluster 中的最新值。然后通过相互感知,传播到Cluster 中的其他节点

 

Redis cluster和Redis sentinel的区别:

①Redis cluster适用于高可用,高性能,以及海量数据的使用场景

②Redis sentinel适用于高可用,高性能,数据量不是特别大的场景。

③Redis cluster由于底层实现机制(slot槽),使得数据都是分散到各个节点,因此不支持批量操作,求交集操作。

④Redis cluster的数据迁移,需要手工操作。