目录
一、Redis主从集群
主从数据同步原理
全量同步
1)为什么是基本一致而不是完全一致呢?
2)上述过程还有一个问题,怎么判断是不是第一次同步?
增量同步
1)master节点怎么知道slave节点与自己的数据差异在哪里呢?
2)那么如何减少repl_baklog大小上限超出问题的出现呢?
全量同步和增量同步的区别
二、Redis哨兵
背景
哨兵工作原理
1)Sentinel怎么知道一个Redis节点是否宕机呢?
主观下线
客观下线
2)如何选举新的master?
3)如何实现故障转移?
三、Redis分片集群
背景
散列插槽
插槽原理
1)那为什么要做这个插槽呢?
2)那为什么数据key要与插槽绑定而不是与节点绑定?
3)Redis如何判断某个key应该在哪个实例?
4)如何将同一类数据固定的保存在同一个Redis实例?
集群伸缩
故障转移
手动转移
一、Redis主从集群
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。
主从数据同步原理
全量同步
主从第一次建立连接时,会执行全量同步,将master节点的所有数据都拷贝给slave节点。
具体流程:判断是否是第一次同步,如果是,返回版本信息(replication id 和offset),将salve节点的版本信息变为master的,随后进行全量同步。全量同步,即为进行bgsave命令进行异步生成RDB快照并发送给salve节点,salve节点拿到RDB快照之后就等于拿到了master主节点的数据,接下来把salve节点自己本地的数据情清空,然后把RDB快照加载到内存中。此时就能确保主从节点上的数据基本一致。
1)为什么是基本一致而不是完全一致呢?
因为在生成RDB快照过程中,主进程还可能会去处理用户的请求,就意味着还可能在新的数据写入。这些写操作会进入repl_baklog缓冲区,repl_baklog缓冲区里记录的就是在这个RDB期间收到的一些新的写命令。最后把这些写命令也发送给slave节点,就完成了主从数据的一致。
2)上述过程还有一个问题,怎么判断是不是第一次同步?
判断依据:
- Replication Id:简称replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave则会继承master节点的replid。
- offset:偏移量,随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。
因此slave节点做数据同步,必须向master节点声明自己的replication id 和offset,master节点才可以判断到底需要同步哪些数据。
因为slave节点原本也是一个master节点,有自己的replid和offset,(宕机重启后)当第一次变成slave节点,与master节点建立连接时,发送的replid和offset是自己的replid和offset。master节点判断发现slave节点发送来的replid与自己的不一致,说明这是一个全新的slave节点,就知道要做全量同步了。master节点会将自己的replid和offset都发送给这个slave节点,slave节点保存这些信息。以后slave节点的replid就与master节点一致了。因此,master节点判断一个节点是否是第一次同步的依据就是看replid是否一致。
因此,完整流程描述如下:
- slave节点请求增量同步。
- master节点判断replid,发现不一致,拒绝增量同步。
- master节点将完整内存数据生成RDB,发送RDB到slave节点。
- slave节点清空本地数据,加载master的RDB节点。
- master节点将RDB期间的命令记录在repl_baklog,并持续将log中的命令发送给slave节点。
- slave节点执行接收到的命令,保持与master节点之间的同步。
增量同步
全量同步需要先做RDB,然后将RDB文件通过网络传输给slave节点,成本太高了。因此除了第一次做全量同步,其它大多数时候slave节点与master节点都是做增量同步。
什么是增量同步?就是只更新slave节点与master节点存在差异的部分数据。即只用更新offset后的命令即可,offset之后的命令就是主从之间的数据差异,只更新这部分即可。
1)master节点怎么知道slave节点与自己的数据差异在哪里呢?
这就要说到全量同步时的repl_baklog文件了。这个repl_baklog文件是一个固定大小的数组,只不过数组是环形,也就是说角标到达数组末尾后,会再次从0开始读写,这样数组头部的数据就会被覆盖。
repl_baklog中会记录Redis处理过的命令日志及offset,包括master节点当前的offset、slave已经拷贝到的offset,即主从之间的offset差就是要同步到从节点的数据。
随着不断有数据写入,master节点的offset逐渐变大,记录的最新位置就是master节点当前的offset。同时slave节点也不会去做数据同步,同步的位置就是slave的offset,追赶master节点的offset。因此,slave节点与master节点的offset之间的差异,就是salve节点需要增量拷贝的数据了。
所以可以理解成,repl_baklog其实就是slave节点与master节点之间的数据差异的一个缓冲区。只要slave节点与master节点之间的数据差异不超过这个环的存储上限,那你永远都能够这个环里找到你所需要的数据,永远能实现增量同步。
但是如果slave节点与master节点之间的数据差异差太多,已经超过这个环的存储上限,那么这个时候就没办法做增量同步了。
我们继续分析,直到数组被填满,
此时,如果有新的数据写入,就会覆盖数组中的旧数据。不过,旧的数据只要是绿色的,说明是已经被同步到slave的数据,即便被覆盖了也没什么影响。因为未同步的仅仅是红色部分。
但是,如果slave出现网络阻塞,导致master的offset远远超过了slave的offset,马上就要超过这个个环的上限。如果master继续写入新数据,其offset就会覆盖旧的数据,直到将slave现在的offset也覆盖,注意此时master已经覆盖了从节点尚未同步过去的数据,这就出现了主从之间数据的不一致。
棕色框中的红色部分,就是尚未同步,但是却已经被覆盖的数据。此时如果slave恢复,需要同步,却发现自己的offset都没有了,无法完成增量同步了。只能做全量同步。 这就保证了主从数据的一致性。
总结一下,repl_baklog大小有上限,写满后会覆盖最早的数据。如果slave断开时间过久,导
致尚未备份的数据被覆盖,则无法基于log做增量同步,只能再次全量同步。
2)那么如何减少repl_baklog大小上限超出问题的出现呢?
主从同步可以保证主从数据的一致性,非常重要。 可以从以下几个方面来优化Redis主从就集群:
- 在master中配置repl-diskless-sync yes启用无磁盘复制,直接通过网络传输,把数据发送给从节点,只适用于网络较快的设备,这样就能避免全量同步时的磁盘IO,提高全量同步的性能。
- Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO,提高全量同步的性能。
- 适当提高repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步。
- 限制一个master上的slave节点数量,如果实在是太多slave,则可以采用主-从-从链式结构,减少master压力。
全量同步和增量同步的区别
(1)全量同步:master将完整内存数据生成RDB,发送RDB到slave。后续命令则记录在repl_baklog,逐个发送给slave。
什么时候执行全量同步?
- slave节点第一次连接master节点时。
- slave节点断开时间太久,repl_baklog中的offset已经被覆盖时。
(2)增量同步:slave提交自己的offset到master,master获取repl_baklog中从offset之后的命令给slave
什么时候执行增量同步?
- slave节点断开又恢复,并且在repl_baklog中能找到offset时。
二、Redis哨兵
背景
主从结构中,如果一个slave节点宕机,根本不用担心。因为只要这个slave节点重启,就能从master节点上完成数据同步恢复数据。
那如果宕机的不是slave节点而是master节点呢?如果说已经做了master节点的持久化,那master节点重启是可以解决这个问题的,数据也不会丢失。但是如果说在master节点宕机的这段时间内,还有在重启数据恢复的过程中,由于目前master节点是宕机状态,用户是无法执行写操作,也就意味着这个集群的可用性就下降了。
那么有什么办法能保证主从集群的高可用性呢?我们只需要去监控集群中的节点状态, 当发现master节点宕机的那一刻,立即选出一个新的slave节点作为新的master节点,因为这个slave节点本身一直在数据同步,有完整的数据,把它变成master节点很容易,这个时候我们整个集群依然是健康的,不仅可以进行读操作还可以写操作。至于那个宕机的master节点,将来等它启动之后变成slave节点就可以了。
这样就实现了主从切换,也就意味着这个集群的高可用性可以得到一个很好的保障。
但是这个监测和重启的动作谁来做呢?Redis主从模式当主服务器宕机后,需要手动把一台从服务器切换为主服务器,需要人工干预费事费力,为了解决这个问题出现了哨兵模式。
哨兵工作原理
Redis提供了哨兵
(Sentinel
)机制来监控主从集群监控状态,确保集群的高可用性。
哨兵的作用如下:
- 状态监控:Sentinel 会不断检查您的master和slave是否按预期工作。
- 故障恢复(failover):如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后会成为slave。
- 状态通知:Sentinel充当Redis客户端的服务发现来源,当集群发生failover时,会将最新集群信息推送给Redis的客户端。
那么问题来了,
1)Sentinel怎么知道一个Redis节点是否宕机呢?
Sentinel基于心跳机制监测服务状态。每个Sentinel会每秒钟 一次的频率向它所知的主服务器、从服务器以及其他Sentinel实例发送一个PING命令,获取其拓扑结构和状态信息。然后通过实例的响应结果来做出判断,来确认这些节点是否可达,共同监控数据节点的运行状况。
- 主观下线(sdown):如果某sentinel节点发现某Redis节点未在规定时间响应,则认为该节点主观下线。
- 客观下线(odown):若超过指定数量(通过quorum设置)的sentinel都认为该节点主观下线,则该节点客观下线。quorum值最好超过Sentinel节点数量的一半,Sentinel节点数量至少3台。
主观下线
如果某个sentinel节点发现某实例未在规定的时间内(down-after-milliseconds)响应,则认为实例下线了。这种下线是主观下线。
哨兵每秒向所有与它创建命令连接的实例(包括主服务器、从服务器、其他sentine)发送PING命令,并通过实例返回的PING命令回复来判断是否在线。收到的有效回复为:+PONG、-LOADING、-MASTERDOWN命令。其余的都是无效回复(也包括没有回复的)。
如果在down-after-milliseconds毫秒以内,sentinel收到的都是无效回复,那么这个sentinel就会认为实例进入主观下线状态,同时修改实例结构中的flags属性,改为SRI_S_DOWN(主观下线)。
down-after-milliseconds毫秒不仅会成为sentinel判断master进入主观下线的标准,还会成为sentinel判断master属下所有从服务器,以及所有同样监视master的其他sentinel进入主观下线的依据。
客观下线
如果超出指定数量(quorum)的sentinel都认为该实例主观下线,则该实例就。quorum值最后超过sentinel实例数量的一半。
当sentinel将一个主服务器判断为主观下线后,为了确认这个主服务器是否真的下线了,它(当前这个sentinel)会向同样监视这一主服务器的其他sentinel进行询问,看看它们是否也认为当前主服务器已经进入了下线状态(无论是主观的还是客观的)。
根据其他sentinel回复的SENTINEL is-master-down-by-addr命令,sentinel将统计其他sentient是否同意主服务器已经下线的数量。当这个数量达到配知道你个判断客观所需要的数量时候,也就是当sentinel从其他sentinel那接收到足够数量的已下线判断之后,sentient会将主服务器实例结构flags属性的SRI_O_DOWN标识打开,标识主服务器已经进入客观下线状态,并对主服务器执行故障专业操作。
2)如何选举新的master?
一旦发现master故障,sentinel需要在salve中选择一个作为新的master。
选择依据是这样的:
首先会判断slave节点与master节点断开时间长短,如果断开时间超过down-after-milliseconds * 10,因为断开时间越长证明丢失的数据越多,则就会直接排除该slave节点,它就不具有选举权。这样就可以排除掉一部分数据是旧的slave节点。
然后判断slave节点的slave-priority值(默认都是1),这个slave-priority值越小优先级越高,如果是0则永不参与选举。
如果slave-prority一样,则判断slave节点的offset值,代表当前slave节点与master节点之间数据同步的进度,offset值越大证明这个当前slave节点与master节点之间的数据月接近,说明数据越新,优先级越高。因为我们要优先保证数据的完整性。
假设所有的slave节点的offset值都一样,那最后是判断slave节点的run_id大小,这个run_id(通过info server可以查看run_id)是在slave节点启动时的那一刻由Reids自动生成的一个id。这个run_id其实大小并不重要,此时我们就随便挑一个,run_id越小优先级越高。
那么,问题来了,当选出一个新的master后,该如何实现身份切换呢?
3)如何实现故障转移?
当选中了其中一个slave为新的master后(例如slavel),故障的转移的步骤如下:
假设现在的master宕机了,此时slave节点7002被选择作为新的master节点。
那么在做故障转移的时候,第一步sentinel会向salve节点发起一个请求,告诉salve节点去执行slaveof no one 命令,执行命令之后这个salve节点就会成为master节点。
第二步就是广播,sentinel给所有其他的slave节点发送slaveof xxx.xxx.xx.xx 端口。比如我们新master节点是192.168.1.11 7002,那么命令就是 slaveof 192.168.1.11 7002命令,让这些slave成为新master的从节点,开始从新master上同步数据。
最后,sentinel会强制修改故障节点的配置文件,在配置文件中设置slaveof 192.168.1.11 7002命令,也就是把它标记为slave。一旦配置文件修改了之后,当故障节点恢复后自动成为新master的slave节点。
此时,所有的slave就都成为了新master的从节点,整个主从切换就完成了。
三、Redis分片集群
背景
主从集群可以去应对Redis的高可用、高并发读的问题。但是Redis主从之间也会做同步,为了提高主从同步的性能,我们单节点的Redis的内存设置不能太高。如果内存占用过多,在RDB持久化的时候或者做全量同步的时候,会导致大量的IO,性能会下降。
那如果单节点的Redis的内存上限降低了,那如果有海量数据存储需要存储的时候怎么办?遇到了高并发写的问题该如何解决?
总结就是,但依然有两个问题没有解决:
-
海量数据存储
-
高并发写
要解决这两个问题就需要用到分片集群了。分片的意思就是把数据拆分存储到不同节点,这样整个集群的存储数据量就更大了。
分片集群特征:
- 集群中有多个master,每个master保存不同分片数据 ,解决海量数据存储问题。同时每个master都可以写操作,并发写能力也得到提升。
- 每个master都可以有多个slave节点 ,同时具备了主从集群的特性,确保高可用。
- master之间通过ping监测彼此健康状态 ,类似哨兵作用。
- 客户端请求可以访问集群任意节点,最终都会被转发到数据所在节点。
散列插槽
插槽原理
Redis会把每一个master节点映射到0~16383共16384个插槽(hash slot)上,查看集群信息时就能看到,
1)那为什么要做这个插槽呢?
既然每个master都可以存储数据,那存储到哪一个里面,以后怎么取。插槽就是来解决这个问题的。
数据key不是与节点绑定,而是与插槽绑定。redis会根据key的有效部分计算插槽值,分两种情况:
- key中包含"0",且“0”中至少包含1个字符,,“0!”中的部分是有效部分。
- key中不包含“!”,整个key都是有效部分。
例如,key是num,那么就根据num计算,如果是{itcast}num,有效部分就是itcast,则根据itcast计算。计算方式是利用CRC16算法得到一个hash值,然后对16384取余,得到的结果就是slot值。
2)那为什么数据key要与插槽绑定而不是与节点绑定?
正因为Redis的master节点是可能出现宕机情况的,或者是集群扩容增加了节点,或者是集群伸缩删除了节点。那如果一个节点删除或者宕机了,节点上面的数据也会跟着丢失。
而如果数据是与插槽绑定,那当节点删除或者宕机时,可以将这个节点对应的插槽转移到活着的节点。集群扩容时也可以将插槽进行转移。这样数据跟着插槽走就永远可以找到数据所在的位置。
3)Redis如何判断某个key应该在哪个实例?
- 将16384个插槽分配到不同的实例。
- 根据key的有效部分计算哈希值,对16384取余。
- 余数作为插槽,寻找插槽所在实例即可。
如图,在7001这个节点执行set a 1时,对a做hash运算,对16384取余得到的结果是15495,而15495槽是在7003这个节点上,因此此时会重定向到7003节点取值。在7003节点执行 get a就可以拿到a对应的value值1。在7003节点执行get num命令时,对num做hash运算,对16384取余,得到的结果是2765,因此会重定向到7001节点取值。
4)如何将同一类数据固定的保存在同一个Redis实例?
这一类数据使用相同的有效部分,例如key都以{typeld}为前缀。
集群伸缩
redis-cli --cluster提供了很多操作集群的命令,可以通过下面方式查看:
比如,添加节点的命令:
故障转移
分片集群虽然没有哨兵机制,但是也具有故障转移的功能。
当集群中有一个master宕机会发生什么呢?
- 首先是该实例与其它实例失去连接。
- 然后是疑似宕机。
- 最后是确定下线,自动提升一个slave为新的master。
手动转移
利用cluster failover命令可以手动让集群中的某个master宕机,切换到执行cluster failover命令的这个slave节点,实现无感知的数据迁移。
大致的流程:在一个slave节点上执行cluster failover命令,那么这个slave节点对应的master节点会被替换掉,这个slave节点会成为新的主节点,而那个对应的master节点会成为从节点。
具体流程如下:
手动的Failover支持三种不同模式:
- 缺省:默认的流程,如图1~6歩
- force:省略了对offset的一致性校验
- takeover:直接执行第5歩,忽略数据一致性、忽略master状态和其它master的意见
当在一个slave节点上执行cluster failover命令的时候,slave节点会向master节点发一个消息,告诉master节点我要替换你了。这个时候为了避免消息的丢失,master节点会拒绝客户端的一切请求,返回当前的offset值给slave节点,告诉slave节点我现在的数据已经最新到这个位置了。
此时,slave节点和master节点会检查当前的这个offset值是否一致。如果slave节点和master节点的数据不一致,slave节点会进行数据同步。数据同步完成之后,slave节点和master节点数据就会完全一致(因为此时已经不接收新的客户端请求了)。
数据完全一致之后,就可以进行故障转移了。完成之后,让slave把自己标记成一个master,广播通知集群中的每一个节点。当集群中的每一个节点收到消息之后,以后就和这个新的master节点做交互。并且原来的那个master节点接收到消息之后,就会知道与新的master节点之间的数据同步和转移已经结束了,会转为一个slave节点。