数据分区
在介绍Redis Cluster之前,先简单介绍下分布式数据库的数据分区。所谓的数据分区就是将一个较大的数据集分布在不同的节点上进行储存。常见的数据分区方式:节点取余、一致性哈希、虚拟槽,下面我们来看下这几种分区方式。
节点取余:根据key的hash值和节点数取模的方式计算出节点ID,然后向对应的节点提交数据,如下图所示。
对于这种分区方式,新增或者删除节点会造成大量的数据迁移。
假设数据集为1 2 3 … 10,那么数据分布应如下所示。
如果新增一个节点,那么数据分布会变成什么样子呢?
来看下结果对比:只有1 2 3还分布在原来的节点上,其余所有的数据都进行了迁移。在这种分区方式下,如果新增的节点时原来的节点的倍数时,迁移的节点数量量会少很多。
一致性哈希:对于任何的哈希函数,都有其取值范围。我们可以用环形结构来标识范围。通过哈希函数,每个节点都会被分配到环上的一个位置,每个键值也会被映射到环上的一个位置,然后顺时针找到相邻的节点。如下图所示,例如key分布在range1内,那么数据存储在node2上。
对于这种分区方式,新增或者删除节点会造成数据分布不均匀。
假设数据集为1 2 3 … 12,数据范围也是1~12,那么数据分布应如下所示。
如果我们在node1和node2之间新增一个节点,那么数据分布应该变成什么样子呢?
可以看到,我们只是讲数据3进行了迁移。但是造成了每个节点负责的数据范围不等。会造成数据分布不均等的问题。
虚拟槽:在redis cluster中使用槽来存储一定范围内的数据集。每个redis节点上有一定数量的槽。当客户端提交数据时,要先根据CRC16(key)&16383来计算出数据要落到哪个虚拟槽内。
假设我们有3个节点,那么可以按如下分配槽:
与节点取余和一致性哈希分区不同,虚拟槽分区是服务端分区。客户端可以将数据提交到任意一个redis cluster节点上,如果存储该数据的槽不在这个节点上,则返回给客户端目标节点信息,告知客户端向目标节点提交数据。
Redis Cluster
redis cluster采用无中心结构,节点间使用gossip协议进行通信。每个节点保存数据和所有节点和槽的映射关系。其架构图如下:
moved异常
从上图可以看出,客户端是采用直连的方式来连接redis客户端。那么redis客户端是如何知道要提交到节点呢?
实际上,客户端可以将数据提交到任意一个redis cluster节点上,如果存储该数据的槽不在这个节点上,则返回给客户端moved异常,客户端通过moved异常,永久的将请求转移到目标节点,如下图所示。
通过moved异常,客户端可以知道目标节点并重新提交数据,但是这种情形效率不算高,那怎么解决这个问题的呢?
我们看下redis客户端JedisCluster怎么解决这个问题的。
- 初始化时从集群中选一个可运行节点,使用cluster slots初始化槽和节点映射。
- 将cluster slots的结果映射到本地,为每个节点创建JedisPool。
- 准备执行命令。
现在我们来实际看下这个异常,关于redis cluster的搭建过程本篇文章不进行描述。
首先来看下redis cluster的节点信息,执行命令redis-cli cluster nodes
可以看到使用的是3主3从的配置,虚拟槽的分配是0-5460、5461-10922、10923-16383,我们来演示下这个异常。。
127.0.0.1:6379> cluster keyslot a
(integer) 15495
127.0.0.1:6379> set a b
(error) MOVED 15495 127.0.0.1:6381
可以看到,key为的时候应该将数据插入到15495的槽内。但是15495这个槽是分配在端口号为6381的这个节点上。当尝试插入操作的时候返回了moved异常,异常信息包括目标节点和槽的信息。
ask异常
在redis cluster中,如果新增或者删除节点,需要进行虚拟槽的迁移。ask异常与moved异常类似,只不过ask异常发生在虚拟槽的迁移过程中,另外它与moved异常不同,ask异常只是将下一次的请求转移到目标节点,如下图所示。
消息机制
redis cluster集群中通过消息来进行通信。消息共有以下5种。
- meet消息:发送者会向接受者发送cluster meet命令,请求接受者将发送者加入到集群中。上文提到的无中心网络结构就是使用meet命令构建的。
- ping消息:集群中的每个节点每秒钟都会从已知节点列表选举出5个节点,然后从这5个节点中选中一个最长时间没有发送ping消息的节点作为目标节点来发送ping消息,来检测目标节点是否处于在线状态。
- pong消息:接受者接受到发送者发送的meet消息或者ping消息后,会回复pong消息,用于确认消息已经到达。另外,pong消息也可以用于刷新接收者对发送者的信息,例如故障转移后,从节点会向集群中发送pong消息,用于告知从节点已经升级为主节点。
- fail消息:fail消息用于通知将某个节点置为下线状态。例如节点A认为节点B已下线,节点A会向集群中发送一条fail消息,接受到这条消息的节点会将节点B标记为已 下线。
- publish消息:当某个节点收到publish命令后,会向集群中发送一条publish消息。
故障检测
集群中的每个节点都会定期地向集群中其它节点发送ping消息,以此来检测对方是否在线。如果在规定时间内没有收到pong回复。则认为目标节点标记为疑似下线(PFAIL)。
如果集群中的半数异常(大于等于N/2 + 1)的主节点认为某个节点A疑似下线(PFAIL),那么这个节点A将被标记为已下线(FAIL)。将节点标记A为已下线的节点会向集群一条关于节点A显现的消息,所有收到这条F消息的节点都会立即将主节点A标记为已下线。
举个例子,在下图所示中,主节点7002和主节点7003都认为主节点7000进入了下线状态。并且主节点7001也认为主节点7000进入了下线状态。综合起来,在集群中4个主节点里面,有3个都将主节点7000标记为下线。所以主节点7001会将7000节点标记为已下线。并向集群中广播一条关于主节点7000的FAIL消息,如下图所示。
故障转移
当一个从节点发现自己正在复制的主节点进入了已下线时,从节点将开始对已下线的主节点进行故障转移操作,以下是故障转移的执行步骤:
- 下线的主节点的所有从节点里面,会进行选举,选举出一个新的主节点。
- 被选中的从节点会执行 slave no one命令,成为新的主节点。
- 新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽指派给自己。
- 新的主节点向集群广播一条pong消息,这条pong消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点处理的槽。
- 新的主节点开始接受和自己负责处理的槽有关的命令请求,故障转移操作完成。
epoch
- current epoch:current epoch类似于系统的逻辑时钟,是一个64位无符号整数,启动时设置为0。redis中的每个节点都会维护一个current epoch。通过gossip协议,集群中的current epoch大多数情况下是一致的。current epoch越高,代表节点的配置或者操作越新。当一个节点被重新启动其current epoch被设定为已知的节点的最大config epoch。
- config epoch:config epoch类似于节点的版本号,每个节点都有一个不同的config epoch, 是一个单调递增的64位无符号整数。当故障转移操作后会将config epoch的值设置为current epoch + 1。
主节点选举
上文提到了主节点选举,现在简单介绍下主节点时如何选举出来的,以下是主节点选举的步骤。
- 当从节点发现自己复制的主节点进入已下线时,从节点(这里发出请求的从节点可能会有多个)会向集群广播一条cluster_type_failover_auth_request的消息,要求有投票权(负责处理槽)的主节点向这个节点进行投票。
- 收到cluster_type_failover_auth_request消息的主节点,根据自身条件(发起投票节点的current epoch不低于投票节点的current epoch)判断是否赞成该从节点成为新的主节点,若赞成则返回一条cluster_type_failover_auth_ack消息。
- 从节点接收到cluster_type_failover_auth_ack消息,会将选票数加1。
- 如果某个从节点的选票大于等于集群中主节点的一半时(大于等于N/2 + 1),这个节点就会成为新的主节点。
- 如果在一个配置周期内,没有一个从节点获得足够多的选票,那么集群中会进入新的配置周期,并在此进行选举,知道选出新的主节点为止。