文章目录
前言
在上一篇文章认识redis的cluster(1)中介绍了Redis Cluster 中两个节点之间握手的核心流程,这也是搭建 Redis Cluster 的第一步
:让各个 Cluster 节点之间感知到彼此的存在。
我们知道, Redis Cluster中每个Master节点至少会有一个Slave节点, 所以设置 Redis Cluster 中每个节点的主从角色就是启动 Redis Cluster 第二步
要做的事情。
另外,Redis Cluster 会将它所有的键值对分散到 16384 个 slot 槽位中,而这 16384 个槽位会分配到不同 Master 节点进行管理 ,如何分配 slot 就是启动 Redis Cluster 第三步
要做的事情。
集群配置与slot分配解析
集群配置
在 Redis Cluster 中,各个节点之间相互握手之后,我们还需要进行一些配置操作才能得到一个真正可用的 Redis Cluster 集群:一是设置各个节点之间的主从关系,另一个是为 Master 节点分配 slot。
配置主从关系
在 Redis Cluster 中各个节点之间相互握手之后,我们可以通过 CLUSTER NODES
命令查看它们的状态。下面就是 CLUSTER NODES 在一个 6 节点组成的 Redis Cluster 上的返回:
从这张图中每个 Cluster 节点的 flags 状态信息我们可以看到,这些节点之间没有主从关系,全部都是 Master 类型节点,此时,我们可以通过 CLUSTER REPLICATE <node-id>
命令将当前节点变为另一个节点的 Slave 节点。
<node-id>
参数指定的 Cluster 节点需要是Master节点<node-id>
参数指定的 Cluster 节点不能分配过slot或者已经写入了key到db中
执行后当前节点的变化
-
清理当前节点 flags 中的 MASTER 和 MIGRATE_TO 标记位,并设置 SLAVE 标记位。
-
绑定两个节点的主从关系
一是更新当前节点的 slaveof 字段,指向
<node-id>
指定的这个 Master 节点对应的 clusterNode 实例二是将当前 Cluster 节点对应的 clusterNode 实例,添加到其 Master 节点的 slaves 数组中,并增加其 numslaves 字段值。
-
接下来, 开始与主库建立主从复制连接。后续执行的都是主从复制部分的逻辑了。
我们在上述 6 个 Redis 实例的 6380 实例上,执行 CLUSTER REPLICATE d8b8f1cd26f1ceaa91914c7604dd7e691aa1d1a2
命令之后,再次执行 CLUSTER NODES
命令查看集群状态,就会看到下图主从状态的变化,其中红色部分为发生变更的地方:
此时6380节点变成了6379的从节点。此时Slave 节点的 configEpoch 值也确实和 Master 节点一致了,也就是上图中标红的 4 这个值。
配置完主从节点后, 只是主从两个节点知道了彼此的身份,但是其他 Redis Cluster 节点并不知道这一信息。在上一讲分析 clusterMsg 结构体时候提到,其中会携带发送该消息的 Cluster 节点的一些信息,其中就包含 slaveof 字段,那么当一个 Cluster 节点收到对端节点发来的 PING 等消息时,就可以根据消息中携带的 slaveof 信息,感知到对端节点的主从信息。
接收该变化的节点会有如下三种处理
- 如果对端 Cluster 节点是从 Slave 切换到 Master,则将对端节点的 clusterNode 实例,从其原 Master 节点的 slaves 数组中移除,然后清理对端 Cluster 节点 flags 字段中的 SLAVE 标记位,并设置 MASTER 标记位。
- 如果是对端节点从 Master 切换到 Slave,则将对端节点管理的全部 slot 从其 slots 字段中删除,然后将其 flags 字段中的 Master 标记位切换成 Slave 标记位。
- 还有一种可能是对端节点始终是 Slave 节点,但是其 Master 发生了变更,此时会将对端节点从其原 Master 节点的 slaves 数组中移除,然后添加到其新 Master 节点的 slave 数组中,最后修改对端节点的 slaveof 字段执行新 Master 节点。
slot 的分配与同步
完成主从关系的配置之后,要正常使用 Redis Cluster 读写数据,还需要进行 slot 的分配。Redis Cluster 会将整个集群存储的全部 Key 映射到 16384 个 slot 中,每个 Key 映射到的 slot 是固定不变的。Redis Cluster 中每个 Master 节点只会负责管理一部分 slot,其 Slave 节点与 Master 负责的 slot 相同,只作为主库备份以及读写分离使用。
CLUSTER ADDSlOTS <slot> [<slot> ...]
: 该命令可以批量分配 slot
当节点收到该命令时,将传入的 slot 添加到当前 Cluster 节点对应 clusterNode 实例的 slots 字段中,同时会更新 clusterState->slots 数组中对应的指针,将其指向当前 Cluster 节点自身,最后还会在 cluster->todo_before_sleep 字段中设置 UPDATE_STATE 和 SAVE_CONFIG 两个标志位。
CLUSTER ADDSlOTS
命令执行完成之后,会出现与 CLUSTER REPLICATE
命令类似的情况,那就是只有当前节点自己能够感知到一个 slot 分配给了自己,而其他节点并感知不到这一信息。我们还是要再来审视一下 clusterMsg 结构体,其中包含了一个 myslots 字段,也就是消息发送节点负责管理的 slot 集合,那么当前 Cluster 节点就可以通过 PING 等消息,让其他节点感知到其 slot 集合的变化,这部分逻辑的核心步骤如下
- 当一个节点收到 PING 等消息的时候,会先查找到对端节点(sender 节点)所在主从关系中的 Master 节点(sender_master 节点)
- 比较消息携带的 slot 集合与步骤 1 中 sender_master 节点所管理的 slot 集合,如果两者不同,则表示其 slot 发生了变更
- 如果 sender 节点是 Master 且其管理的 slot 发生了变更,则查找当前节点所在主从关系中的 Master 节点,这里使用 curmaster 记录该节点。然后遍历全部 16384 个 slot,并结合消息携带的 slot 信息,针对每个 slot 进行下面的判断。
- a. 如果该 slot 已经被 sender 节点管理,则无需处理,直接跳过该 slot
- b. 如果该 slot 正在从另一个节点迁移到当前节点,也无需处理,直接跳过该 slot。
- c. 在当前节点视角中,没有任何一个节点声称负责管理该 slot,这就是前文介绍的 Redis Cluster 初始化分配 slot 的场景,此时可以直接将该 slot 分配给 sender 节点进行管理。
- d. 在当前节点视角中,负责管理该 slot 的是当前节点本身,且当前节点还有该 slot 的 Key 存在,这就与 sender 节点告诉我们的信息冲突了,因为 sender 节点告诉我们:该 slot 应该由 sender 节点负责管理。此时,如果 sender 节点拥有了更大的 configEpoch 值,说明当前节点是下线后又重新上线的旧 Master 节点,新一任 Master 为 sender 节点。
- e. 在当前节点视角中,负责管理该 slot 的是当前节点的 Master 节点(curmaster,即当前节点所在主从关系中的 Master),但是 sender 节点告诉我们说它已经负责管理该 slot 了,且 sender 节点拥有了更大的 configEpoch 值。 说明当前节点是下线 Master 的其他从节点(sender 节点已经提升为新一任 Master 了)。
- f. 在当前节点视角中,该 slot 不是由当前节点所在主从复制组管理,当前节点只需要更新该 slot 的分配关系即可。
在场景 c、f 中,直接修改当前节点自身的 slot 视图,将该 slot 分配给 sender 节点管理即可。
而在 d、e 场景中,除了修改 slot 归属节点之外,还要将当前节点变更为 sender 节点的 Slave 节点,如果当前节点本身是个 Slave 节点直接切换就好了,但如果当前节点是一个下线又上线的旧 Master 节点,我们需要先清理掉其管理的全部 slot 以及其中全部 Key,才能将其降级为 sender 节点的 Slave。
- 在步骤 3 中处理的主要是 sender 节点拥有更大 configEpoch 的场景,还有一种场景是:对某个变更的 slot 来说,在当前节点 slot 分配视图中记录的该 slot 管理节点的 configEpoch 值,比 sender 节点的 configEpcoh 更大。这种场景下,需要向对端的 sender 节点发送 UPDATE 消息。
UPDATE 消息与 PING 消息不同之处主要在于 data 字段的取值,UPDATE 消息的 data 字段指向了一个 clusterMsgDataUpdate 实例,其中记录了指定节点的名称、configEpoch 以及负责的 slot 集合。在上述步骤 4 中,当前节点就会将其 slot 分配视图中,与 sender 节点冲突的节点信息,填充到 clusterMsgDataUpdate 实例中,然后发送给 sender 节点。
sender 节点收到UPDATE消息后, 会解析消息中的 clusterMsgDataUpdate,确认其中携带的 configEpoch 值大于当前节点的 configEpoch 值之后,会更新其 configEpoch 值,并调用上面介绍的1、2、3步流程
定时 PING 消息
ping消息除了上一篇文章认识redis的cluster(1)介绍的握手之外, 还承担着探活的任务。发送方式主要分为随机探活以及超时探活。
随机探活
是指一个节点会每隔一秒从自己的 clusterState->nodes 节点列表中,随机选取 5 个节点,然后从其中选出一个符合下列条件的节点发送 PING 消息。
- 该节点与当前节点已建连,也就是这个节点对应的 clusterNode->link 字段不为 NULL。
- 该节点已经响应了当前节点的全部 PING 消息,也就是其 ping_sent 字段值为 0。
- 该节点是这 5 个节点中最长时间没有收到 PONG 回复的节点,即 pong_received 最小。
超时探活
是指每次定时任务执行都会检查所有节点的 PONG 回复时间距今是否超过了 cluster_node_timeout 配置值的 1/2,如果超过了,会立刻发送 PING 消息进行探活。
PING消息的探活处理
- 根据 PING 消息中携带的对端 Cluster 节点名称,从当前节点的 clusterState->nodes 节点列表中查找对应的 clusterNode 实例。如果对端节点是一个未知节点,则 PING 消息没有什么过多的处理逻辑,内容基本就被忽略了。
- 接下来,将 sender 节点中的 data_received 字段更新为当前时间,记录最后一次收到对端 Cluster 节点消息的时间。
- 然后,解析 PING 消息中携带的 currentEpoch 、configEpoch、repl_offset 三个值,并更新到相应字段。以 currentEpoch 值的更新为例,如果 PING 消息携带的 currentEpoch 值比当前节点的 currentEpoch 值大,则更新当前节点 clusterState->currentEpoch 字段。configEpoch、repl_offset 值的更新逻辑类似。
- 向 sender 节点返回 PONG 消息。
- 更新 sender 节点的 flags 标记。这里是保留 sender->flags 字段中标记位的同时,新增 PING 消息携带的 flags 标记位。另外,还会设置 NOFAILOVER 标记位,毕竟我们收到了 sender 节点的心跳,也就认为它没有发生故障。
- 检查 sender 节点的 ip、port 是否发生变更。进行下面的操作:
- 更新到对应 clusterNode 实例的 ip、port 字段,记录 sender 节点的新网络地址。
- 释放 clusterLink 连接,等待下次定时任务执行时使用新 ip、port 建连。
- 如果 sender 节点是当前 Cluster 节点的 Master,则更新 redisServer.masterhost 和 masterport 字段记录新地址,然后断开主从复制连接,并重新建连。
- 最后会在 clusterState->todo_before_sleep 字段中设置 SAVE_CONFIG、UPDATE_STATE 标志位,尽快更新集群状态并持久化 nodes.conf 文件。
- 检查 sender 节点的主从关系是否发生变化,这部分逻辑在前文“配置主从关系”中已经详细分析过了,这里不再重复。
- 检查 sender 管理的 slot 集合是否发生变化。这部分逻辑在前文“slot 的分配与同步”中已经详细分析过了,这里不再重复。
- 处理 configEpoch 冲突。
- 解析 PING 消息中携带的 clusterMsgDataGossip 数组。
解析 clusterMsgDataGossip
通过前文介绍我们知道,无论是 MEET、PING 还是 PONG 消息,它们的 data 字段部分携带的都是一个 clusterMsgDataGossip 数组,其中包含的数据是发送该消息 Cluster 节点从自身 clusterState->nodes 列表中,筛选出的 1/10 已知节点的信息,以及它视角下的全部疑似故障节点的信息。
在 Redis Cluster 节点处理这三类消息的时候,都会遍历消息中携带的 clusterMsgDataGossip 数组,尝试从 clusterState->nodes 节点列表中查找每个 clusterMsgDataGossip 元素对应的 clusterNode 实例,然后进行下面的处理。
-
如果查找不到 clusterMsgDataGossip 对应的 clusterNode 实例,表示感知到一个全新的未知节点。当前 Cluster 节点会创建一个新的 clusterNode 实例,并添加到 clusterState->nodes 列表中,表示感知到了这个 Cluster 节点。在后续定时任务中,会尝试与该新节点进行建连、握手等操作。
-
如果找到了 clusterNaode 实例,表示该节点是一个已知节点,会根据 clusterMsgDataGossip 更新该 clusterNode 的相关信息。
-
首先是检查该节点是否发生故障,这是根据 clusterMsgDataGossip 携带的 flags 字段是否包含 FAIL、PFAIL 状态进行判断的。如果发生故障,会将发送消息的 sender 节点添加到该故障节点对应的 clusterNode->fail_reports 列表中,具体含义是,当前节点感知到 sender 节点认为该节点故障了。在当前 Cluster 节点感知到半数以上节点认为该节点宕机时,就会将该节点从 PFAIL 状态切换成 FAIL,并立刻向所有节点广播 FAIL 消息,快速让其他节点感知到该节点的宕机。
如果 clusterMsgDataGossip 消息携带的 flags 字段告诉我们这个节点没有发生故障,会将发送消息的 sender 节点,从该节点的 clusterNode->fail_reports 列表中删除(如果有)。
-
接下来,是尝试延后该节点对应的 pong_received 值。如果没有其他节点认为该节点出现宕机(即其对应的 fail_reports 列表为空),且该节点已经及时响应了当前节点的全部 PING 消息,说明这是一个正常的节点,不用频繁地发送探活消息了,这里会尝试延后该节点的 pong_received 字段值,进而延迟下次向该节点发送 PING 消息的时间。
-
最后是更新节点的 ip、port 信息。如果当前 Cluster 节点认为该节点发生了故障,而 sender 节点认为其未发生故障,并且该节点在 sender 节点和当前节点的感知中 ip、port 不同,那可能是当前节点感知到的 ip、port 已经过期了,当前节点会更新 ip、port,并在之后的定时任务中重新连接该节点。
-
这里简单说一下 FAIL 消息,它也是由 clusterMsg 结构体表示,其中 data 字段中包含了一个 clusterMsgDataFail 实例,其中只记录了发生故障节点的名称。在 Cluster 节点收到 FAIL 消息之后,在故障节点的 flags 字段中设置 FAIL 标记位。
redis cluster中的failover原理
到此, redis集群分配好了, slot槽位也分配好了, 可以开始对外提供服务了。在提供服务的期间,Redis Cluster 中可能会因为网络、磁盘、内存等各种方面的问题,导致其中某些 Master 节点出现不可用的情况。这个时候,就需要 Redis Cluster 进行自动故障转移,将 Slave 节点提升为 Master 节点继续对外提供服务,保证整个 Redis Cluster 集群的高可用。
Redis Cluster 中的 failover 分为自动 failover
和手动 failover
,自动 failover 是由 Redis Cluster 通过自身的探活机制发现宕机而触发的,手动 failover 则是由客户端发送 CLUSTER FAILOVER 命令触发的。
自动 failover
通过前文对消息中 clusterMsgDataGossip 数组的分析我们知道,有 PFAIL
和 FAIL
两种表示节点故障的状态位。在 Redis Cluster 会周期性地检查每个节点的 ping_sent、data_received 字段,确认多长时间没有收到其发来的数据,如果超过 cluster_node_timeout 配置的时长(默认 15 秒),会认为该节点已经不可达,会在该节点对应的 clusterNode->flags 标记中设置 PFAIL 标志位。在后续发送 PING、PONG 等消息的时候,当前节点会将有 PFAIL 标志位的全部节点添加到 clusterMsgDataGossip 数组中,通知到其他节点。
在当前节点感知到半数以上的节点都认为一个节点处于 PFAIL 的时候,就会广播 FAIL 消息,让所有节点快速感知到该节点可能宕机的情况。但是 PFAIL 状态并不是不可逆的,当前节点如果收到了对端节点的 PONG 消息时,就会立刻清理掉对端节点 clusterNode->flags 字段中的 PFAIL 状态。
预处理
当一个 Slave 节点发现其 Master 节点被标记为 FAIL 状态之后,会在定时任务中执行时,尝试主动发起一次 failover 操作,执行 failover 有三方面的前置条件。
第一方面要检查当前节点视角下 Master 节点的状态
- 当前 Cluster 节点本身是一个 Slave 节点。
- 在当前节点视角中,Master 节点有 FAIL 标记位,或者当前正在进行手动 failover。
- 当前 Cluster 节点的 Master 节点至少负责管理一个 slot。如果 Master 节点不管理任何 slot,也就不会对外提供服务,没必要进行 failover。
第二方面要检查当前 Slave 节点的主从复制情况,如果 Slave 节点能够提升为新一任 Master,需要 Slave 节点中的数据尽可能和 Master 一致。我们可以从当前 Slave 节点是否长时间未与 Master 交互来进行判断,这个时间使用 data_age 进行记录。
第三方面是计算 failover 操作启动的时间点,也就是 clusterState->failover_auth_time 字段。只有到了该时间点时,当前 Slave 节点才能开始执行后续 failover 操作。
failover 操作
salve节点的视角下
- 递增 currentEpoch 加一,让整个 Redis Cluster 进入新纪元,并将其赋值给 failover_auth_epoch 字段。
- 向其他节点发送 FAILOVER_AUTH_REQUEST 消息。如果是手动发起的 failover 操作,会设置一个 FLAG0_FORCEACK 标记,让收到消息的节点必须回复。
- FAILOVER_AUTH_REQUEST 消息发送完成之后,当前节点会将 failover_auth_sent 设置为 1,防止重复发送,在此次 failover 操作超时之后,会将 failover_auth_sent 重置为 0。因为递增了 currentEpoch 值,这里还会在 todo_before_sleep 中添加 SAVE_CONFIG、FSYNC_CONFIG 持久化 nodes.conf 配置文件。
下面我们转换视角到接收 FAILOVER_AUTH_REQUEST 消息的节点,在节点收到 FAILOVER_AUTH_REQUEST 消息之后,先会更新 currentEpoch 等信息,这些公共操作就不再重复了。然后会检查下面这些条件是否成立:
- 接收到消息的当前节点是一个 Master 节点,而发送 FAILOVER_AUTH_REQUEST 消息的节点(即发起 failover 操作的节点)是一个 Slave 节点。如果是 Slave 节点接收到了 FAILOVER_AUTH_REQUEST 消息,会直接忽略调用这条消息。
- 检查 epoch 信息。一个是保证 FAILOVER_AUTH_REQUEST 消息携带的 currentEpoch 必须比所有已知节点的 currentEpoch 值大,另一个是保证 FAILOVER_AUTH_REQUEST 消息携带的 configEpoch 是该主从复制组中最大的。
- 检查当前节点最后一次投票的纪元值(也就是 lastVoteEpoch 字段)是否与当前 currentEpoch 相等。在同一个 Epoch 中,每个 Master 节点内只投票一次,投票完成之后会将 lastVoteEpoch 设置为 currentEpoch。如果当前接收消息的 Master 节点已经在这个 epoch 中投过票了,就不会再次投票。
- 在一段时间(cluster_node_timeout * 2)内,每个 Master 节点只能为一个 Master 节点的 failover 投票一次。也就是说,一个 Master 频繁地 failover,当前 Master 也会拒绝投票。clusterNode 中的 voted_time 字段就是用来记录上次投票时间戳的,每次投票结束之后,Cluster 节点都会将当前时间更新到该字段,然后在下次投票时进行检查。
只有通过上述全部检查,当前 Master 节点才会向发起 failover 操作的 Slave 节点返回 FAILOVER_AUTH_ACK 消息完成投票。同时,还会更新 lastVoteEpoch、voted_time 字段,防止重复投票。
下面我们再将视角转回到发起 failover 操作的 Slave 节点,该节点会处理收到其他 Master 节点返回的 FAILOVER_AUTH_ACK 投票消息。它会先进行前置条件检查,确保投票节点为 Master、投票节点至少负责管理一个 slot 槽位、投票节点的 currentEpoch 值合理。然后才会将 failover_auth_count 值加一,表示自己收到一张投票;最后在 todo_before_sleep 字段中设置 HANDLE_FAILOVER 标识。
当前节点在定时检查当前 Slave 获取到的票数,如果超过半数,就将当前 Slave 节点提升为 Master。步骤如下:
- 将当前节点从上一任 Master 节点的 clusterNode->slaves 列表中删除。更新当前节点中的 flags 标记位,删除 SLAVE 标记位,添加 MASTER 标记位。
- 断开之前的主从复制连接,并完成释放 client 相关资源、设置 replid2 等一系列善后操作。
- 修改当前节点的 slot 分配视图。将原本由上一任 Master 管理的 slot 全部分配给当前节点。
- 更新集群状态以及 nodes.conf 配置文件。每次有节点发生 failover 的时候,都需要调用重新检测集群状态,保证每个 slot 都有节点负责。
- 广播 PONG 消息,通知其他节点此次 failover 操作的结果。
- 如果是手动触发的 failover 操作,这里会清理相关字段
集群状态检查
到此为止,自动 failover 的核心流程就结束了。
最后,我们展开介绍一下如何判断当前整个 Redis Cluster 是否可用的,该函数判断的结果会记录到 clusterState->state 字段中。在每次执行命令之前,都需要检查 clusterState->state 字段,也就是如果当前 Redis Cluster 是否可用,如果不可用,那么当前节点不会对外提供任何读写能力。
只要是感知到 Redis Cluster 节点发生变化,就会进行集群状态检查。例如,感知到某个节点进入 PFAIL 或是 FAIL 时、感知到节点的 ip 地址发生变更时或者感知到某个 Master 节点降级为 Slave 时以及切换自身 Master 等
手动 failover
在有的场景中,我们是有目的地、明确地要求某个 Master 下线,比如,我们要给某个服务器添加磁盘或者硬盘,需要这个服务器上运行的 Master 节点下线。这个时候,我们就可以手动执行 CLUSTER FAILOVER
命令,指定其他机器上的某个 Slave 节点成为新 Master,继续对外提供服务。
CLUSTER FAILOVER
命令支持 DEFAULT
、FORCE
、TAKEOVER
三种模式,下面我们以 DEFAULT 模式作为主线进行分析,最后说明 FORCE、TAKEOVER 两种模式的核心区别。
下面是手动 failover 执行的核心逻辑如下。
- 在一个 Slave 节点收到
CLUSTER FAILOVER
命令时,会立刻计算此次手动 failover 的超时时间(默认 5 秒超时)并记录到 mf_end 字段中。在后续定时任务中,会检查是否到达 mf_end 时间点,一旦到达,就会停止手动 failover 流程,并清空所有相关字段。 - 当前 Slave 节点会向其 Master 节点发送 MFSTART 消息。
- Master 节点在收到 MFSTART 消息之后,会暂停所有与 Master 交互的 client(暂停时长为 10 秒),然后向发起手动 failover 的 Slave 节点返回 PING 消息。这次 PING 消息的主要目的是向 Slave 节点返回 Master 当前的 Replication Offset 值。
- Slave 节点收到 PING 消息之后,会将其中携带的 Master Replication Offset 记录到 mf_master_offset 字段中。
- 检查当前 Slave 节点的 Replication Offset 是否已经追上了 Master 节点,如果追上了,会将当前节点的 clusterState->mf_can_start 设置为 1,表示可以进行主从切换。
之后,当前 Slave 节点就会进入前文介绍的自动 failover 流程,注意,因为上面介绍的手动 failover 流程已经完成了 Replication Offset 的对齐,所以就无需 Slave 节点排序、延迟到 failover_auth_time 时间点执行等一系列操作,而是当前 Slave 节点会直接发起 failover。
如果 CLUSTER FAILOVER
命令中携带了 FORCE 参数,Slave 节点不会再执行上述 2~5 步操作,来追平主从同步的 Replication Offset,而是直接将 mf_can_start 字段设置为 1,然后发起 failover。
如果 CLUSTER FAILOVER
命令中携带了 TAKEOVER 参数,Slave 节点不会进行上述所有 failover 操作,即没有对齐 Replication Offset 或选择最佳 Slave 节点的过程,也不会让其他 Master 节点进行投票,而是直接生成新的 currentEpoch 和 configEpoch 值,然后将自己提升为 Master 节点,并广播 PONG 消息来通知其他节点此次 failover 操作的结果。
TAKEOVER
和 FORCE
这两种模式因为没有对齐 Replication Offset,可能会导致数据丢失。例如,我们指定的 Slave 节点并不是最优的从库,而是一个 Replication Offset 落后很多的从库,在它提升为 Master 节点之后,就会导致其他从库进行全量同步,导致部分数据丢失。
节点迁移与数据迁移
节点飘逸
redis的定时任务会检查Master节点的单点问题。所谓“单点 Master 问题”意思就是:一个 Master 节点下没有任何可用的 Slave 节点存在,如果此时 Master 节点发生了故障,整个 Redis Cluster 将进入不可用的状态。
为了解决这个问题,Redis Cluster 提供了 Slave 节点漂移的功能,redis.conf 配置文件中的 cluster-allow-replica-migration 配置项为该功能的开关。Slave 节点漂移的核心原理是:当 Redis Cluster 发现单点 Master 的时候,会从其他拥有多个可用 Slave 的 Master 节点那里,借用一个 Slave 节点,从而解决单点 Master 的问题。
如下图左侧所示,Master1 节点处于单点状态,通过将 Slave2 节点漂移成 Master1 节点的 Slave,解除了其单点状态。
每个 Slave 节点在执行定时任务的时候,都会对每个 Master 节点的 clusterNode->slaves 列表进行检查,计算其可用的 Slave 节点数量。如果发现 Master 节点下没有可用的 Slave 节点,且当前 Master 负责管理多个 slot,则会将其判定为单点 Master。在计算单点 Master 的同时,还会计算 max_slaves、this_slaves 两个辅助变量,max_slaves 记录了当前可用 Slave 节点数的最大值,this_slaves 记录了当前这个主从复制组中的可用 Slave 节点数。
如果当前 Slave 在上述检查中发现了单点 Master,且当前 Slave 节点所在的主从复制组中可用的 Slave 节点数最多,则当前节点可以发起 Slave 节点漂移。首先检查整个 Redis Cluster 的状态,在 Redis Cluster 状态正常的情况下,才会进行后续的漂移操作。在 redis.conf 配置文件中,有个 cluster-migration-barrier
配置项,它指定了每个 Master 至少要有多少个可用的 Slave 节点才算安全。这里会检查当前主从复制组中可用的 Slave 节点数量是否超过了cluster-migration-barrier
配置项指定的阈值(默认值为 1),如果没有超过该阈值,则无法继续外迁 Slave 节点。
下面简单描述一下 Slave 漂移的核心流程。
- 首先,确定要进行漂移的候选者。这里会迭代 clusterState->nodes 列表来查找有单点问题的 Master,然后从有最多 Slave 节点的主从复制组中,查找 name 最小的 Slave 节点作为漂移的候选者。
- 假设当前 Slave 节点就是 Slave 漂移的候选节点,此时,如果存在单点 Master 节点,并且其单点状态持续了 5 秒以上,就可以将当前 Slave 节点切换成单点 Master 的 Slave 节点,从而解除该 Master 的单点问题。将当前 Slave 节点从原 Master 节点的 slaves 列表迁移到新 Master 的 slaves 列表中,然后与新 Master 节点建立连接,开始一次全量的主从复制。
扩容缩容的slot 迁移
在 Redis Cluster 上线运行一段时间之后,可能无法继续支持业务的流程增长,这个时候,就需要对 Redis Cluster 进行扩容
,向 Redis Cluster 中新增一批节点,使整个 Redis Cluster 集群能够存储更大的数据量,支持更高的 QPS。在添加完新节点之后,我们需要将原有 Master 节点中负责管理的一部分 slot ,迁移到新增加的 Master 节点。
除了扩容的场景,Redis Cluster 缩容
的场景也是可能出现的,比如下线一部分 Master 节点,此时我们就需要将下线 Master 节点负责的 slot 迁移到其他 Master 节点中。
无论是上述哪种场景,都会涉及到 slot 以及其中数据的迁移,此时就需要使用到 CLUSTER SETSLOT
命令。这里我们举个例子,假设需要将编号为 100 的 slot 从节点 A 迁移到节点 B,需要依次执行下面的步骤:
- 在节点 B 中执行
CLUSTER SETSLOT 100 IMPORTING A-name
命令。 - 在节点 A 中执行
CLUSTER SETSLOT 100 MIGRATING B-name
命令。 - 之后,在节点 A 上执行
CLUSTER GETKEYSINSLOT 100 {count}
命令,从 slot 100 中获取 count 个 Key,并执行MIGRATE B-host B-port "" 0 1000 KEYS key [key ...]
将上述获取到的 Key 从节点 A 迁移到节点 B(DB 的编号由参数 0 指定,1000 则是超时时长,单位为毫秒),循环该过程,直至 slot 100 中的全部 Key 都迁移到节点 A 中。 - slot 100 中全部的 Key 都迁移完成之后,需要依次在节点 B 和节点 A 上都执行
CLUSTER SETSLOT 100 NODE B-name
命令,明确 slot 100 已经不再由 A 节点负责管理,而是由 B 节点负责管理。之后,slot 100 的变更将会随着 PING 等消息传播到整个 Redis Cluster。
IMPORTING、MIGRATING 状态
了解了 slot 迁移的基本操作之后,下面我们展开介绍一下这些命令底层分别执行了哪些逻辑。
1、首先是 CLUSTER SETSLOT 100 IMPORTING A-name
命令,这条命令会修改节点 B 的视图,将 slot 100 设置为 IMPORT 状态,其实就是将节点 B 的 clusterState->importing_slots_from[100]
指向节点 A。
2、然后,是 CLUSTER SETSLOT 100 MIGRATING B-name
命令,它是在节点 A 上执行的,它会修改节点 A 的视图,将 slot 100 设置为 MIGRATING 状态,其实就是将 clusterState->migrating_slots_to[100]
指向节点 B。
之所以执行设置这两个状态,是为了处理后续 slot 迁移过程中收到的客户端请求。如果我们迁移过程中,一个客户端来请求 slot 100 中的 Key1,当 GET Key1 命令发给节点 A 时,节点 A 在命令执行之前,会检查 slot 100 的状态:
该函数会发现 slot 100 处于 MIGRATING 状态,如果访问的目标 Key 1 还在节点 A 中,则继续后续的访问操作。如果访问的目标 Key 1 已经被迁移到了节点 B 中,则返回 ASK 错误
以及节点 B 对应的 clusterNode 实例
,最终返回给客户端的是 ASK 错误以及节点 B 的 ip 和端口,客户端在收到 ASK 错误之后,会去节点 B 访问目标 Key1。
接下来,客户端会先向节点 B 发送 ASKING
命令,该命令会在对应的 client 上添加 CLIENT_ASKING 标记。然后,客户端才会发送原来访问 Key1 的命令。节点 B 会发现 slot 100 处于 IMPORTING 状态,正常执行后续访问逻辑.
ASKING命令
有的同学可能会问,为什么客户端在访问节点 B 的时候,需要先发送 ASKING 命令呢?直接发送原始的访问命令不可以吗?
如果客户端不提前发送一条 ASKING 命令来设置 ASKING 状态,那么无论 Key 1 是否已经迁移到了节点 B,节点 B 都将返回给客户端一个 MOVED 错误。
这里简单区分一下 MOVED 和 ASKING 两个错误。
- MOVED 表示的是 slot 已经从一个节点转移到了另一个节点。在 Jedis、redis-cli 等客户端中,都会缓存一份 slot 与 Redis Cluster 节点的映射关系,当收到 MOVED 错误时,会修改该缓存,之后访问该 slot 的请求会直接发送到 MOVED 错误所指定的目标节点。
以 redis-cli 为例,如果我们要让它实现自动处理 ASK 和 MOVED 的功能,需要在启动 redis-cli 客户端的时候,添加 -c 参数如下所示:
./redis-cli -h 127.0.0.1 -p 6381
127.0.0.1:6381> GET key1
# 自动处理MOVED和ASK命令
-> Redirected to slot [9189] located at 127.0.0.1:6382
"value"
ASKING 表示的是 slot 迁移过程中产生的中间态。在客户端收到 ASKING 错误时,不会修改缓存,所以只是影响 ASKING 响应的这条请求,不会影响后续其他的请求。如果客户端之后还需要访问该 slot,则仍然会按照缓存将请求发送到目前负责该 slot 的节点,可能还会触发 ASKING 错误。
这从另一个角度说明,client 的 ASKING 状态是一个一次性标状态,当节点执行完一条非 ASKING 命令之后,ASKING 状态就会被清除。
迁移 Key
分析完 CLUSTER SETSLOT 100 IMPORTING A-name
、 CLUSTER SETSLOT 100 MIGRATING B-name
两条命令的底层原理以及对数据访问带来的影响之后,我们再来分析 CLUSTER GETKEYSINSLOT 100 {count}
命令和 MIGRATE B-host B-port "" 0 1000 KEYS key [key ...]
迁移 Key 的实现逻辑。
首先,当节点接收到 CLUSTER GETKEYSINSLOT
命令时,会先去 redisDb->slots_to_keys 中查找指定 slot 中 Key 的个数,然后从相应的 by_slot 列表中获取指定数量的 Key,最后将这些 Key 返回给客户端。
客户端拿到 CLUSTER GETKEYSINSLOT
返回的一批 Key 之后,就可以通过 MIGRATE
命令进行迁移了。MIGRATE 命令本身有非常多参数,当节点 A 接收到 MIGRATE
命令的时候,会先对其参数进行校验,例如,会检查命令中指定的 Key 是否还存在于当前 DB 中,至少存在一个 Key 才会执行后续的迁移操作。完成 MIGRATE 命令参数的校验之后,当前节点会根据命令参数中指定的 ip、port,与迁移的节点 B 建立连接。
建连完成之后,节点 A 就可以开始组装迁移 Key 的相关命令。
- 首先创建一个基于 Buffer 的 io 实例,后续需要发送到节点 B 的命令会先组装到该 Buffer 中。
- 向 Buffer 中写入 SELECT 命令,将迁移 Key 写入到节点 B 的指定 DB 中。
- 接下来循环待迁移的 Key,为每个 Key 生成一条
RESTORE-ASKING
命令(集群模式下使用 RESTORE-ASKING 命令,单机模式下使用 RESTORE 命令)。这里先会检查 Key 的过期时间,如果已经过期,直接跳过该 Key。然后才会真正向 Buffer 中写入的是 RESTORE-ASKING 命令,该命令的具体格式是RESTORE-ASKING key ttl serialized-value
,其中的序列化的 Value 值是按照 RDB 文件的格式,将 Value 值写入到 Buffer 中的。
迁移相关的命令全部写入到 Buffer 之后,节点 A 就可以将 Buffer 中的命令发送到节点 B ,注意,这里使用的同步方式进行发送,超时时间是 MIGRATE
命令中指定的,默认是 1000 毫秒。
在节点 B 收到 RESTORE-ASKING
命令之后,会反序列化 Key、Value 值以及过期时间,然后将 KV 数据写入到指定 DB 中,并设置相应过期时间。如果节点 B 有冲突的 Key,则根据 RESTORE-ASKING 命令的相关参数确定是覆盖原有 Key 还是报错。
回到节点 A 这边,在发送完 Buffer 中的命令之后,它就会阻塞等待节点 B 对每条 RESTORE-ASKING 命令的响应,对于迁移成功的 Key,节点 A 会将该 Key 从自身的 DB 中删除;对于迁移失败的 Key,节点 A 会将节点 B 返回的错误信息透传给客户端。
更新 slot 归属
完成 Key 迁移之后,我们就可以依次在节点 B 和节点 A 上执行 CLUSTER SETSLOT 100 NODE B-name
命令,变更 slot 100 的归属权了。
节点 B 接到 CLUSTER SETSLOT 100 NODE B-name
命令的时候,会执行下面的 slot 迁移逻辑。
- 修改自身维护的 slot 视图,将 slot 100 与节点 A 解绑,并将 slot 100 修改为节点 B 负责管理。
- 将 slot 100 的 IMPORTING 状态清理掉,也就是将 clusterState->importing_slots_from[100] 设置为 NULL。
- 因为有 slot 的变更,所以 currentEpoch 和 configEpoch 值都需要增加。这里会将 currentEpoch 的值增加 1,并将其作为自身的最新 configEpoch 值。
- 然后向其他节点广播 PONG 消息,其他节点也就可以更新到最新的 currentEpoch 和 configEpoch 值,同时也会变更 slot 100 的归属关系。
之后节点 B 收到访问 slot 100 的请求时,就可以直接进行响应了。
在节点 A 接到 CLUSTER SETSLOT 100 NODE B-name
命令的时候,会执行下面的操作。
- 如果节点 A 还没有收到来自 PONG 消息时,会发现当前 slot 100 是由节点 A 自己负责管理的,而命令指定的却是节点 B,此时就需要检查在节点 A 中是否还持有 slot 100 中的 Key,如果没有,才能正常执行下面的 slot 迁移操作。
- 将 slot 100 的 MIGRATING 状态清除掉,也就是将 clusterState->migrating_slots_to[100] 设置为 NULL。
- 修改节点 A 中维护的 slot 视图,将 slot 100 与节点 A 解绑,将 slot 100 修改为由节点 B 负责管理。
节点 A 之后收到访问 slot 100 的请求时,就会立刻返回 MOVED 响应,让客户端去访问节点 B,这也是在节点 A 上执行 CLUSTER SETSLOT 命令的主要作用。
使用 Redis Cluster 的注意事项
1、数据倾斜问题
在 Redis Cluster 环境搭建以及 Key 设计的过程中,我们应该尽可能地保证键值对数量以及 Key 的访问量,均匀地散落在不同的 slot 中,同时尽可能保证 slot 均匀地散落在 Redis Cluster 的多个 Master 上,这样就可以避免出现数据量或是访问量的倾斜。
出现数据量倾斜的问题可能是出现了某些大 Key,例如,我们的业务中出现了一部分特别大的 Hash 表,而且这些 Hash 表对应的 Key 都落到了一个 slot 中,这就会导致某个 Redis Cluster 节点内存使用率很高,其他节点的内存使用率很低。而且对大 Key 访问一般耗时会比正常 Key 要长,这也会造成 Redis Cluster 中的某些节点耗时长,影响整个 Redis Cluster 的性能表现。
我们可以通过 redis-cli 命令行工具的 --bigkeys 参数来查询 Redis 中的大 Key,但是要根本解决大 Key 的问题,还是需要在进行 Key 设计的时候对可能的数据模型和数据量进行评估,对可能遇到的大 Key 进行拆分。
另一个导致数据量倾斜的问题就是 Key 或是 HashTag 的设计不当造成的,这里我们先来展开介绍一下计算一个 Key 所属 slot 的核心算法是 crc16 算法,默认整个 Key 都会参与到 slot 的计算中,如下图所示第一组 Key 值所示,它们会散落在不同的 slot 中。
但是如果 Key 中有被大括号包裹起来的部分,如上所示的第二组的 Key 值所示,则只有大括号之内的部分会参与 slot 的计算,这就是所谓的 HashTag,上图第二组 Key 会落到同一个 slot 中。
大量 Key 中的 HashTag 计算出相同的 slot 值,也会导致数据倾斜。一个比较好的方式就是在上线之前预估 Key 分布,然后模拟计算一下 slot 的分布情况。
如果已经出现 slot 数据不均匀的情况,例如,出现了多个非常大 slot ,我们可以手动调整 slot 的分布,将这几个大 slot 归属到不同的 Redis Cluster 节点上,避免大 slot 集中到一起,压垮单个节点。当然,这仅仅是一种补救措施,我们还是应该尽可能让 Key 分布到 slot 中,让 slot 均匀分布到各个节点上。
再来看访问量倾斜
的问题,其实就是热点 Key 的问题,本质上也是 Key 设计的问题,最根本的解决方案就是重新设计一套合理的 Key。
2、Redis Cluster 对 Key 批量操作以及事务等都有一定限制
例如,MSET、MGET 等命令操作的多个 Key 必须要归属于同一个 slot 值的。如下面这个示例,key1 和 key2 归属于不同的 slot ,就会返回异常:
127.0.0.1:6379> MSET key1 v1 key2 v2
(error) CROSSSLOT Keys in request don't hash to the same slot
个人公众号: 行云代码
参考文章
https://juejin.cn/book/7144917657089736743/section/7147530707298942976
https://juejin.cn/book/7144917657089736743/section/7147530737963663395
https://juejin.cn/book/7144917657089736743/section/7147530758368952354