兄弟萌,想必大家学习 Redis 时,都听说过 Redis Cluster 吧,有没有想过这样一个问题,做 Redis 集群时,有了哨兵机制就能够实现监控以及自动故障转移,还要 Redis Cluster 干嘛呢?
下面,我们就深入聊一下 Redis Cluster,废话不多说,Let’s Go!
大家知道,Redis 有三种集群方式:主从复制,哨兵模式和集群 Cluster。
对于主从复制来说,如果 master 出现故障,不会自动恢复,需要人为干预来恢复。
对于哨兵模式来说,其实它就是主从复制的升级版,引入了 Sentinel(哨兵),哨兵的作用就是监控 Redis 系统的运行状况,它的功能包括:
但是,主从和哨兵存在难以扩容以及单机存储、读写能力受限的问题,并且集群之前都是一台 Redis 都是全量的数据,这样所有的 Redis 都冗余一份,就会大大消耗内存空间。
集群模式实现了 Redis 数据的分布式存储,集群通过分片(sharding)来进行数据共享,每个 Redis 节点存储不同的内容,并提供复制和故障转移功能。
我们为了将不同的 key 分散放置到不同的 redis 节点,通常的做法是获取 key 的哈希值,然后根据节点数来求模,但这种做法有明显弊端,当我们需要增加或减少一个节点时,会造成大量的 key 无法命中,所以就提出了一致性 hash 的概念。
但是,Redis 集群没有使用一致性 hash,而是引入了哈希槽的概念。
Redis Cluster 采用哈希槽分区,所有的键根据哈希函数映射到 0~16383 整数槽内,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽,每一个节点负责维护一部分槽以及槽所映射的键值数据。
计算公式:CRC16(key) % 16384
使用哈希槽的好处就在于可以方便的添加或移除节点:
另外,大家有想过这个问题吗,为什么 Redis Cluster 哈希槽的个数要设置为 16384?
CRC16 校验码计算结果是 2 个字节,16 位,所以槽的个数实际上最大取值可以是 2^16 = 65536。
集群扩容通过重新分片来实现。
重新分片可以将已经分配给某个节点的任意数量的 slot 迁移给另一个节点,并且相关槽所属的键值对也会从源节点被移动到目标节点。
重新分片操作可以在线进行,在重新分片过程中,集群不需要下线,并且源节点和目标节点都可以继续处理命令请求。
Redis 集群中的节点分为主节点(master)和从节点(slave),读&写请求其实都是在 master 上完成的,slave 节点只是充当了一个数据备份的角色,当 master 发生了宕机,就会将对应的 slave 节点提拔为 master,来重新对外提供服务。
简单来说,集群中的每个节点都会定期地向集群中其它节点发送 PING 消息,以此来检测对方是否在线,节点正常状态下接收到 PING 消息会返回一个 PONG 消息。针对 A 节点,某一个节点认为 A 宕机了,那么此时是疑似下线。而如果集群内超过半数的节点认为 A 挂了, 那么此时 A 就会被标记为已下线。
一旦节点 A 被标记为了已下线,集群就会开始执行故障转移。其余正常运行的 master 节点会进行投票选举,从 A 节点的 slave 节点中选举出一个,将其切换成新的 master 对外提供服务。当某个 slave 获得了超过半数的 master 节点投票,就成功当选。
故障转移步骤:
当 slave(从节点)发现自己的 master(主节点)不可用时,便尝试进行故障切换,以便成为新的 master。由于挂掉的 master 可能会有多个 slave,从而存在多个 slave 竞争成为 master 节点的过程, 其选举过程如下:
实质上,前面都说的比较笼统,下面我们好好分析一下从 Redis 集群的构建到槽指派过程。
一个 Redis 集群通常由多个节点组成,通过向指定节点发送 cluster meet 命令,可以让与指定节点进行握手,如果握手成功,就可以将指定节点添加到当前节点所在集群中。
通过向节点发送 cluster addslots 命令,可以将一个槽或多个槽指定给节点负责,如:
当以上三个 cluster addslots 命令都执行完毕后,数据库中的 16384 个槽已经被指派给了相应的节点,集群进入上线状态。
一个节点除了会将自己负责处理的槽记录在 clusterNode 结构的 slots 属性和 numslots 属性外,它还会将自己的 slots 数组通过消息发送给集群中其它节点,以此来告知其它节点自己目前负责处理哪些槽。因此,集群中的每个节点都会知道数据库中的 16384 个槽分别被指派给了集群中的哪些节点。
clusterStatue 结构中的 slots 数组记录了集群中所有 16384 个槽的指派信息:
引入 clusterStatue 结构的好处在于:如果我们想要知道槽 i 被指定给了哪个节点,直接访问 clusterState.slots[i] 的即可,操作的时间复杂度为 O(1),如果去遍历 clusterNode 结构中 slots 数组,时间复杂度为 O(N)。
当我们执行 cluster addslots 命令后 cluseterState 的结构:
对数据库中的 16384 个槽进行了指派之后,集群就会进入上线状态,这时客户端就可以向集群中的节点发送数据命令了。
当客户端向节点发送与数据库键有关的命令时,接收命令的节点会计算出命令要处理的数据库属于哪个槽,并检查这个槽是否指派给了自己:
那如何来判断槽是否由当前节点处理?
当节点计算出键所属的槽 i 之后,节点就会检查自己在 clusterState.slots 数组中的项 i,判断键所在的槽是否由自己负责:
在重新进行分片期间,源节点向目标节点迁移一个槽过程中,可能会出现这样一种情况:被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对保存在目标节点里面。
如:假设节点 7002 正在向节点 7003 迁移槽 16198,这个槽包含 “is” 和 “love” 两个键,其中 “is” 还留在节点 7002,而键 “love” 已经被迁移到了 7003。
如果我们向节点 7002 发送关于键 “is” 的命令,那么这个命令就会直接被节点 7002 执行。
而如果我们向节点 7002 发送关于键 “love” 的命令,那么客户端会先被转向至节点 7003,然后再次执行命令。
和接到 MOVED 错误时的情况类似,集群模式的 redis-cli 在接到 ASK 错误时也不会打印错误,而是自动根据错误提供的 IP 地址和端口进行转向动作。如果想看到节点发送的 ASK 错误的话,可以使用单机模式的 redis-cli 客户端。
Redis 集群各节点之间的通讯协议:gossip 协议。
gossip 协议的主要用途就是信息传播和扩散,它的基本思想:一个节点想要分享一些信息给网络中的其它的一些节点。于是,它周期性的随机选择一些节点,并把信息传递给这些节点。这些收到信息的节点接下来会做同样的事情,即把这些信息传递给其他一些随机选择的节点。
集群中的各个节点通过发送和接收消息来进行通信,节点发送的消息主要有以下 5 种:
MEET 消息
当发送者接到客户端发送的 CLUSTER MEET 命令时,发送者会向接收者发送 MEET 消息,请求接收者加入到发送者当前所处的集群中。
PING 消息
集群中每个节点默认每隔 1 秒钟就会从已经节点列表中随机选出 5 个节点,然后对这 5 个节点中最长时间没有发送过 PING 消息的节点发送 PING 消息,以此来检测被选中的节点是否在线。除此之外,如果节点 A 最后一次收到节点 B 发送的 PONG 消息的时间,距离当前时间已经超过了节点 A 的 cluster-node-timeout 选项设置时长的一半,那么节点 A 也会向节点 B 发送 PING 消息,这可以防止节点 A 因为长时间没有随机选中节点 B 作为 PING 消息的发送对象而导致节点 B 的信息更新滞后。
PONG 消息
当接受者收到发送者发来的 MEET 消息或者 PING 消息时,为了向发送者确认消息已到达,接收者会向发送者返回一条 PONG 消息。另外,一个节点也可以通过向集群广播自己的 PONG 消息来让集群中的其它节点立即刷新关于这个节点的认识。
FAIL 消息
当一个主节点 A 判断另一个主节点 B 已经进入 FAIL 状态时,节点 A 会向集群广播一条关于节点 B 的 FAIL 消息,所有收到这条消息的节点都会立即将节点 B 标记为已下线。
PUBLISH 消息
当节点接收到一个 PUBLISH 命令时,节点会执行这个命令,并向集群广播一条 PUBLISH 消息,所有接收到这条 PUBLISH 消息的节点都会执行相同的 PUBLISH 命令。