本文将对集群的各个方面进行深入的拆解,比如节点、slot分配、命令执行、resharding、steering、failover、messages。Redis集群原理概述的目的是为了掌握什么是Cluster?clustersharding的原理,client定位数据的原理,failover,master的选择,什么场景使用Cluster,如何部署集群……把数据分成多个副本,分别存储在不同的实例上。Slot和Redis实例映射复制和Failover故障检测Failover选择Master进程用一张表保存Key-Value对和实例的关系为什么需要集群开销?65哥:码哥,自从用了你说的sentinel集群实现了故障自动切换,终于可以和闺蜜开心了,也不怕Redis深夜宕机了。但是最近我遇到了一个可怕的问题。Redis需要保存800万个键值对,占用内存20GB。我用了一台32G内存的主机部署,但是Redis有时候响应很慢。使用INFO命令查看latest_fork_usec指标(最新的fork耗时)发现特别高。主要是RedisRDB持久化机制导致的。Redis会Fork子进程来完成RDB的持久化操作。fork的执行耗时与Redis的数据量呈正相关。阻塞主线程,因为数据量大导致阻塞主线程时间过长,所以Redis出现响应慢。65哥:随着业务规模的扩大,数据量越来越大。主从架构下单个实例的硬件很难升级。扩容、保存大量数据会导致响应变慢。有什么办法解决吗?要保存大量数据,除了使用大内存主机外,我们还可以使用分片集群。如果一台机器不能保存所有数据,那么多台机器将共享它。使用RedisCluster集群主要解决大量数据存储带来的各种慢问题,也方便横向扩展。这两种方案对应Redis数据增长的两种扩展方案:垂直扩展(scaleup)和水平扩展(scaleout)。纵向扩展:升级单个Redis的硬件配置,如增加内存容量、磁盘容量、使用更强大的CPU等。横向扩展:横向增加Redis实例数量,每个节点负责一部分数据。例如,如果您需要一个24GB内存和150GB磁盘的服务器资源,有两种选择:水平扩展和垂直扩展。当面对百万级或者千万级的用户规模时,水平扩展的Redis分片集群会是一个非常好的选择。65哥:那么这两种方案各有什么优缺点呢?垂直扩展部署简单,但是当数据量大,使用RDB持久化时,会造成拥塞,响应慢。另外,受限于硬件和成本,扩展内存的成本太高,比如扩展到1T内存。水平扩展容易扩展,无需担心单实例的硬件和成本限制。但是分片集群涉及到多个实例的分布式管理,需要解决如何将数据合理的分布到不同的实例中,同时让客户端能够正确访问实例上的数据。什么是ClusterRedis集群是一种分布式数据库解决方案。集群通过分片(一种“分而治之”的做法)来管理数据,并提供复制和故障转移功能。将数据分成16384个槽,每个节点负责一部分槽。槽信息存储在每个节点中。它是去中心化的。如图所示,集群由三个Redis节点组成。每个节点负责整个集群的一部分数据。每个节点负责的数据可能不同。Redis集群架构的三个节点相互连接,形成对等集群。它们通过Gossip协议相互交换集群信息。最后,每个节点保存其他节点的时隙分配。开放消息技术不是万能的,程序员也不是最好的,所以一定要搞清楚,不要认为“我是世界上最好的”。一旦我们有了这种意识,它可能会延迟我们的成长。技术是用来解决问题的。如果一项技术不能解决问题,那么这项技术就毫无价值。不要炫耀,没有意义。集群安装点击->《Redis 6.X Cluster 集群搭建》查看一个Redis集群通常由多个节点(nodes)组成。一开始,每个节点都是相互独立的,它们都在一个只包含它们自己的集群中。对于真正工作的集群,我们必须连接各个节点以形成包含多个节点的集群。连接各个节点的工作可以通过CLUSTERMEET命令来完成:CLUSTERMEET。向某个node节点发送CLUSTERMEET命令,使该node节点与ip和port指定的节点握手。当握手成功后,node节点会将ip和port指定的节点添加到node节点在集群中的当前位置。CLUSTERMEET就像一个node节点在说:“嘿,ip=xx,port=xx的兄弟,要不要加入《字节码哥》技术群?加入集群后,你会找到一条成长之路一个大神。关注“码哥字节”“杰”公众号回复“嘉群”,如果你是兄弟,就跟我来吧!关于RedisCluster集群搭建的详细步骤,请点击文末左下角“阅读原文”或点击->《Redis 6.X Cluster 集群搭建》查看,关于RedisCluster的官方详细介绍请见:https://redis.io/topics/cluster-tutorial。集群实现原理65师兄:数据分片后,需要将数据分布在不同的实例上,数据和实例之间如何对应?从Redis3.0开始,官方提供了RedisCluster方案来实现分片集群,该方案实现了数据和实例的规则。RedisCluster方案使用哈希槽(HashSlot,我就直接称之为Slots)来处理数据和实例之间的映射关系。跟着“码哥字节”进入Cluster实现探索原理...将数据分成多个部分。集群中不同实例上的整个数据库分为16384个槽。数据库中的每个键都属于16384个槽中的一个。每个节点可以处理0个或最多16384个时隙。key和hashslot的映射过程可以分为两步:根据键值对的key,使用CRC16算法计算出一个16位的值;对16位值取模16384得到0到16383之间的一个数表示key对应的hash槽。Cluster还允许用户强制将一个密钥挂在特定的插槽中。通过在密钥字符串中嵌入标签,可以强制密钥与标签挂在同一个槽中。哈希槽与Redis实例映射65哥:哈希槽是如何映射到Redis实例的?”在部署集群的例子中,是通过clustercreate创建的,Redis会自动在集群实例之间平均分配16384个哈希槽为例如,N个节点,每个节点上的哈希槽数=16384/N。另外,7000、7001、7002这三个节点可以通过CLUSTERMEET命令连接到一个集群,但是集群仍然处于离线状态因为三个实例都没有处理任何hash槽,可以使用clusteraddslots命令指定每个实例上的hash槽个数。65兄:为什么需要手动制定呢,Redis实例的配置添加到集群不一样,如果承受同样的压力,垃圾机就太吃力了,让强大的机器多支持一点,三个实例的集群,使用如下指令,每个实例分配hashslots:实例1负责0到5460个哈希槽,实例2负责5461到10922个哈希槽,实例3负责10923到16383个哈希槽。redis-cli-h172.16.19.1–p6379clusteraddslots0,5460redis-cli-h172.16.19.2–p6379clusteraddslots5461,10922redis-cli-h172.16.19.3–p6379clusteraddslots10923,16383hashslots,pairs数据的映射关系和Redisinstances如下:data,Slot和instance映射Rediskeyvalue对key“codebrotherbyte”和“Nubi”进行CRC16计算后,hashslots总数为16394,取模结果映射为分别是实例1和实例2。请记住,当所有16384个插槽都分配完后,Redis集群才能正常工作。复制与故障转移65哥:Redis集群是如何做到高可用的?Master和Slave读写分开了吗?Master用于处理slot,Slave节点通过《Redis 主从架构数据同步》同步主节点数据。当Master下线时,Slave代替主节点继续处理请求。主从节点之间没有读写分离,Slave只是作为Master宕机时的高可用备份。RedisCluster可以为每个主节点设置若干个从节点。当单个主节点出现故障时,集群会自动将其中一个从节点提升为主节点。如果一个主节点没有从节点,那么当它出现故障时,集群将完全不可用。不过Redis也提供了一个参数cluster-require-full-coverage,允许部分节点失效,其他节点可以继续对外提供访问。例如7000主节点宕机,7003作为从节点继续作为主节点提供服务。当下线节点7000重新上线后,会成为当前70003的从节点。故障检测65师兄:在《Redis 高可用篇:Sentinel 哨兵集群原理》中,我知道Sentinel通过监听实现自动故障转移,自动切换主库,通知客户端。Cluster如何实现自动故障转移?一个节点认为某个节点断开了,但不代表所有节点都认为它断开了。只有当大部分负责处理slot的节点已经确定一个节点下线时,集群才认为该节点需要进行主从切换。Redis集群节点使用Gossip协议来广播自己的状态和对整个集群的感知变化。例如,如果一个节点发现一个节点断开连接(PFail),它会向整个集群广播这个信息,其他节点也能收到这个断开连接的信息。关于Gossip协议,可以看悟空大哥的一篇文章:《病毒入侵,全靠分布式》如果一个节点收到某个节点的丢失连接数(PFailCount),并且已经达到集群的多数,就可以将该节点标记为a确认下线状态(Fail),然后向整个集群广播,迫使其他节点也接受该节点已经下线的事实,并立即对丢失的节点进行主从切换。Failover当一个Slave发现自己的主节点已经进入下线状态时,从节点就会开始对下线的主节点进行故障转移。从离线的Master和Slave节点列表中选择一个节点成为新的Master节点。新的主节点会撤销对离线主节点的所有槽位分配,并将这些槽位分配给自己。新的主节点向集群广播一条PONG消息。这个PONG消息可以让集群中的其他节点立即知道该节点已经从从节点变成了主节点,并且主节点已经接管了离线节点的责任。加工槽。新的主节点开始接收与处理槽相关的命令请求,故障转移完成。master选举过程65哥:新的master节点是怎么选举出来的?集群的配置epoch+1是一个self-ever计数器,初始值为0,每次执行failover都会+1。检测到主节点下线的从节点向集群广播一个CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到该消息并有投票权的主节点投票给这个从节点。主节点还没有给其他从节点投票,那么主节点会向从节点返回CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息请求投票,表示主节点支持从节点成为新的主节点。参与选举的从节点会收到CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息。如果收集到的票数>=(N/2)+1支持,则从节点将被选举为新的主节点。如果在一个配置epoch中没有从节点能够收集到足够的支持票,则集群进入一个新的配置epoch并重新进行选举,直到选出新的主节点。和Sentinel类似,都是基于Raft算法实现的。流程如图:集群leader选举用一张表来存储key-value对和instance之间的关联关系是否可行?65哥,我来考考你:《RedisCluster方案》中的键值对是通过哈希槽的方式分配给不同的实例,这个过程需要对键值对的key进行CRC计算,以及总数的取模映射hashslots到instances.如果用一张表直接赋值key-value记录pair和instance的对应关系(比如key-valuepair1在instance2上,key-valuepair2在instance2上实例1),这样就不用计算key和hashslot的对应关系了,直接查表就可以了,Redis为什么不这样做呢?如果使用全局表记录,如果键值对与实例之间的关系发生变化(重新分片、实例增减),则需要修改表。如果是单线程操作,所有操作都要序列化,性能太慢。多线程涉及锁定。另外,如果键值对的数量很大,保存键值对与实例关系的表数据所需的存储空间也会很大。对于哈希槽的计算,虽然也记录了哈希槽与实例时间的关系,但是哈希槽的数量要少很多,只有16384个,开销很小。客户端如何定位数据所在的实例65哥:客户端如何判断访问的数据分布在哪个实例上?Redis实例会通过Gossip协议将自己的hashslot信息发送给集群中的其他实例,实现稀有slot分配信息的Diffusion。这样,集群中的每个实例都拥有了所有哈希槽与实例之间的映射关系信息。分片数据时,通过CRC16从key中计算出一个value,然后对16384取模得到对应的Slot。这个计算任务可以在客户端发送请求时执行。但是在定位到slot之后,还需要进一步定位到Slot所在的Redis实例。当客户端连接到任意一个实例时,实例会向客户端响应哈希槽与实例的映射关系,客户端会在本地缓存哈希槽与实例的映射信息。客户端请求时,会计算出key对应的hashslot,通过本地缓存的hashslot实例映射信息定位到数据所在的实例,然后向对应的实例发送请求。Redisclient数据所在的节点重新分配hashslot65哥:新增实例或者负载均衡重新分配,hashslot和实例的映射关系发生变化怎么办?集群中的实例通过Gossip协议相互传递消息获取最新的哈希槽分配信息,然而,客户端并不知道。RedisCluster提供了一种重定向机制:客户端向一个实例发送请求,而这个实例没有对应的数据,Redis实例会告诉客户端将请求发送给另一个实例。65兄:Redis如何告诉客户端重定向访问新实例?有两种情况:MOVED错误和ASK错误。MOVEDerrorMOVED错误(负载均衡,数据已迁移到其他实例):当客户端向某个实例发送key-value对操作请求,而key所在slot不归该实例负责时,该实例将返回一个MOVED错误,指向负责槽的节点。GET公众号:codebyte(error)MOVED16330172.17.18.2:6379该响应表示客户端请求的键值对所在哈希槽16330已经迁移到实例172.17.18.2,端口为6379,这样客户端就与172.17.18.2:6379建立连接,并发送GET请求。同时,客户端也会更新本地缓存,正确更新slot与Redis实例的对应关系。MOVEDcommandASKerror65大哥:如果一个slot的数据很多,有的迁移到新的实例,有的没有迁移。如果在当前节点上找到请求的key,则直接执行命令,否则需要ASK错误响应是的,如果slot部分的迁移没有完成,如果要访问的key所在的Slot正在迁移自实例1迁移到实例2,实例1会向客户端返回一个ASK错误信息:客户端请求的key所在的hashslot迁移到实例2,首先向实例2发送一个ASKING命令,然后发送一个操作命令。GET公众号:codebyte(error)ASK16330172.17.18.2:6379例如,客户端请求在实例172.17.18.1,节点1上定位到key="公众号:codebyte"的slot16330if执行找到直接命令,否则响应ASK错误信息,引导客户端到正在迁移的目标节点172.17.18.2。ASKerror注意:ASKerror命令不会更新客户端缓存的哈希槽分配信息。所以如果client再次请求Slot16330的数据,还是会先向172.17.18.1实例发送请求,但是节点会响应ASK命令让client向新的实例发送请求。MOVED命令更新客户端的本地缓存,以便将后续命令发送到新实例。集群可以设置多大?65哥:有了RedisCluster,大数据量再也不怕了。我可以无限扩张吗?答案是否定的,Redis官方给出的RedisCluster规模是1000个实例。65兄:究竟是什么限制了集群的大小?关键在于实例之间的通信开销。Cluster集群中的每个实例都存储了所有的哈希槽与实例(槽映射到节点的表)的对应关系信息,以及自身的状态信息。集群之间的每个实例通过Gossip协议传播节点数据。Gossip协议的工作原理大致如下:从集群中随机选择一些实例,并以一定的频率向被选中的实例发送PING报文,检测实例状态并交换彼此的信息。PING报文中封装了发送方自身的状态信息、其他一些实例的状态信息以及slot和instance映射表信息。实例收到PING报文后,返回PONG报文,PONG报文包含与PING报文相同的信息。通过集群间的Gossip协议,每个实例在一段时间后都可以获得所有其他实例的状态信息。因此,当一个新节点加入,一个节点发生故障,槽映射变化可以通过PING和PONG消息传播,集群状态可以在每个实例中传播和同步。Gossip消息发送的消息结构由clusterMsgDataGossip结构组成:typedefstruct{charnodename[CLUSTER_NAMELEN];//40bytesuint32_tping_sent;//4bytesuint32_tpong_received;//4bytescharip[NET_IP_STR_LEN];//46bytesuint16_tport;//2字节uint16_tcport;//2字节uint16_tflags;//2字节uint32_tnotused1;//4字节}clusterMsgDataGossip;所以每个实例发送一个Gossip消息,需要发送104个字节。如果集群是1000个实例,每个实例发送一个PING报文会占用大约10KB。此外,Slot映射表在实例间传播时,每条消息还包含一个长度为16384位的Bitmap。每个位对应一个Slot。如果value=1,则表示该Slot属于当前实例。这个Bitmap占2KB,所以一个PING报文大概12KB。PONG与PING消息相同,发送一次和返回的两条消息之和为24KB。随着集群规模的增大,越来越多的心跳消息会占用集群的网络通信带宽,降低集群的吞吐量。实例的通信频率是65哥:码哥,发送PING报文的频率也会影响集群带宽吗?RedisCluster的实例启动后,默认每秒会从本地实例列表中随机抽取5个实例,然后从这5个实例中找到最长时间未收到PING报文的实例,并向该实例发送PING消息。65哥:随机抽取5个,但不能保证选中的实例是整个集群中最长时间未收到PING通信的实例。有些实例可能很久没有收到消息,导致它们维护的集群信息早就过期了。我应该怎么办??这是个好问题。RedisCluster的实例每100毫秒扫描一次本地实例列表。当发现某个实例上次收到PONG消息>cluster-node-timeout/2时,则立即向该实例发送PING消息,更新该节点的集群状态信息。当集群规模变大时,会进一步增加实例间网络通信的延迟。它可能会导致更频繁地发送PING消息。降低实例间的通信开销每秒钟发送一次PING消息,降低这个频率可能会导致集群中各个实例的状态信息无法及时传播。每隔100ms检查实例收到的PONG消息是否超过cluster-node-timeout/2,这是Redis实例默认的周期性检测我们不会轻易修改任务频率。因此,我们只能修改cluster-node-timeout的值:集群中判断实例是否故障的心跳时间,默认为15S。因此,为了避免过多的心跳消息占用集群带宽,设置cluster-node-timeout调整为20秒或30秒,这样可以缓解接收PONG消息的超时。但是,它不能设置得太大。否则,该实例会失败,但它必须等待集群节点-超时时间很长才能检测到此故障,从而影响集群的正常服务。总结《Redis 系列》至今发表了7篇文章。确保每一篇文章都为读者带来价值,让每个人都能得到真正的提升。哨兵集群实现了自动故障转移,但是当数据量过大时,生成RDB的时间过长。但是执行Fork时,主线程会被阻塞。由于数据量大,主线程会阻塞时间过长,所以Redis的响应显得比较慢。使用RedisCluster集群主要解决大量数据存储带来的各种慢问题,也方便横向扩展。当面对百万级或者千万级的用户规模时,scale-out的Redis分片集群会是一个非常好的选择。集群整个数据库分为16384个slot,数据库中的每个key都属于这16384个slot中的一个,集群中每个节点可以处理0个或者最多16384个slot。Redis集群节点使用Gossip协议来广播自己的状态和对整个集群的感知变化。客户端连接到集群中的任意一个实例后,该实例会将哈希槽和实例映射信息发送给客户端,客户端保存这些信息以定位key到对应的节点。集群不能无限增加。由于集群通过Gossip协议传播集群实例信息,通信频率是限制集群规模的主要原因。主要可以通过修改cluster-node-timeout来调整频率。
