讲一讲Redis Cluster分布式集群是如何保证可扩展的
之前的文章我们讲了Redis的主从架构模式、Sentinel哨兵模式,这篇文章我们再来讲讲Redis Cluster分布式集群模式。不要搞混主从模式、Sentinel模式、Cluster集群模式,这三者的侧重点或者说作用是不相同的。
-
主从架构模式
主从模式主要实现数据备份、容灾恢复,以及通过主从复制实现读写分离,通常是一主多从的架构模式,主服务器用来写,从服务器用来读。主从架构模式侧重于支持高并发。
-
Sentinel哨兵模式
多个Sentinel实例组成了一个监视系统,主要是监视所有的服务器,当某个主服务器下线时,执行故障转移操作,在多个从服务器中选举出新的主服务器来替代已下线的主服务器,保证整个服务器系统能够继续正常工作。Sentinel哨兵模式侧重于支持高可用。
-
Cluster集群模式
单台Redis服务器的内存毕竟有限,支持的QPS也有上限,对于处理高并发的业务请求,我们需要多台Redis服务器一起来处理。每一个Redis相当于一个节点,我们把这些节点捆绑在一起,就形成了一个集群。要注意的是主从模式是由“上下级”关系的,而集群中的每一个服务器都是**“平等”的**。Cluster集群侧重于支持可扩展。
接下来我们就来说说Redis的Cluster集群是如何实现的。
我们可以使用cluster meet < ip > < port>
命令,将指定IP的那个节点B加入到当前节点A所在的集群中来。
执行这个命令会:
- 节点A会向节点B发送一条MEET消息
- 节点B接收到节点A发送的meet消息之后,会向节点A返回一条PONG消息
- 节点A节点接收到节点B返回的PONG消息之后,就可以知道节点B已经成功接收到了自己发送的MEET消息
- 节点A会再向节点B返回一条PING消息,节点B接收到节点A返回的PING消息,就可以知道节点A已经成功接收自己发送的PONG消息。
整个过程类似于TCP/IP协议建立连接的三次握手的过程。
节点B加入到当前节点A所在的集群之后,节点A会将节点B的信息通过Gossip协议传播给集群中的其它节点,让其它节点也与节点B进行握手连接,最终节点B会被集群中的所有节点认识。
我们说集群模式是实现了横向扩展,类似于形成了多主多从的架构模式,那集群中的多个节点是怎么保存数据的呢?都保存相同的数据吗?其实不是的,如果每个节点都保存一样的数据,是实现了可扩展性,但是这就会造成数据的大量冗余。实际上Redis集群模式是通过分片的方式来保存数据库中的键值对的。
集群模式下的整个数据库被分为了16384个槽(slot)数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或者最多16384个槽。数据库中的16384个槽都有对应的节点在处理时,集群处于上线状态,如果有任何一个槽没有得到处理,集群就处于下线状态。
节点的数据结构中有slots
数组属性,是一个二进制位数组,长度为16384/8=2048字节,用来记录节点的槽指派信息。一个节点除了记录自己负责处理的槽之外,还会将自己所处理的槽通过消息发送给集群中的其它节点,以此来告知其它节点自己当前负责处理哪些槽。这样每个节点都知道哪些槽是由哪个节点负责处理的。
而所有的槽的指派信息都会保存在集群状态数据结构中的slots数组里面,这样集群可以知道所有节点负责的槽的分布。
在完成对数据中的16384个槽都进行了指派之后,相当于把数据库中的所有数据分散在了多台服务器上面,此时集群就会进入上线状态,这是客户端就可以向集群中的节点发送命令了。
当客户端向节点发送读写命令时,接收命令的节点会先计算出命令要处理的key属于哪个槽,并检查这个槽是否指派给了自己。如果key所在的槽正好指派给了当前节点,那么这个节点直接执行这个命令;如果没有指派给当前节点,那么节点会向客户端返回一个MOVED
错误,指引客户端转向到正确的节点,并再次发送之前执行的命令。
当前节点是如何计算key属于哪个槽的?
是通过计算出key的CRC16校验和,然后再对16384进行取模运算,得到一个0~16383之间的槽位号。
计算出key对应的槽位号之后,节点就会从记录这集群状态的数据结构中的slots数组中检查这个槽位号是否是自己负责的。如果数组中对应的槽位号指向的就是自己,说明这个槽位号是由当前节点负责的,那么这个节点就会执行客户端发送的命令。如果槽位号指向的不是当前节点,而是其他节点,那么当前节点会记录槽位号指向的那个节点的IP地址和端口号,并向客户端返回MOVED错误,并根据记录的IP地址和端口号指引客户端转向负责处理这个槽位的节点。
我们说当key所在槽并不是由当前节点负责处理的时候,当前节点就会返回给客户端一个MOVED错误,指引客户端转向到正在负责这个槽的节点去处理。而这个MOVED错误的格式为MOVED < slot > < ip > : < port >
。
当客户端接收到当前节点返回的MOVED错误时,客户端会根据MOVED错误提供的IP地址和端口号,重定向到负责处理对应槽的节点,并向这个节点重新发送之前执行的命令。
所谓的节点转向实际上就是换一个套接字来发送命令,没有客户端与要转向到的节点还没有创建套接字连接,那么客户端会先创建套接字连接节点,然后再进行转向。
在找到key对应的槽位所负责处理的那个节点之后,就可以执行命令对数据进行读写操作了。而集群几点之保存数据以及数据的过期时间、内存淘汰,和普通单机服务器是完全一样的。只不过只是保存了整个数据库的一部分数据罢了。但是有一点区别就是,节点只能使用0号数据库。
由于key和slot就有对应关系,所以节点除了保存数据库数据以外表,还会用集群状态数据结构中的slots_to_keys跳跃表来保存槽和key之间的关系。之所以使用跳跃表,是因为可以方便的得到某个槽位都包含哪些key,以及对key进行批量操作。
虽然集群采用了分片的方式存储数据库中的数据,但是操作数据库中的数据本质是和单机服务器上的操作是一样的,只不过多了定位槽号这一步,因为我们在操作数据之前,首先得确定这个数据存储在哪个Redis服务器上面。
集群中的每个节点负责的槽的数量是不相同的,极端条件你甚至可以不给节点指派槽,或者把16384个槽都指派给一个节点。如果将已经指派给某个节点的槽,改为指派给其它节点时,相关槽所属的键值对数据也对移动到另一个节点中。这就是Redis集群的重新分片操作。
重新分片操作可以在线进行,重新分片过程中,集群不需要下线,并且源节点和目标节点都可以继续处理命令请求。Redis集群的重新分片操作是由Redis集群管理软件redis-trib复制执行的,redis-trib通过向源节点和目标节点发送命令来进行重新分片操作。
redis-trib对集群的单个槽进行重新分片的步骤:
- 对目标节点发送cluster setslot < slot > importing < source_id> 导入槽命令,让目标节点准备好从指定的源节点导入指定槽的所有键值对。
- 对源节点发送cluster setslot < slot > migrating < target_id > 迁移槽命令,让源节点准备将属于指定槽的所有键值对迁移到目标节点。
- 源节点和目标节点都准备好之后,开始从源节点向目标节点迁移键值对,直到源节点保存的所有属于指定槽的键值对都被迁移到目标节点为止。
- 迁移完所有的键值对之后,向集群中任意一个节点槽更新命令,将重新分片后的槽的指派信息通过消息发送至整个集群。
在进行重新分片期间,源节点向目标节点迁移一个槽中的所有键值对的过程中,有可能会出现一部分键值对保存在源节点中,另一部分键值对保存在目标节点中。那么如果这时候客户端发送命令要读写的key恰好属于正在迁移的槽时,该怎么办?是向源节点中查找这个key,还是向目标节点中查找这个key?实际上源节点会先在自己的数据库里面查找这个key,如果找得到,说明这个key还没有被迁移到目标节点,源节点直接执行这个命令即可。如果源节点没有在自己的数据库中找到这个key,那么这个key很有可能已经被迁移到目标节点了,源节点会向客户端返回一个ASK
错误,指引客户端转向目标节点,并再次发送之前要执行的命令。
客户端接收到返回的ASK
错误后,在客户端转向正在导入槽的目标节点,准备再次发送要执行的命令之前,首先会向目标节点发送一个ASKING
命令。ASKING
命令唯一要做的就是打开发送该命令的客户端的REDIS_ASKING
标识。如果大打开这个标识,那么客户端转向目标节点再次发送命令时,由于目标节点还正在导入槽,此时槽还不属于目标节点,因此目标节点会返回一个MOVED错误,让客户端再次转向源节点,这样就造成了源节点和目标节点互相甩锅!导致两个节点都无法执行客户端发送来的命令!在一般情况下,如果客户端向节点发送关于一个槽的命令,而这个槽有没有指派给这个节点的话,节点就会向客户端返回一个moved
错误,但是如果这个节点是正在导入这个槽的话,并且发送命令的客户端带有REDIS_ASKING
标识,那么虽然这个槽还没有指派给目标节点,但是目标节点会破例执行关于这个槽的命令一次,执行一次完了之后就会移除这个REDIS_ASKING
标识。ASK错误和MOVED错误差不多,都会导致客户端转向另一个节点,但区别在于ASK错误是因为键值对不在当前节点,但是槽还是由当前节点负责处理,而MOVED错误是因为槽并不是由当前节点负责处理的。
这样就通过ASK错误+ASKING命令解决了访问正在迁移中的槽所保存的键值对数据的问题!
我们说Redis集群模式实现了横向扩展,有点质量不够,数量来凑的意思。但是在扩展节点的同时,集群也要保障高可用,因此集群模式也存在主从复制和故障转移。
Redis集群中的节点分为主节点和从节点,其中主节点用来处理槽,而从节点用来复制某个节点
当某个主节点下线,就会自动执行故障转移,会从它的从节点中选出一个作为新的主节点,并接管原来主节点负责处理的槽继续工作,原来的从节点进行复制的目标节点也改为新的主节点。
故障转移完成之后,如果下线的节点重新连接,那么他会成为升级为新的主节点的从节点
接下来我们就来说说集群模式下的复制和故障转移是怎么实现的。
首先我们要指定集群中的哪些节点来充当从节点,我们可以向一个节点发送cluster replication < node_id >
复制命令,让当前节点成为指定节点的从节点,并开始对主节点进行复制。实际上集群模式下的从节点复制主节点,和单机Redis服务器的复制功能,也就是主从复制,底层实现都是一样的,相当于执行了slaveof
这个命令。
集群中的每个节点都会定期地向集群中的其它节点发送PING消息,以此来监测对方是否还在线,如果接收PING消息的节点没有在指定的时间内返回PONG消息,那么发送PING消息的节点就会将这个节点标记为疑似下线,和Sentinel模式中的主观下线差不多。
如果在一个集群里面,半数以上负责处理槽的主节点都将某个主节点视为疑似下线,那么这个主节点将被标记为已下线,和Sentinel模式中的客观下线差不多。集群中的各个节点会通过互相发送消息的方式来交换集群中各个节点的状态信息,此时将主节点标记为已下线的节点会向集群广播这个主节点已下线的消息,收到这条消息的节点都会立即将这个主节点标记为已下线。
当一个主节点被标记为已下线状态时,它的从节点就开始对已下线主节点进行故障转移操作,步骤如下:
- 从已下线主节点的所有从节点,选出一个从节点成为新的主节点
- 被选中的从节点会执行slaveof摆脱与已下线主节点的主从关系,成为新的主节点
- 新的主节点会撤销所有对已下线主节点的槽指派,并将所有槽指派给自己
- 新的主节点向集群广播一条PONG消息,这条PONG消息可以让集群中的其它节点立即知道这个节点已经由从节点变成了主节点了,并且这个新的主节点已经接管了原本由已下线主节点负责处理的槽。
- 故障转移完成,新的主节点替代已下线的主节点继续工作。
这么多从节点,是如何选出新的主节点的呢?
集群选举新的主节点方法如下:
- 集群中有一个配置纪元,是一个自增计数器,初始值为0
- 当集群里的某一个节点开始一次故障转移操作时,集群配置纪元的值就会加1
- 对于每个配置纪元,集群里每个负责处理槽的主节点都只有一次投票机会,而第一个向主节点要求投票的从节点将获得主节点的投票
- 当从节点发现主节点进入已下线状态时,从节点会向集群广播一条主节点已下线的消息,所有收到这条消息的节点、并且具有投票权的主节点都要向这个从节点进行投票。
- 如果一个主节点具有投票权,并且这个主节点尚未投票给其它从节点,那么主节点将向要求投票的从节点返回一条确认消息,表示支持这个从节点成为新的主节点。
- 参与选举的从节点会统计收到了多少条确认消息,也就是统计得到了多少主节点的支持。
- 如果集群里面有超过一半的主节点支持这个从节点成为新的主节点,那么这个从节点就会被选举为新的主节点。
- 如果在一个配置纪元里面没有从节点收集到足够多的支持,集群进入一个新的配置纪元,并再次进行选举,直到选出来新的主节点为止。
从集群中选举新的主节点的方式,和选举领头Sentinel的方式差不多,本质上二者都是基于Raft算法来实现的。
从上面的讲的集群复制以及故障转移来看,实际上集群的复制和我们之前文章讲的主从复制差不多,而故障转移又和Sentinel哨兵模式下的自动故障转移差不多。
至此我们讲完了Redis cluster的集群模式,集群模式在支持可扩展的同时也保证了高可用!