前言
Redis Cluster 是 Redis 3.0 版本正式推出的,用来解决分布式的需求,同时实现了高可用。跟 Codis 不同的是,它是去中心化的,客户端可以连接任意一个 Redis 节点。
Redis Cluster 模式具备如下特点:
操作步骤
参考 redis cluster 集群搭建与重新分片、redis-cli --cluster命令明细、redis cluster 扩容与缩容。
工作流程
主要分析一下 Redis Cluster 模式的工作流程。
一、启动节点
根据配置文件中的 cluster-enabled
选项是否是 yes 来决定是否开启集群模式。
客户端连接到某个 Redis 服务端节点后,向其发送 cluster meet <ip> <port>
命令,尝试将指定 ip、port 的 Redis 服务端节点加入到当前节点所在的集群中。
可以通过 cluster nodes
命令查询当前 Redis 服务端节点所在的集群此时都有哪些服务端节点。
cluster meet 命令的实现
比如客户端向服务端节点 a 发送 cluster meet <ip> <port>
命令,尝试将节点 b 加入到节点 a 所在的集群中。
1、节点 a 会为节点 b 创建一个 clusterNode 结构,并将该结构添加到自己的 clusterState.nodes 字典里面。
2、节点 a 根据 cluster meet 命令指定的 ip、port,向节点 b 发送一条 meet 消息。
3、节点 b 同样会为 节点 a 创建一个 clusterNode 结构,并将该结构添加到自己的 clusterState.nodes 字典里面。
4、节点 b 向节点 a 返回一条 pong 消息。通过这条消息,节点 a 可以知道节点 b 已经成功接收了 meet 消息。
5、节点 a 向节点 b 返回一条 ping 消息。通过这条消息,节点 b 可以知道节点 a 已经成功接收了 pong 消息,至此节点 a 与节点 b 握手完成。
6、节点 a 通过 Gossip 协议将节点 b 的信息传播给集群中的其它节点,让其它节点与节点 b 进行握手,最终经过一段时间后,节点 b 被集群中的所有节点认识。
Gossip协议
Gossip 协议也叫做 Epidemic Protocol(流行病协议),是一种一致性算法。
基本思想是一个节点想要分享一些信息给网络中的其它节点,于是随机选择 fan-out 个相邻节点来散播消息。每次散播消息都会选择尚未发送过的节点。并且收到消息的节点不再往发送节点传播。按照这样的规则,收到消息的节点就会把信息传递给其它没有收到过消息的节点。
【优点】
拓展性强:集群中节点数量的增加,不会导致消息通信成本有明显变化。
容错性好:因为 Gossip 协议本身是去中心化的,所有节点都是对等的。任何节点的宕机、重启并不会影响消息的传播。
最终一致性:系统状态的不一致可以在很短的时间内最终达到一致。
【缺点】
消息延迟:节点随机向少数几个节点发送消息,经过多轮次的传播才会达到全部节点,不可避免造成消息延迟。
消息冗余:Gossip 协议会周期性传播,节点定期并且随机选择周围节点进行发送消息,不可避免存在消息重复发送给同一个节点的情况。
拜占庭问题:如果存在恶意传播消息的节点,整个系统就会出现问题。
拜占庭帝国军队的将军们必须全体一致的决定是否攻击某一支敌军。问题是这些将军在地理上是分隔开来的,并且将军中存在叛徒。叛徒可以任意行动以达到以下目标:欺骗某些将军采取进攻行动;促成一个不是所有将军都同意的决定,如当将军们不希望进攻时促成进攻行动;或者迷惑某些将军,使他们无法做出决定。如果叛徒达到了这些目的之一,则任何攻击行动的结果都是注定要失败的,只有完全达成一致的努力才能获得胜利。
二、槽的指派
通过 cluster addslots <slot> [<slot> ... ]
命令,将一个或者多个槽指派给当前节点。
比如将编号从 0 到 500 的槽指派给当前节点。
cluster addslots 0 ... 500
Redis Cluster 使用分片的方式保存 Redis 数据库中的所有数据。整个数据库分为 16384 个槽(slot),从 0 到 16383 进行编号。当 16384 个槽都有节点在处理时,集群处于上线状态(OK);如果有任意一个槽没有得到处理,集群处于下线状态(FAIL)。
数据落到哪个 Redis 节点上?对 key 进行 CRC16 算法计算,再 % 16384,得到一个 slot 值,然后数据落到负责这个 slot 的 Redis 节点上。可以通过 cluster keyslot <key> 命令查看数据属于哪个槽。
Redis 节点用一个 bit 序列维护自己负责的 slot,比如序列的第 0 位是 1,表示第一个 slot 由它负责;序列的第 1 位是 0,表示第二个 slot 不是由它负责的。
key 与 slot 之间的关系永远不会变,会变的是 Redis 节点与 slot 之间的关系。
如果想让相关的数据落到同一个 Redis 节点上,可以在 key 里面加上相同的 {hash tag} 即可,因为 Redis 计算槽编号的时候只会获取大括号里面的字符串进行槽计算。
三、在集群中执行命令
当 Redis 集群处于上线状态时,客户端可以向集群中的节点发送命令,接收到命令的 Redis 节点会计算数据属于哪个槽,并且检查这个槽是否指派给了自己。如果这个槽指派给了自己,则该节点直接执行命令;否则当前节点返回一个 MOVED 错误给客户端,然后指引客户端 redict 到正确的节点上并执行命令。
故障检测与故障转移
故障检测
集群中的每个节点会定期向集群中的其它节点发送 PING 消息,以此来检测对方是否在线。如果接收到 PING 消息的节点没有在规定的时间内返回 PONG 消息,那么就会标记该节点为疑似下线(probable fail,PFAIL)。
如果集群中有半数以上负责槽的主节点将某个主节点标记为疑似下线,则该主节点被标记为已下线(FAIL)。
故障转移
当从节点发现自己的主节点变为已下线时,将自己记录的集群 currentEpoch 加 1,并广播 FAILOVER_AUTH_REQUEST 信息,要求所有接收到该消息并且具有投票权(即正在负责处理槽)的主节点向自己投票。
集群中只有主节点响应,并且会判断请求者的合法性,然后发送 FAILOVER_AUTH_ACK,每个 epoch 只会发送一次 ack。
如果从节点收到半数以上的 FAILOVER_AUTH_ACK,则执行 slaveof no one
命令并成为新的主节点。
新的主节点会撤销之前主节点的槽的指派,并将这些槽指派给自己。
然后向集群广播一条 PONG 消息。这条 PONG 消息可以让集群中的其它节点立即知道该节点已经成为新的主节点。
最后开始接收并处理自己负责的槽的相关的命令请求。
重新分片
可以将任意数量已指派给某个节点的槽修改为指派给另一个节点。重新分片可以在线进行,并且重新分片的过程中,集群不需要下线,并且源节点和目标节点可以继续处理客户端的请求。
在重新分片期间,客户端向源节点发送命令,可以会遇到对应的槽与数据都在目标节点上(如果在源节点上则直接执行客户端的命令),这时源节点会向客户端返回一个 ASK 错误,指引客户端转向目标节点并再次发送之前想要执行的命令。
重新分片操作是由 Redis 的集群管理软件 redis-trib 负责执行的。redis-trib 对集群的单个槽进行重新分片的步骤如下:(如果涉及多个槽,那么 redis-trib 会对每个槽执行如下)
1、redis-trib 向目标节点发送 cluster setslot <slot> importing <source_id>
命令,让目标节点准备好从源节点导入对应 slot 的数据。
2、redis-trib 向源节点发送 cluster setslot <slot> migrating <target_id>
命令,让源节点准备好将对应 slot 的数据迁移到目标节点。
3、redis-trib 向源节点发送 cluster getkeysinslot <slot> <count>
命令,获取最多 count 个属于指定 slot 的数据的 key。
4、针对步骤 3 获取的每个 key,redis-trib 向源节点发送 migrate <target_ip> <target_port> <key> 0 <timeout>
命令,将指定的数据从源节点迁移到目标节点。
5、重复步骤3、步骤4,直到源节点指定 slot 保存的所有数据都被迁移到目标节点为止。
6、redis-trib 向集群中的任意一个节点发送 cluster setslot <slot> node <target_id>
命令,将指定的 slot 指派给目标节点,这一指派信息会通过消息发送给整个集群,最终集群中的所有节点都会知道指定 slot 已经指派给了目标节点。