当前位置: 首页 > 科技观察

不懂RedisCluster的原理,被同事diss!

时间:2023-03-12 18:29:33 科技观察

【.com原创文章】Redis缓存是使用最多的缓存工具,各大厂商都在使用。通常我们使用单个Redis应用作为缓存服务,为了保证其高可用,我们也会采用主从模式(Master-Slave),或者说读写分离的设计。图片来自Pexels,但是当缓存数据量增加,缓存服务无法由单个服务器承载时,就需要对缓存服务进行扩展。将需要缓存的数据划分到不同的分区,将数据分区放在不同的服务器上,使用分布式缓存来承载高并发的缓存访问。正好RedisCluster方案正好支持这部分功能。今天就来看看RedisCluster的核心原理和实践:RedisCluster实现数据分区分布式缓存节点间通信请求分布式缓存路由缓存节点扩缩容故障发现与恢复RedisCluster实现数据分区就像开头提到的,分布式数据库要解决的是将整块数据按规则分布到多个缓存节点,解决大量单个缓存节点的问题。如果要将这些数据拆分存储,就必须有一个算法。比如:哈希算法和哈希共识算法,这些都是比较经典的算法。RedisCluster使用虚拟槽分区算法。提到了槽(Slot)的概念。这个slot是用来存放缓存信息的单元。在Redis中,存储空间被划分为16384个槽,也就是说RedisCluster槽的范围是0-16383(2^4*2^10)。缓存信息通常以Key-Value的形式存储。存储信息时,集群会对Key进行CRC16校验,取模16384(slot=CRC16(key)%16383)。得到的结果就是放置Key-Value的slot,这样就可以自动将数据分到不同的节点中。然后将这些槽分配给不同的缓存节点进行存储。图1:Redis集群中的数据分片如图1所示,假设有1、2、3三个缓存节点,RedisCluster将存储缓存数据的槽(Slots)分别放到这三个节点中:缓存节点1存储(0-5000)个Slots的数据。缓存节点2存放的是(5001-10000)Slot的数据。缓存节点3存放的是(10000-16383)Slot的数据。这时候RedisClient需要根据一个Key获取对应的Value数据。首先通过CRC16(key)%16383计算Slot的值,假设计算结果为5002,将这个数据发送给RedisCluster,cluster收到后会去查找Slot=5002所属的缓存节点在比较表中。发现属于“缓存节点2”,于是沿红线方向调用缓存节点2中存储的Key-Value内容返回给RedisClient。分布式缓存节点之间的通信如果RedisCluster的虚拟槽算法解决了数据拆分和存储的问题,那么存储缓存数据的节点之间如何通信就是我们接下来要讨论的。缓存节点存储缓存数据。在RedisCluster分布式部署下,缓存节点会被分配给一台或多台服务器。图2:新上线的缓存节点2与缓存节点1通信。缓存节点的数量也可以根据缓存数据量和支持的并发量进行扩展。如图2所示,假设RedisCluster中存在“缓存节点1”,因业务扩展增加了“缓存节点2”。新加入的节点会通过Gossip协议向老节点发送“Meetmessage”。“CacheNode1”收到消息后会礼貌地回复“Pongmessage”。之后,“CacheNode2”会周期性地向“CacheNode1”发送“Pingmessage”,同一个“CacheNode1”每次都会回复“PongMessage”。上面的例子说明RedisCluster中的缓存节点是通过Gossip协议进行通信的。实际上,节点之间通信的目的是维护节点之间的元数据信息。这个元数据是每个节点包含什么数据以及是否有故障。节点通过Gossip协议不断地相互交换这些信息,就像一群人在一起八卦一样。用不了多久每个节点都知道所有其他节点的情况。这种情况就是节点的元数据。整个传输过程大致分为以下几点:RedisCluster的每个缓存节点都会开启一个独立的TCP通道,用于与其他节点进行通信。有节点定时任务,会每隔一定时间从系统中选出“发送节点”。这个“发送节点”按照一定的频率随机向最长时间没有通信的节点发起Ping报文,比如:每秒5次。收到Ping消息的节点用Pong消息回复“发送节点”。不断重复上述行为,使所有节点保持通信。它们之间的通信是通过Gossip协议实现的。从类型上分为四种,分别是:Meetmessage,用于通知新节点加入。就好像上面例子中提到的新节点上线,会向老节点发送Meet消息,表示有“新成员”加入。Ping报文是使用频率最高的报文,将本节点和其他节点的状态数据封装在报文中,定时发送给其他节点。Pong消息,在收到Meet和Ping消息后,也会将自己的数据状态发送给对方。同时,它还可以向集群中的所有节点广播,告知每个人自己的状态。失败消息,如果一个节点下线或者挂了,它会向集群广播这个消息。图3:Gossip协议结构Gossip协议的结构如图3所示,其中type定义了消息的类型,如:Meet、Ping、Pong、Fail等消息。另外还有一个myslots数组,定义了节点负责的slot信息。每个节点向其他节点发送Gossip协议最重要的是将信息告诉其他节点。此外,消息正文通过消息请求的clusterMsgData对象传递。请求分布式缓存的路由对,分布式缓存的节点通过Gossip协议相互发送消息,以保证节点了解彼此的情况。那么对于外界来说,Redis客户端如何通过分布式节点获取缓存数据,就是分布式缓存路由要解决的问题。上面提到,Gossip协议会将各个节点管理的slot信息发送给其他节点,使用unsignedcharmyslots[CLUSTER_SLOTS/8]这样的数组来存储各个节点的slot信息。myslots属性是一个位数组,其中CLUSTER_SLOTS是16384。这个数组的长度是16384/8=2048字节。由于每个字节包含8位(二进制位),所以一共包含16384位,即16384个二进制位。每个节点使用一个位来标识它是否拥有某个槽的数据。如图4所示,假设这张图表示的是节点A所管理的slot的情况。图4:0、1、2这三个数组下标用于存储二进制数组中的slot信息,表示0的三个slot,1,2.如果对应的二进制值为1,则表示该节点负责存储0,1,2这三个槽数据。同样,后面的数组下标0表示该节点不负责存储对应槽位的数据。使用二进制存储的好处是判断效率高。例如,对于编号为1的时隙,节点只需要判断序列的第2位,时间复杂度为O(1)。图5:接收节点在本地保存节点slot的相应信息。如图5所示,接收节点接收到发送节点的节点slot信息后,会将信息保存在本地的clusterState结构体中,其中的Slots数组就是存储每个slot对应的节点信息。图6:ClusterStatus的结构以及slot和node的对应关系如图6所示,ClusterState中存储的Slots数组中的每个下标对应一个slot,每个slot信息对应一个clusterNode,也就是缓存的节点。这些节点会对应一个实际的Redis缓存服务,包括IP和Port信息。RedisCluster的通信机制实际上保证了每个节点与其他节点和slot数据之间存在对应关系。无论Redis客户端访问集群中的哪个节点,都可以路由到对应的节点,因为每个节点都有一个ClusterState,记录了所有槽和节点的对应关系。我们来看看Redis客户端是如何通过路由调用缓存节点的:图7:MOVED重定向请求如图7所示,Redis客户端通过CRC16(key)%16383计算Slot的值,发现需要找到“Cachenode1”读/写数据,但是由于缓存数据迁移或其他原因,该对应Slot的数据被迁移到“cachenode2”。那么这个时候Redis客户端是无法从“缓存节点1”获取数据的。但是,由于“缓存节点1”存储了集群中所有缓存节点的信息,它知道这个槽的数据存储在“缓存节点2”中,所以它向Redis客户端发送MOVED重定向请求。这个请求告诉它它应该访问的“缓存节点2”的地址。Redis客户端拿到这个地址,继续访问“缓存节点2”,获取数据。上面的例子表明数据槽已经从“缓存节点1”迁移到“缓存节点2”,所以客户端可以直接向“缓存节点2”索取数据。那么如果两个缓存节点都在迁移节点数据,此时客户端请求会如何处理呢?图8:ASK重定向请求如图8所示,Redis客户端向“缓存节点1”发送请求。“缓存节点1”正在向“缓存节点2”迁移数据。如果没有命中对应的Slot,则返回ASK重定向请求给客户端,并告知“缓存节点2”的地址。客户端向“缓存节点2”发送Asking命令,询问“缓存节点2”上是否有需要的数据,“缓存节点2”收到消息后返回数据是否存在的结果。缓存节点的扩缩容作为缓存节点的分布式部署,总会存在缓存扩容和缓存失效的问题。这样就会导致缓存节点的上下线问题。由于槽数据保存在每个节点中,当缓存节点数量发生变化时,这些槽数据将根据相应的虚拟槽算法迁移到其他缓存节点。图9:分布式缓存扩展如图9所示,集群中有“缓存节点1”和“缓存节点2”。此时“缓存节点3”在线并加入集群。此时,根据虚拟槽位算法,当有新节点加入时,会将“缓存节点1”和“缓存节点2”中槽位对应的数据迁移到“缓存节点3”。对于节点扩容,新建的节点需要运行在集群模式下,所以新建节点的配置要和集群中其他节点的配置保持一致。当一个新节点加入集群时,作为孤儿节点,它不与其他节点通信。因此,它将使用clustermeet命令加入集群。在集群中的任意节点上执行clustermeet命令以允许新节点加入。假设新节点为192.168.1.15002,旧节点为192.168.1.15003,则运行如下命令将新节点添加到集群中。192.168.1.15003>clustermeet192.168.1.15002这是老节点发起的,意思是老成员欢迎新成员加入。新节点刚刚创建了没有slot对应的数据,也就是说还没有缓存数据。如果该节点是主节点,则需要扩容槽数据;如果该节点是从节点,则需要同步主节点上的数据。简而言之,就是同步数据。图10:节点迁移槽数据流程如图10所示,客户端发起节点间的槽数据迁移,数据从源节点迁移到目标节点:目标节点准备导入槽数据。这里使用clustersetslot{slot}importing{sourceNodeId}命令。然后向源节点发送命令,让源节点准备将对应的slot数据迁移出去。使用命令clustersetslot{slot}导入{sourceNodeId}。至此,源节点准备好迁移数据,迁移前已获取待迁移数据。通过命令clustergetkeysinslot{slot}{count}。Count表示迁移的Slot数。然后在源节点执行migrate{targetIP}{targetPort}“”0{timeout}keys{keys}命令将获取到的key通过pipeline批量迁移到目标节点。重复步骤3和4,不断将数据迁移到目标节点。目标节点获取迁移的数据。数据迁移完成后,目标节点会通过clustersetslot{slot}node{targetNodeId}命令通知相应的slot分配给目标节点,并将该信息广播给全网其他主节点更新其自身自己的slot节点对应表。既然有缓存服务器在线运行,那么也就有离线运行。离线操作正好与在线操作相反,将要离线的缓存节点的槽数据分配给其他缓存主节点。迁移过程也和上线操作类似,不同的是下线时需要通知全网其他节点忘记自己。此时通过命令clusterforget{downNodeId}通知其他节点。当节点收到forget命令后,会将下线的节点放入only-use列表中,之后就不需要再向该节点发送GossipPing消息了。但是这个table-only表的超时时间是60秒。在此时间之后,Ping消息仍将发送到该节点。不过可以使用redis-trib.rbdel-node{host:port}{donwNodeId}命令来帮助我们完成离线操作。特别是当下线节点是主节点时,会安排相应的从节点接替主节点的位置。故障发现与恢复前面讲到缓存节点的扩容和缩容的时候,提到了缓存节点缩容的时候,会有一个下线的动作。有时候是为了节省资源,或者是有计划的下线,但更多的时候是节点故障导致下线。下线失败的判断有两种方式:主观下线:当节点1例行向节点2发送Ping报文时,如果节点2工作正常,则返回Pong报文,并记录节点1的状态相关信息。同时,节点1收到Pong消息后,也会更新与节点2最近一次通信的时间,如果此时两个节点因为某种原因断开连接,节点1会在一段时间后主动连接到节点2的时间。如果一直通讯失败,节点1将无法更新与节点2的最后一次通讯时间。此时,当节点1的定时任务检测到与节点2的最佳通信时间超过cluster-node-timeout时,会更新本地节点的状态,更新节点2主观下线。这里的cluster-node-timeout是发现节点宕机的超时时间。如果节点返回的Pong消息超过这个时间,则认为节点宕机。这里的主观下线是指节点1主观上认为节点2没有返回Pong消息,所以认为节点2下线了。节点1和节点2之间的网络可能断开了只是节点1的主观意见,但是其他节点仍然可以和节点2通信,所以主观下线并不代表一个节点真的下线了。目标下线:由于RedisCluster的节点不断地与集群??中的节点通信,因此下线信息也会通过Gossip消息传递给所有节点。因此,集群中的节点将继续接收离线报告。当超过半数持有槽位的主节点将节点标记为主观下线时,将触发客观下线过程。也就是说,只有当集群中超过一半的master节点主观上认为某个节点下线时,才会启动这个过程。这个过程有一个前提,就是直接针对master节点,如果是slave节点则忽略。也就是说,集群中的一个节点每次接收到其他节点的主观下线,都会做如下的事情。将主观离线报告保存到本地ClusterNode结构中,检查主观离线报告的时效性。如果超过cluster-node-timeout*2的时间,则忽略此报告。否则,记录报告内容,当报告的主观节点标记下线的数量大于或等于主节点持有槽数时,标记为客观下线。同时向集群广播Fail消息,通知所有节点将故障节点标记为客观下线。此消息指的是故障节点的ID。之后,组内所有节点都会将该节点标记为客观下线,并通知故障节点从该节点开始故障转移的过程,即故障恢复。客观下线说白了就是整个集群中有一半的节点认为某个节点主观下线了,那么这个节点就被标记为客观下线。如果认为主节点客观下线,则需要从其从节点中选出一个节点来代替主节点的位置。此时,离线主节点的所有从节点负责恢复,这些从节点会定时监测主节点是否离线。一旦发现掉线,会经过以下恢复过程:①资格检查,每个节点都会检查自己与主节点断开连接的时间。如果这个时间超过cluster-node-timeout*cluster-slave-validity-factor(slave节点有效性因子,默认为10),那么就没有failover的资格。也就是说,从节点与主节点断开连接的时间太长,主节点的数据也很长时间没有同步。不适合成为新的主节点,因为成为主节点后,其他从节点会同步自己的数据。②触发选举,通过以上资格的从节点可以触发选举。但是有一个开始选举的顺序,是根据replicationoffset的大小来判断的。该偏移量记录了用于执行命令的字节数。主服务器每向从服务器传输N个字节,就会将自己的副本偏移量偏移+N,当从服务器收到主服务器发送的N个字节的命令时,会将自己的副本偏移量偏移+NN.复制偏移量越大,从节点的延迟越低,即从节点与主节点通信越频繁,从节点上的数据也会更新,所以从节点越大复制偏移量将首先发起选举。③要发起选举,首先每个主节点都会更新配置epoch(clusterNode.configEpoch),这是一个不断增加的整数。在节点上与Ping/Pong消息交互也会更新这个值,它们都会将最大值更新到它们的配置纪元。这个值记录了每个节点的版本和整个集群的版本。每当发生重要事情时,例如:出现一个新节点,从该节点中选择。会增加全局配置的epoch,分配给相关的主节点来记录这个事件。说白了,更新这个值的目的是保证所有的主节点对这个“大事件”保持一致。每个人都统一成一个配置epoch(一个整数),也就是说每个人都知道这个“大事件”。更新配置时代后,您将需要在组内发起广播选举消息(FAILOVER_AUTH_REQUEST)。并且保证每个从节点在一个配置周期内只能发起一次选举。④投票,只有主节点参与投票,从节点没有投票权。超过一半的主节点通过某个节点成为新的主节点,投票完成。如果从节点在cluster-node-timeout*2时间内没有获得足够多的票数,则本次选举无效,进行第二轮选举。这里每个候选从节点都会收到其他主节点的投票。在第2步领先的从节点通常会在此时获得更多的选票,因为它更早地触发了选举。拿到票的几率更大,而且因为和原来的主节点延迟少,理论上数据会更新一点。⑤当满足投票条件的从节点被选中时,将触发更换主节点的操作。选举出新的主节点后,删除原主节点负责的槽数据,将这些槽数据添加到自己的节点中。并且广播让其他节点知道,新的主节点诞生。总结本文以RedisCluster提供的分布式缓存解决方案为切入点,描述了该方案中缓存节点的分区方式。虚拟槽的分区算法将整块数据分配给不同的缓存节点,数据通过Slot和Node的对应关系找到节点的位置。对于分布式部署的节点,需要通过Gossip协议进行Ping、Pong、Meet、Fail通信,以达到互通的目的。当客户端调用缓存节点数据时,通过MOVED和ASKED重定向请求找到正确的缓存节点。还介绍了扩缩容缓存时需要注意的处理流程,以及数据迁移的方式。最后,描述了如何发现故障(主观离线和客观离线)以及如何从故障中恢复(选举节点)。【原创稿件,合作网站转载请注明原作者和出处为.com】