通常我们会使用单体Redis应用作为缓存服务,但是为了保证高可用的环境,通常还会使用主从复制模式或读写分离的设计。
随着缓存数据量的增加,单体服务器无法承载缓存服务,此时就需要对缓存服务进行扩展。我们将需要缓存的数据切分成不同的分区,将数据分区分布在不同的服务器中,从而形成分布式缓存来承载高并发的缓存访问。恰好Redis Cluster能支持上述方案。
我们从以下几个方面来学习Redis Cluster的核心原理:Redis Cluster数据分区的实现、分布式缓存节点之间的通讯、请求分布式缓存的路由、缓存节点的扩展和收缩、故障发现和恢复。
分布式缓存的主要目的是,按照一定的规则,将原来存储在单个服务器上的一整块数据分配到多个缓存节点(即多个实现缓存应用的服务器),解决的是单个缓存节点(即服务器)处理数量大的问题。
Redis Cluster使用虚拟槽分区算法实现数据的拆分和存放。在虚拟槽分区算法中,槽的概念指的是存放缓存信息的单位,Redis将存储空间划分为16384个槽,即Redis Cluster槽的范围是0~16383(16 * 1024)。
缓存信息通常使用的是key-value的形式存放,在存储信息时,Redis Cluster会对key进行CRC16校验并对16384取模 [ 即信息存储的槽slot = CRC16(key) % 16383]。通过计算得到key-value所存放的槽slot,从而自动地将缓存数据分割到不同的缓存节点上,最后再将16384个槽分配给不同的缓存节点。
如下图所示,此时存在三个缓存节点A、B、C,Redis Cluster将存放数据的槽分配给三个节点。其中节点A存放槽编号0–5000的数据,节点B存放槽编号5000–10000的数据,节点C存放槽编号10001–16383的数据。
在Redis Cluster环境下,缓存节点被分配到一个或多个服务器上,那么这些缓存节点之间该如何进行通信?
如上图所示,假设存在这么一个情景,Redis Cluster中已存在缓存节点A,此时加入了一个新的缓存节点B,则接下来会发生如下过程:
1)节点B利用Gossip协议向节点A发送"meet消息";
2)节点A收到消息后,给节点B回复一个"pong消息";
3)此后,为了保证Redis Cluster中每个节点都能知道其他节点的存活状态,节点B会定期给节点A发送"ping消息",同样,缓存节点A每收到消息,都会回复一个"pong消息"。
上述例子说明,Redis Cluster中的缓存节点之间使用Gossip协议进行通信,而节点之间的通讯是为了维护节点之间的元数据信息,即节点包含哪些数据、是否出现故障等。因此,节点之间通过Gossip协议可以不断地交换元数据信息,从而使Redis Cluster中所有的节点都知道其他节点的情况,即节点的元数据。
整个通讯过程大致如下:Redis Cluster中的每个缓存节点都会单独开通一个用于节点间通讯的TCP通道,且Redis Cluster中存在一个定时任务,每隔一段时间就会从系统中选出发送节点,该发送节点按照一定的时间频率随机地向最久没有通讯的节点发起"ping消息",接受到"ping消息"的节点会使用"pong消息"向"发送节点"进行回复。
不断重复上述行为,可以让所有节点保持通讯。
从类型上说,Redis Cluster的消息可以分为4种:
1)meet消息,用于通知新节点加入。
2)ping消息,使用得最为频繁,该消息中封装了自身节点和其他节点的状态数据,有规律地发给其他节点。
3)pong消息,缓存节点在接受到meet和ping消息以后,也将自己的数据状态发给对方。同时也可以对集群中所有的节点发起广播,向所有节点告知自身的状态。
4)fail消息,如果一个节点下线或者挂掉了,会向集群中广播这个消息。
Gossop协议的结构如下图所示,其中type定义了消息的类型,如:meet、ping、pong、fail等。此外,数组myslots定义了该节点负责的槽信息,这是每个节点发送Gossip协议给其他节点最重要的信息,且消息体通过clusterMsgData对象传递消息征文。
分布式缓存要解决的问题是,如何使Redis客户端通过分布式缓存节点获取缓存数据。
我们知道节点之间通过Gossip协议进行通讯,其中还会将每个节点管理的槽信息unsigned char myslots[CLUSTER_SLOTS/8]
发送给其他节点。
myslots属性是一个二进制位数组(bit array),其中CLUSTER_SLOTS为16384,则这个数组的长度为16384/8=2048个字节,由于每个字节包含8个bit位(二进制位),所以共包含16384个bit,即16384个二进制位。
每个节点用bit来标识自己是否拥有某个槽的数据,如下图所示,该图表示缓存节点A所管理的槽信息,如:0号槽的bit值为0,表示不存储该槽对应的数据;2号槽的bit值为1,表示存储该槽对应的数据。
接收节点以如下的数据结构保存该节点管理的槽信息。
如下图所示,ClusterState中保存的Slots数组中每个下标对应一个槽,每个槽信息中对应一个clusterNode,即缓存节点。这些节点对应一个实际存在的Redis缓存服务,包括对应的IP和Port信息。实际上,上文讲到的Redis Cluster通讯机制保证了每个节点都有其他节点和槽信息的对应关系。因此,Redis的客户端无论访问集群中的哪个节点,都可以自动路由到对应的节点上,因为每个节点各自维护一份ClusterState,它记录了所有的槽与节点的对应关系。
接下来分两种情况来说明如何通过路由来调用缓存节点。
(1)MOVED重定向请求。如下图所示:
a)首先计算slot,发现需要向缓存节点A发送读/写请求。但是,由于缓存数据迁移或者其他原因导致这个对应的Slot的数据被迁移到了缓存节点B上面;
b)此时Redis客户端无法从缓存节点A获取数据。但是,缓存节点A保存了集群中所有缓存节点的信息,所以它知道这个slot是在缓存节点B,于是节点A就给Redis客户端发送了一个MOVED重定向请求;
c)此时,Redis客户端得到该重定向请求的地址,继续访问缓存节点B,并且拿到数据。
(2)ASK重定向请求。如下图所示:
a)Redis客户端向缓存节点A发送请求,而此时节点A在向节点B迁移数据。若没有命中对应的Slot,它会给Redis客户端返回一个ASK重定向请求,并告知节点B的地址。
b)Redis客户端向缓存节点B发送Asking命令,询问需要的数据是否在缓存节点B上,缓存节点B接到消息以后返回数据是否存在的结果。
在Redis Cluster工作的过程中,会出现缓存扩容和故障的情况,这就会导致缓存节点的上线和下线问题。由于每个节点中都保存着槽数据,因此,当缓存节点出现变动时,则这些槽数据会根据对应的虚拟槽算法被迁移或复制到其他的缓存节点上。
如下图所示,Redis Cluster集群中已经存在两个缓存节点A和B,此时,缓存节点C上线了且加入到了集群中。根据虚拟槽算法,缓存节点A和B中对应的槽数据会因新节点的加入而被迁移到缓存节点C上。
针对节点扩容,新建立的节点需要运行在集群模式下,且最好保证新节点的其他配置与集群内其他节点的配置保持一致。当新节点加入到集群时,作为孤儿节点是没有和其他节点进行通讯的。使用cluster meet {targetIP} {targetPort}命令加入到集群中。此外,新节点加入时,它没有建立槽Slots对应的数据,即没有任何缓存数据。
上述描述的是节点上线的过程,即扩容的情况。缓存节点有上线的操作,自然就有下线的操作。下线操作正好和上线操作相反,将要下线的缓存节点中的槽数据分配到其他的缓存主节点中。不同的是,下线的时候需要通知集群中的其他节点忘记自己,此时通过命令cluster forget{downNodeId}
通知其他的节点。
当节点收到forget
命令后,会将这个下线节点放到仅用列表中,那么之后就不用再向这个节点发送Gossip的ping消息了。不过这个仅用列表的超时时间是60秒,超过了这个时间,依旧还会对这个节点发起ping消息。不过可以使用redis-trib.rb del-node {host:port} {donwNodeId}
命令帮助我们完成下线操作。
需要注意的是,若下线的节点是主节点,则需要安排对应的从节点接替主节点的位置。
前面提到,缓存节点存在扩容和收缩的情况,而缓存收缩是一个下线的动作,有时候这是为了节约资源或执行计划性的下线,但更多的时候是故障导致的下线。
针对故障的下线,有两种确定方式:主观下线和客观下线
(1)主观下线。
当节点A向节点B例行发送ping消息的时候,如果节点B正常工作就会返回pong消息,并记录节点A的相关信息。同时接收到pong消息的节点A也会更新最近一次与节点B通讯的时间。
如果此时两个节点由于某种原因断开连接,过一段时间以后,节点A还会主动连接节点B,如果一直通讯失败,节点A中就无法更新与节点B最后通讯时间了。此时节点A的定时任务检测到与节点B最近通讯的时间超过了cluster-node-timeout
的时候,就会更新本地节点状态,把节点B更新为主观下线。这里的cluster-node-timeout
是节点挂掉被发现的超时时间,如果超过这个时间还没有获得节点返回的 Pong 消息就认为该节点挂掉了。
综上所述,主观下线指的是:节点A没有收到节点B返回的pong消息,且超过了cluster-node-timeout
限定的时间,从而主观上认为节点B挂掉了。但是,有可能是节点A与节点B之间的网络断开了,而其他节点依旧可以和节点B进行通讯。因此,主观下线并不能代表某个节点真的下线了。
(2)客观下线。
由于Redis Cluster的节点不断地与集群内的节点进行通讯,下线信息也会通过Gossip消息传遍所有节点。因此集群内的节点会不断地收到下线报告,当半数以上持有槽的主节点标记了某个节点是主观下线时,便会触发客观下线的执行过程。即当集群内半数以上的主节点,认为某个节点主观下线了,才会启动客观下线的执行过程。
这个执行过程有个前提,就是只针对主节点,若是从节点则直接忽略。当集群中的节点每次接受到其他节点的主观下线时,都会做以下的事情:
a)记录。将主观下线的报告保存到本地的ClusterNode
的结构中,并且对主观下线报告的时效性进行检查,如果超过cluster-node-timeout * 2
的时间,则忽略这个报告。反之,则记录该报告内容。
b)比较。被标记下线的主观节点的报告数量大于等于持有槽的主节点数量的时候,将该节点标记为客观下线。
c)广播。若被标记为客观下线,则向集群中广播一条fail消息,通知所有的节点将故障节点标记为客观下线,该消息指包含故障节点的ID。
d)Redis Cluster集群内的所有节点都会标记这个节点为客观下线,通知故障节点的从节点触发故障转移的执行过程,即故障恢复。
需要注意的是:如果某个主节点被认为客观下线了,那么需要从它的从节点中选出一个节点来替代主节点的位置。此时下线主节点的所有从节点都担负着恢复的义务,这些从节点会定时监测主节点是否下线。
(3)触发的故障恢复过程如下图所示。
a)资格检查过程表明,若该从节点和主节点断开的时间太久,即T_disconnected超过T_flag,则说明该从节点很久没有同步主节点的数据,不适合成为新的主节点,因为成为主节点后其他的从节点会同步自己的数据,这会造成数据丢失。
b)cluster-slave-validity-factor表示从节点有效因子,默认为10。
c)复制偏移量记录了执行命令的字节数。主节点每次向从节点传播N个字节时,就会将自己的复制偏移量+N,从节点在接收到主节点传送来的N个字节的命令时,就将自己的复制偏移量+N。复制偏移量越大说明从节点延迟越低,即该从节点和主节点沟通更加频繁,该从节点上面的数据也会更新一些。因此,复制偏移量大的从节点会率先触发选举。
d)发起选举时,首先每个主节点会去更新配置纪元(clusterNode.configEpoch),该值是不断增加的整数。此外,在节点进行ping/pong消息交互式时,也会更新这个值,它们都会将最大的值更新到自己的配置纪元中。
该值记录了每个节点的版本和整个集群的版本。每当发生重要事情的时候,例如:出现新节点、从节点竞选等,都会增加全局的配置纪元并且赋给相关的主节点,用来记录这个事件。而更新这个值的目的是:保证所有的主节点都对该事件保持一致,即统一的配置纪元,表示所有的主节点都对该事件知情。
e)在进行投票选举时,率先触发选举的从节点通常会获得更多的票。获得投票的机会越大,说明它与原主节点的延迟少,数据表现上与原主节点更一致。