前面两篇文章介绍了Redis主从和哨兵模式,不难发现,它们都有一些共同的缺点,首先在主从切换的过程中会丢失数据;另一个就是只有一个master,只能单点写,并没有水平扩容能力。而且每个节点都保存了所有的数据,这使得内存占用率变高,另一个就是如果进行数据恢复也比较慢。为了解决这些问题,集群模式应运而生,下面我们来一起看看。
一、什么是Redis Cluster
1.1 概念
Redis Cluster 是 Redis 数据库的一种集群方案,用于实现高可用性和横向扩展。Redis Cluster 将数据分布在多个节点上,每个节点负责存储部分数据,同时提供数据复制和自动故障转移功能,以确保系统的可靠性和性能。
Redis Cluster 通过支持分片(sharding)和复制(replication)来实现数据在集群中的分布和备份,从而在数据量增长时能够水平扩展,并且能够容忍单个节点的故障而不影响整个系统的正常运行。
在高可用上,集群基本是直接复用哨兵模式的逻辑,并且针对水平扩展进行了优化。
1.2 特点
- 采用去中心化的集群模式,将数据按槽分布在多个Redis节点上,集群共有16384个槽,每个节点负责处理部分槽,并使用CRC16算法来计算key所属的槽:crc16(key,keylen) & 16383
- 所有的Redis节点彼此互联,通过PING-PONG机制来进行节点间的心跳检测。
- 客户端与Redis节点直连,不需要中间代理层(proxy)。客户端不需要连接集群中所有的节点,只用连接集群中任意一个可用节点即可。
- 采用主从复制机制,将数据复制到多个节点上。每个主节点会有一个或多个从节点。在实际使用中,通常会将主从分布在不同机房,避免机房出现故障导致整个分片出问题,如下图。
二、在集群中执行命令
在对16384个槽指派后,集群就会进入上线状态,当客户端向节点发送与数据库键有关的命令时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽:
- 如果刚好是当前槽,那么节点直接执行命令。
- 反之,节点就会返回一个MOVED错误,指引客户端转向至正确的节点,并再次发送之前想要执行的命令。
2.1 计算key属于哪个槽
ps:由于篇幅原因,这个在后续的文章中会详解。
Redis 集群中内置了 16384 个哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。当需要在 Redis 集群中放置一个 key-value时,redis先对key使用crc16算法算出一个结果,然后用结果对16384求余数[ CRC16(key) % 16384],这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,也就是映射到某个节点上。
使用CLUSTER KEYSLOT <key>命令可以查看一个key属于哪个槽,这个命令就是通过调用上面给出的槽分配算法来实现的。
2.2 MOVED错误
当节点发现key所在的槽并非是自己负责处理的时候,节点就会向客户端返回一个MOVED错误(MOVED <slot> <ip>:<port>),其中slot为key所在槽,ip和port则是负责处理槽的节点ip和端口号。
在客户端接收到节点返回的MOVED错误时,客户端会根据MOVED错误中提供的ip和端口号,转向负责处理槽的节点,并向该节点发送之前想要执行的命令。
三、 重新分片
Redis集群的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),并且相关槽所属的键值对也会从源节点被移动到目标节点,该过程可以在线(online)进行。
Redis集群的重新分片操作是由Redis的集群管理软件redis-trib负责执行的,步骤如下:
- redis-trib对目标节点发送CLUSTER SETSLOT<slot>IMPORTING<source_id>命令,让目标节点准备好从源节点导入(import)属于槽slot的键值对。
- redis-trib对源节点发送CLUSTER SETSLOT<slot>MIGRATING<target_id>命令,让源节点准备好将属于槽slot的键值对迁移(migrate)至目标节点。
- redis-trib向源节点发送CLUSTER GETKEYSINSLOT<slot><count>命令,获得最多count个属于槽slot的键值对的键名(key name)。
- 对于步骤3获得的每个键名,redis-trib都向源节点发送一个MIGRATE<target_ip><target_port><key_name>0<timeout>命令,将被选中的键原子地从源节点迁移至目标节点。
- 重复执行步骤3和步骤4,直到源节点保存的所有属于槽slot的键值对都被迁移至目标节点为止。
- redis-trib向集群中的任意一个节点发送CLUSTER SETSLOT<slot>NODE<target_id>命令,将槽slot指派给目标节点,这一指派信息会通过消息发送至整个集群,最终集群中的所有节点都会知道槽slot已经指派给了目标节点。
四、ASK错误
在进行重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面。
当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽时。源节点会先在自己的数据库里面査找指定的键,如果找到的话,就直接执行客户端发送的命令。
否则,这个键有可能已经被迁移到了目标节点,源节点将向客户端返回一个 ASK 错误,指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令,从而获取到结果。
五、MOVED和ASK的区别
从上面看,MOVED和ASK都启到了重定向客户端的效果,那么它们有什么区别呢,简单的说就是:
- MOVED代表该槽已经完全由另一个节点负责了,会触发客户端刷新本地路由表,之后对于该槽的请求都会请求新的节点。
- ASK只是在槽迁移中的一种临时措施,只是会产生一次重定向。
ps:本地路由表是该集群的插槽和负责处理该槽的节点地址的映射,通过该路由表,客户端可以在大部分情况下都直接请求到正确的节点,而无需重定向。
六、复制与故障转移
6.1 gossip协议
6.1.1 什么是gossip协议
这里先介绍一下gossip协议(下面会用到),全称Gossip protocol也叫Epidemic Protocol(流行病协议),那么它是干嘛的呢?
在整个Redis集群中,如果出现以下情况:
- 新节点加入。
- slot迁移。
- 节点宕机。
- slave选举成为master。
我们希望这些变化能够让整个集群的每个节点能够尽快发现,那么各个节点之间就需要相互连通并且携带相关状态数据进行传播,这就用到了gossip协议。
你可能会想到用广播的方式不也可以实现,答案是当然可以,但是对CPU和带宽的消耗比较大,所用还是采用goosip协议。
该协议的特点是,在节点数量有限的网络中,每个节点都会“随机”(也不是真正的随机,而是根据规则选择通信节点)与部分节点通信,经过一番杂乱无章(传染病)的通信后,每个节点的状态可以在一定时间内达成一致。
6.1.2 gossip协议消息
gossip协议包含多种消息,比如ping、pong、meet、fail等
- ping:每个节点都会频繁给其它节点发送ping,包含自己的状态还有维护的集群元数据,通过ping互相交换元数据。
- pong:返回ping和meet,包含自己的状态和其它信息,也可以用于信息广播和更新。
- fail:某个节点判断另一个节点fail后,就发送fail通知其它节点该节点下线了。
- meet:当发送者接到客户端发送CLUSTER MEET命令时,发送者会向接收者发送MEET消息,请求接收者加入到发送者所在的集群里面。
6.2 复制
集群的每个分片都使用主从复制保证高可用,之前的文章中已经介绍过了,就不再赘述了。
6.3 故障检测
与哨兵模式的故障检测核心思想基本一致。
集群中的每个节点都会定期地向集群中的其它节点发送PING消息,以此来检测是否在线,发送PING消息的时机主要有两个:
- 每秒执行一次:随机检查5个节点,选出最早收到PONG回复的节点,即最久没有通信过的节点,发送PING消息。
- 每100毫秒执行一次:轮询集群的节点,对于正常节点,如果上次收到该节点的PONG回复的时间距离现在,已经超过集群超时时间的一半(cluster-node-timeout/2)则直接向该节点发送PING消息。
如果接收 PING 消息的节点在规定的时间内(cluster-node-timeout,默认15秒-conf配置文件),没有返回 PONG 回复或者发送其他任何消息,那么发送 PING 消息的节点就会将接收 PING 消息的节点标记为疑似下线(probable failure,PFAIL)。
ps:这边 Redis 没有将 PONG 回复作为目标节点存活的唯一证明,而是将目标节点的任何消息都作为存活的证明。这是因为在集群负载较高的时候,收到 PONG 回复可能会出现延迟。
如果一个集群里面,半数以上主节点都将某个主节点x报告为疑似下线,那么该主节点x将被标记为已下线(FAIL),将主节点x标记为已下线的节点会向集群广播一条关于主节点x的FAIL消息,所有收到消息的节点会立即将主节点x标记为已下线。
6.4 选举新的主节点
故障转移的第一步就是选举新的主节点,该方法和选举领头Sentinel的方法非常相似,因为两者都是基于Raft算法实现的。
6.4.1 epoch(纪元)
在介绍该流程之前我们先了解一个名词epoch(纪元):Redis集群中使用了类似Raft算法term(任期)的概念成为epoch(纪元),在Redis集群中主要有两种:
- currentEpoch(集群配置纪元):是一个集群范围内的全局递增计数器,用于跟踪集群的状态变化。每当集群发生状态变化,如节点加入、离开、主从切换等,currentEpoch就会增加。这有助于确保集群中的各个节点都了解到最新的集群状态。例如,当一个新的节点加入集群时,它会从其他节点获取当前的 currentEpoch,以确保它了解到最新的集群状态。
- configEpoch(节点配置纪元):是每个节点上的参数,表示节点在处理集群配置时的版本号。当一个节点成为集群中的主节点时,它的 configEpoch 会增加,并且这个增加的值会被广播给整个集群,以通知其他节点。这个值在 Redis 集群中被用来判断一个节点是否已经接受了来自其他节点的新的配置。如果一个节点的 configEpoch 落后于其他节点,则说明它需要更新配置以保持与集群的一致性。这对于确保集群的配置是一致的非常重要,特别是在故障恢复、主从切换等情况下。
6.4.2 流程
当从节点发现自己正在复制的主节点进入已下线状态时,会发起一次选举:将 currentEpoch 加1,然后向集群广播一条 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。
其它节点收到消息后,会判断是否要给发消息的节点投票:
当前节点是slave或master,但不负责处理槽,则当前节点没有投票权,直接返回。
请求节点的currentEpoch小于当前节点,校验失败。因为发送者的状态与当前集群不一致,可能是长时间下线的节点刚刚上线,直接返回。
当前节点已经投过票了,检验失败。
请求节点是master或为空,检验失败。
请求节点的master没有故障,并且不是手动故障转移,检验失败。
上一次为该master投票时间在cluster-node-timeout的2倍范围之内,检验失败。这个用于使获胜从节点有时间将其成为新主节点的消息通知给其他从节点,从而避免另一个从节点发起新一轮选举又进行一次没必要的故障转移。
请求节点的configEpoch如果比之前负责这些槽位的节点小,检验失败。
通过以上校验,主节点才会向要求投票的从节点返回一条CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,从节点会根据收到这个消息的条数来统计获得了多少个主节点的支持(票数)。
当一个从节点收集到N/2+1张票支持(N代表具有投票权的主节点)时,这个从节点就会当选为新的主节点。(每个主节点只能投一次票)
如果在一个配置纪元内所有从节点都没能收到足够的票,那么集群会开启新的纪元,再次选举,直到选出为止。
6.5 故障转移
当slave发现自己正在复制的master处于已下线(FAIL)状态时,slave会对下线的matser进行故障转移:
- 发起一次选举,该下线的master的所有slave里面,会有一个slave被选中。
- 被选中的slave会执行SLAVEOF no one命令,成为新的master。
- 新的master会撤销所有对已下线master的槽指派,并将这些槽全部指向自己。
- 新的master向集群广播一条PONG消息,这条PONG消息可以让集群中的其它节点立即知道这个节点已经“上位”了,并且已经接管了原本由已下线节点负责处理的槽。集群中的其他节点收到消息后会更新自己保存的相关配置信息。
- 新的master开始接收和自己负责处理的槽相关的命令请求,至此,故障转移完成。
七、优缺点
优点:
- Redis Cluster 采用分布式架构,自动将数据分片存储在多个节点上,提供了良好的横向扩展性和高可用性。
- 集群节点可以自动发现和重新分片,并且支持节点动态扩容和缩减,减少了集群管理的复杂性。
- 当新的节点加入或离开集群时,Redis Cluster 能够自动进行节点发现和重新分片,而无需手动干预,这简化了集群管理的复杂度。
缺点:
- Redis Cluster 不支持跨分片的事务操作,事务必须在同一分片上执行,这可能会在某些场景下带来限制。
- 配置和管理 Redis Cluster 相对复杂,需要理解分布式系统的原理和网络分区等问题。需要投入更多的精力来实施和维护集群。
End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。