Redis Cluster

Redis 主从复制 文末提到了一些痛点:

对于第一个痛点,可以通过 Redis Sentinel 来实现自动故障转移;对于如何解决第二个痛点,正是本片文章要介绍的

即然受到了单机的瓶颈,那正常思路就是横向扩展,把数据分散存储到 n 个 Redis 节点中,且没有重复的部分。当客户端要进行读写操作时,可以根据路由规则将请求重定向到集群中正确节点上

同时为了使集群具有高可用性,可以为集群中的每个主节点配置一个或多个从节点,当主节点挂了后可以进行故障转移,这一部分和主动复制 + Sentinel 实现高可用原理一样

通过集群横向扩展后,不仅提高了系统的存储能力和并发量,而且还可以根据实际的情况动态调整集群的节点数量,如:删除节点、添加节点

通过上面的介绍,总结一下 Redis Cluster 的优势:

最后来一张 Redis Cluster 结构图

17

数据分片

数据分片的核心就是将全量数据按照一定规则划分成 n 个子集存储在 m 个节点上

18

Redis Cluster 采用虚拟槽分区,所有的 key 根据哈希函数映射到[0, 16383]整数槽内,计算公式:slot = CRC16(key) & 16383,其中CRC16(key)表示计算 key 的 CRC-16 校验码

每一个节点负责维护一部分槽以及槽所映射的键值对数据,16384 个槽必须都指派给主节点集群才会处于可用状态,也可以通过配置cluster-require-full-coverage no取消该限制

Redis 虚拟槽分区通过引入槽的概念实现了数据和节点的解耦合。当客户端需要写入一个数据,只需要关心该数据应存入的槽,不需要关心存放在哪个节点中,槽和节点的映射由节点自身维护

这样设计的好处在于节点扩容和收缩时非常方便,只需要以槽为单位移动数据即可,而且还能保证扩容或收缩后的数据分布的均匀性

每个节点都保存所有槽与所有节点的映射关系,也就是集群中的任意一个节点都知道某个槽被指派给哪个节点。节点和节点之间还会定时发送ping消息,用于检测节点是否在线以及交换彼此的状态信息

状态信息中就包含了当前节点被指派的槽,从而目标节点就可以更新自己的信息。在一定时间内,集群中的所有节点都会知道其它节点的槽映射,而且是动态更新 (P2P 去中心化通信)

下面给出一些对 slot 的常用命令:

在这部分的最后,抛出一个常见问题:为什么 Redis Cluster 槽的数量是 16384 个?

CRC-16 算法产生的校验码有 16 位,理论可以产生 216=65536 个值,为什么 Redis Cluster 槽的数量是 214=16384 个呢?

在 2015 年,Redis 的作者 antirez 巨佬本人专门回答了这个问题 -> why redis-cluster use 16384 slots? #2576

原因一:集群中节点间会定时发送心跳包检测在线状态以及交换状态信息,状态信息中包含了该节点被指派的槽信息,是一个char myslots[16384/8]数组,每一位的 0/1 表示一个槽的指派状态,该数组大小刚好是 2KB。如果把槽数量设置为 65536 个,那么该数组大小将变为 8KB,这无疑会增加网络带宽和流量的开销

原因二:基于其它设计上的权衡,Redis Cluster 不太可能扩展超过 1000 个主节点,所以 16384 个槽已经够用

原因三:myslots[]记录了当前节点被指派的槽信息,如果槽数量增多,但主节点数不超过 1000,会导致每个主节点指派槽数量增多,进而在传输时,对myslots[]数组的压缩率就越低

一条命令完整执行过程

有了上面的铺垫,我们重构一张更详细的 Redis Cluster 结构图,以及一条命令完整执行的过程图

19

当客户端以单机模式启动:redis-cli -h 127.0.0.1 -p 6379,客户端会直接打印出 MOVED 错误:(error) MOVED 866 127.0.0.1 6380

当客户端以集群模式启动:redis-cli -c -h 127.0.0.1 -p 6379,客户端会直接重定向到目标节点:-> Redirected to slot [866] located at 127.0.0.1 6380

注意:自动重定向后,客户端对应的主节点就被永久改变成 slot 对应的主节点

重新分片

数据分片 部分介绍了如何给主节点指派槽以及如何删除指派给主节点的槽

重新分片是指可以将任意数量已经指派给某个主节点 (源节点) 的槽更改为指派给另一个主节点 (目标节点),并且相关槽所属的键值对也会从源节点移动到目标节点

重新分片操作可以在线进行,也就是在重新分片的过程中,集群不需要下线,并且源节点和目标节点都可以继续处理命令请求

Redis 重新分片操作是由 Redis 集群管理软件 redis-trib 负责执行的,该软件对单个槽进行重新分片的步骤如下:

20

 

在重新分片期间,源节点向目标节点迁移一个槽的过程中,可能槽中部分键值对还在源节点中,部分键值对已经被迁移到目标节点中

此时客户端对源节点发送关于键 key 的命令,会有两种情况:「key 还在源节点中」or「key 已经迁移至目标节点中」

比较 MOVED 重定向和 ASK 重定向

在文章开头提到集群支持动态的扩容和缩容,其实核心就是重新分片。集群扩容就是将已有节点的部分槽迁移到新加入的节点;集群缩容就是将要删除的节点的全部槽均匀迁移至其它节点

节点通信

Redis Cluster 采用 P2P 的 Gossip (流言) 协议,节点彼此不断的通信交换信息,一段时间后所有节点都会知道集群完整信息,类似于流言传播

集群中每个节点都会单独开辟一个 TCP 通道,用于节点之间彼此通信,通信端口号在基础端口号上加 10000

每个节点在固定周期内通过特定规则选择几个节点发送ping消息,接收到ping消息的节点用pong消息作为响应

常用的 Gossip 消息有四种:meet消息、ping消息、pong消息、fail消息

所有的消息格式划分为:消息头和消息体。消息头包含发送节点自身状态数据,接收节点根据消息头就可以获取到发送节点的相关数据;消息体包含发送节点对其它节点的状态数据

接收节点收到meet/ping消息时,执行解析消息头和消息体的流程:

故障转移

注意:在集群中,主节点才负责读写请求和集群槽等关键信息的维护,而从节点仅仅只是主节点数据和状态信息的复制,只用于故障转移时被选举成为新的主节点

Redis Cluster 也可以实现自动故障转移,而且和 Redis Sentinel 的故障转移很相似,但却不见哨兵的影子。因为实现哨兵功能的其实是集群中的主节点,也就是主节点扮演了哨兵的角色

集群中也有 主观下线和客观下线 的概念,但更准确的来说是 pfail (疑似下线) 和 fail (下线)

当集群内一个节点向其它部分节点发送了ping消息,但是在规定时间内没有收到对方的pong回复,就可认为对方处于 pfail 状态,该节点的状态会跟随消息在集群内传播

ping/pong消息的消息体会携带集群 1/10 的其它节点状态数据,当接收节点发现消息中含有 pfail 状态的节点时,会在本地找到该节点,保存到该节点的下线报告链表中

需要注意的是,如果是从节点发送的消息包含 pfail 节点,直接忽略,即不会加入到下线报告链表中;如果下线报告链表中已经有某主节点,也不会加入链表,但是会更新

集群中也没有类似 选举 Sentinel Leader 的概念,Sentinel Leader 主要用于选择一个最优的从节点作为新的主节点,而集群中新的主节点是根据所有主节点在从节点中投票产生的,也就是新主节点的产生和 Sentinel Leader 的产生很相似!

集群中的节点每次接收到其它节点的 pfail 状态时,都会尝试触发 fail,也就是判断下线报告是否有超过一半的主节点数量 (包括下线的主节点),如果成立,该节点会向集群内广播一条fail消息

fail消息有两个作用:

只有主节点才有投票的机会,且每轮只有一次投票机会。当一个从节点获得超过一半主节点的票,那么它就被选举成为新的主节点;如果该轮投票中没有从节点获得超过一半主节点的票,则进行下一轮投票

新当选主节点的从节点会做三件事情:

注意:必须至少有 3 个主节点,否则当一个主节点故障后,无法顺利故障转移。假设主节点数量 N=2,当一个主节点下线后,它的从节点必须获得 N/2+1=2 票才能成为新的主节点,可是集群中只有一个可用的主节点,所以无法顺利故障转移

参考文章