深入理解Redis的主从复制集群及哨兵机制

1.Redis的高可用

前面几篇博客我们介绍了Redis的一些基础数据结构和数据的持久化,这些都是基于单机版的redis来介绍的。作为我们生产使用,我们都要保证Redis的高可用,那我们就要进行横向或者纵向的扩展。假如我们拿Redis主要是作为缓存,没有太多的数据存储,那我们可以采用水平扩展,即采用主-从复制的模式,可以是一主一从或者一主多从的方式,实现Redis的高可用,使得服务尽量少的中断。这样,Redis采用这种横向增加副本冗余量,将一份数据同时保存在多个实例上,这样即使一个实例出现了故障,其他实例仍然可以继续提供服务,降低对业务的影响。

2. Redis的主从复制

我们知道,只要涉及到的数据的多个副本,就要考虑数据的一致性,这也是所有数据集群必须要考虑和解决的问题,当然,Redis也不例外。

1.Redis的读写分离方式

Redis的主从模式默认是采用主从库读写分离的方式进行的,对于读操作,主库和从库都可以接收,而对于写操作,首先到主库执行,然后,主库将写操作同步给从库。
在这里插入图片描述

2.Redis为什么采用读写分离方式

Redis采用读写分离的方式,提供单点(Master)的写,多点(Slave)的读,那为什么采取这种方式呢?为什么不让所有实例都能读写呢?那么我们假设,所有节点都可写,那么对同一个key进行多次修改,那么这多次的修改操作都是在不同的实例上,那么这个key的value就有可能产生不一致,那么在读取的时候,各个实例的值就不相同,可能会产生脏读。这时候如果要保证各个节点的数据一致性,那么在往各个实例上写的时候,就要涉及到加锁、数据一致性协商等额外的操作,带来巨大的性能开销(只要加锁,我们基本都可以认为是低效的)。但是采用了读写分离方式就不一样了,所有的修改(新增)操作都只会在Master一个节点上进行,不用三个实例进行相互协调了。主库只要有了数据变化,就直接同步给从库就行了,从而保证数据的一致性(其实是最终一致性)。

3.Redis是怎么进行主从数据同步的

从之前的一篇博客《CentOS-7下Redis主从复制搭建过程》中我们知道,通过replicaof配置,就能建立从库与主库之前的追随关系。当Redis实例启动后,就会进行首次的数据同步过程。

1.主库从库间是怎么进行首次同步的

  1. 从库与主库建立连接,向主库发送同步请求,主库确认请求,并且给从库最初回复,即这是一个准备阶段。具体来说,主要是包括以下几个步骤:
    a.从库给主库发送psync命令,这个命令包含两个参数,主库的 runID复制进度 offset ;

解释:runID,是每个 Redis 实例启动时都会自动生成的一个随机 ID,用来唯一标记这个实例。当从库和主库第一次复制时,因为不知道主库的runID,所以将 runID 设为“?”
offset,此时设为 -1,表示第一次复制

b.主库收到psync命令后,会用FULLRESYNC命令回复从库,这个命令也包含两个参数:主库的 runID复制进度 offset,从库收到回复后,记录下这两个参数;

注意:FULLRESYNC命令表示第一次复制采用的全量复制,主库会把当前所有的数据都复制给从库。

  1. 主库依赖于内存生成的RDB文件,将数据同步给从库,从库接收到数据后,在从库上完成数据加载。主库执行 bgsave 命令,生成 RDB 文件,接着将文件发给从库。从库接收到 RDB 文件后,首先会清空当前数据库,然后再加载 RDB 文件。这是因为从库在通过 replicaof 命令开始和主库同步前,可能保存了其他数据。
    那么在主库给从库同步数据的过程后,主库还是能够继续接收写的请求,即Redis服务并不会产生中断,那么此时主库会把没有记录的在RDB文件中的数据,以命令(redis的修改命令)的方式写入的一个缓冲区replication buffer。
  2. 当主库将RDB文件同步给从库完成后,主库就会把缓冲区replication buffer的命令同步给从库,从库执行这些命令。
    至此,主库从库间数据的首次同步就完成了。
    在这里插入图片描述

2.主库从库间同步是RDB还是AOF?

从上面的论述,我们直接说主库从库之间的同步是以RDB的形式同步的,即同步的内容是RDB,不是AOF。那么为什么是RDB而不是AOF呢?基本有以下两点原因:

  1. RDB文件内容是经过压缩的二进制数据,文件比较小,而AOF记录是每次执行的写操作命令,写操作越多,AOF文件会越大,并且还有很多对同一个key的多次冗余操作。在主从全量数据同步时,传输RDB文件能够很好的降低对主库机器网络带宽的消耗;同时,从库在加载RDB文件时,由于文件小,读取速度就快,并且RDB文件都是二进制数据,从库直接按照RDB相关协议直接解析还原数据就可以了,速度比较快;而AOF每次都要依次执行每个命令,这个过程会经过很长的处理逻辑,从库加载数据要比RDB慢的多,所以使用RDB进行主从全量的数据同步成本最低。
  2. 假设要使用AOF进行主从数据同步,那么AOF默认就要打开,那就要选择文件刷盘的策略,选择不当,就会影响Redis主库的性能;而RDB只有在需要定时备份和主从全量同步数据时才会触发生成一次快照。而在很多丢失数据不敏感的业务场景,其实是不需要开启AOF的。
    基于以上两点,所以采用RDB进行主从全量的数据同步最合适。

3.主库从库间基于长连接的命令传播

主库和从库完成了首次全量同步后,那么主库和从库之间就会一直维护一个长链接,主库会通过这个连接,把后续收到的命令及时传递给从库,这样就可以减少频繁建立连接产生的网络开销。但是我们知道,只要是网络连接,就是不稳定的,一旦出现网络闪断或者阻塞了,那就不能进行及时同步了,那么这时候数据会丢失吗?会影响数据的最终一致性吗?其实不会,下面我们就看一下redis是如何处理网络中断产生的数据同步问题的。

4.增量同步

主库和从库的链接中断之后,如果稍后网络恢复,主库和从库之间会采用增量同步(2.8版本之后)的方式,将主从库网络断连期间主库收到的命令,同步给从库。主库从库之间的增量同步,其实也是采用一个命令缓冲区repl_backlog_buffer,当主从库断连后,主库会把断连期间收到的写操作命令,写入 replication buffer,同时也会把这些操作命令也写入repl_backlog_buffer 这个缓冲区。repl_backlog_buffer 是一个环形缓冲区,主库会记录自己写到repl_backlog_buffer 的位置,用偏移量master_repl_offset来表示,从库则会记录自己已经读到repl_backlog_buffer 的位置,用偏移量slave_repl_offset 来表示。正常情况下,这两个值是相等的。那么主库和从库之间具体是怎么根据这两个偏移量来进行数据同步的呢?
在主库和从库网络中断后,由于主库很可能会收到写的命令,那么此时master_repl_offset就会大于slave_repl_offset ,稍后,主库和从库网络恢复,此时从库就会向主库发送psync命令请求,这时候从库会发送主库的runId和从库的slave_repl_offset 给主库,主库接收到从库的psync请求后,主库会计算自己的 master_repl_offset 和 slave_repl_offset 之间的差距,只徐亚把 master_repl_offset 和 slave_repl_offset 之间的命令操作同步给从库就行了,这就完后成了增量同步,实现了数据的最终一致性。
在这里插入图片描述

 注意:主从增量复制可能会产生一个小问题,repl_backlog_buffer 是一个环形的缓冲区,缓冲区写满之后,会覆盖之前的命令,如果从库的读取速度比较慢,就有可能导致从库还未读取的操作被主库新写的操作覆盖了,这会导致主从库间的数据不一致。我们可以通过调整repl_backlog_size 参数,扩大repl_backlog_buffer 的大小,降低主从增量复制时产生的数据不一致的可能性。

到此,Redis主从复制的过程就完成。它使用读写分离的模式,即避免了单个实例提供服务的局限性,也解决了多个实例同时读写带来的数据不一致性的问题。但是,这种模式还有一个关键的问题没解决,那就是Master的单点故障问题。下面我们就探讨一下在主从复制模式下,Master单点故障的问题怎么解决,实现真正的高可用。

3. Redis的哨兵机制

从上面主从复制模式我们知道,由于Master存在单点故障问题,一旦Master出现故障,那么整个集群的写操作都会受到影响。那怎么解决这个问题呢?根据我们以前的知识,如果Master出现故障,我们可以重新在从实例中选出一个来作为新的Master,这样就能解决Master的单点故障问题。
从理论上来说,所有解决单点故障问题,进行Matser切换时,都需要考虑三个问题:

  1. 如何判断Master是的挂了?判断,得出结论
  2. 应该选择哪个Slave作为Master?讨论方案,执行
  3. Slave如何知道的Matser信息?执行结果的通知
    为了解决上面这三个问题,Redis引入了哨兵机制,有效地解决了上面的这三个问题,实现了Redis主从复制集群的高可用。

1. Redis的哨兵机制基本流程

哨兵是什么?哨兵其实也是一个Redis进程,只不过这个进程是运行在特殊模式下。主从实例运行时,哨兵其实也在运行。哨兵的主要任务有以下三个方面:监控、选择主库和通知。

  1. 监控:哨兵在运行过程中,会定时给所有的主从实例发送PING命令,进行健康监测,如果从库没有在规定时间内给出响应,那么哨兵直接就标记它为下线状态,同样,如果主库没有在规定时间内给出响应,那么哨兵也会把它标记为下线状态,进行主的切换;
  2. 选择主库:哨兵检测到主库挂掉以后,哨兵就需要从很多个从库里面,按照一定的规则,把新的主库选出来;
  3. 通知:哨兵选出新的主库以后,哨兵会把新的主库信息发给每个从库,从库通过replicaof指令,重新关联到新的主上,同时,哨兵也会把新主库的连接信息通知给客户端,让客户端把新的写请求发送的新的主库上。

2.哨兵如何判断Redis实例下线

哨兵判断redis实例是否下线,对于主库和从库的标准是不一样的,判断从库下线比较简单,那我们先说一下如何判断从库下线。

1.哨兵判断从库下线

上面我们提到,哨兵会通过PING 命令检测所有主从实例的健康抓状态,当检测的是从库,如果从库在规定的时间内没有给出响应,那么哨兵直接将从库标记为下线就可以了,即使哨兵判断错了,也不会有太大的影响,因为从库下线,并不会影响集群的可用性,只要我们及时恢复就可以了。

2.哨兵判断主库下线

当哨兵检测的是主库,如果主库在规定时间内没有给出响应,那么哨兵是否可以直接把Master标记为下线吗?我们知道,哨兵是通过PING命令,即网络来进行监控的,那么网络就会产生不可靠性,那么就会出现误判。即主库没有问题,却被检测说有问题,一旦误判产生,那么就要进行主库的切换,只要启动了主从切换,后续的选主和通知操作都会带来额外的计算和通信开销,所以对于如此高性能的redis来说,我们要降低哨兵误判的可能。那么怎么减少误判呢?其实这就是和我们生活中一样,大家商量着来,大家开会决定,而不是一言堂,老大说什么就是什么,而应该是民主,少数服从对数的方式来进行表决。
所以基于上面的分析,哨兵也采用了集群的方式,即多个哨兵组成一个集群,来共同检测redis主从集群的可用性。

1.哨兵集群

单个哨兵,如果出现哨兵单点故障,那么整个redis集群肯定就没办法进行监控、选择新主和通知了,因为唯一的一个哨兵都没了,那就相当于没有部署哨兵一样,所以为了解决的哨兵的点点故障,我们将多个哨兵组成一个集群,哨兵集群,这样即使有哨兵实例出现故障挂掉了,其他哨兵还能继续协作完成主从库切换的工作,包括判定主库是不是处于下线状态,选择新主库,以及通知从库和客户端。
在配置哨兵集群的时候,在每个哨兵实例上,我们并没有配置其它哨兵实例的信息,仅仅配置了Redis的Master实例,(不清楚的小伙伴,请查看我之前的博客《CentOS-7下Redis主从复制+sentinel的搭建过程》)那这些哨兵是如何进行沟通,组成哨兵集群的呢?

port 36379
sentinel monitor mymaster 192.168.15.20 6379 2

带着这个问题,我们继续进行探讨。

2. 基于 pub/sub 机制的构建哨兵集群

前面我们说过,每个哨兵其实是一个运行在特殊模式下的Redis实例,每个哨兵实例也提供 pub/sub 机制。哨兵只要和主库建立了连接,就可以在主库上发布消息了,例如自己的连接信息(IP、port等);除了发布消息,哨兵还可以订阅消息,它也可以从主库上订阅消息,获得其他哨兵发布的连接信息。当多个哨兵实例都在主库上做了发布和订阅操作后,它们之间就能知道彼此的 IP 地址和端口。这样哨兵就和主库建立了连接,同时也和其他哨兵组成了哨兵集群。但是哨兵除了监测主库,也要监测从库,但是我们哨兵配置项中,并没有配置从库的任何信息,那哨兵集群是如何与从库建立起连接的呢?
其实尽管哨兵开始并不知道从库的相关配置信息,但是主库却知道所有从库的信息(IP、PORT等),所以哨兵和主库建立联系后,就会向主库发送 INFO 命令,哨兵 给主库发送 INFO 命令,主库接受到这个命令后,就会把从库列表返回给哨兵。哨兵就可以根据从库列表中的连接信息,和每个从库建立连接,并在这个连接上持续地对从库进行监控 。
那么哨兵具体是如何判断主库下线的呢?
这里我们先说两个概念:“主观下线”和“客观下线”,主观下线就是单个哨兵认为Master挂掉了,但并不一定真的下线了,可能是误判,客户下线就是大多数哨兵都认为Master挂掉了,那么我们就可以认为Master是真的下线了,就可以进行选择新的Master进行切换了。
客观下线的量化标准是什么呢?其实最好的是采用过半原则。假设有n个哨兵,那么只要有n/2 +1个哨兵认为Master主观下线了,那么Master就是客观下线了,这样就可以降低误判的概率,从而避免误判带来的无谓的主从库切换。当然到底是多少个哨兵认为主观下线就真的的认证Master客观下线了,这是有参数quorum设置的。只要达到quorum个哨兵认为Master主观下线了,那Master就被认定为客观下线了,基本上我们设置为过半就可以了。
那么这个判断的过程是什么样的呢?
任何一个哨兵只要判断Master主观下线了,就会给其他哨兵实例发送 is-master-down-by-addr 命令,然后,其他哨兵实例会根据自己和主库的连接情况,给出自己的主观判断Y(YES,赞成)或者N(NO,反对),一个哨兵获得了仲裁所需的赞成票数后,就可以标记主库为“客观下线”。这个所需的赞成票数是通过哨兵配置文件中的 quorum 配置项设定的。例如,现在有 5 个哨兵,quorum 配置的是 3,那么,一个哨兵需要 3 张赞成票,就可以标记主库为“客观下线”了。这 3 张赞成票包括哨兵自己的一张赞成票和另外两个哨兵的赞成票。

3.哨兵如何选择新主库

上面第二部分,哨兵判断Master客观下线之后,就要进行新Master的选择过程。选主是一个比较复杂的过程,这里我准备多啰嗦几句。选择新主库,会涉及到两个问题:

  1. 谁来选?即哪个哨兵称为leader?
  2. 怎么选,及选择的标准和依据是什么?
    下面我就根据上面的这两个问题,进行深入的探讨一下。

1.哪个哨兵称为leader

哨兵称为leader的标准:

  1. 第一,拿到半数以上的赞成票;
  2. 第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。
    为了更好的说明选举leader的过程,我以 3 个哨兵、quorum 为 2 的哨兵集群的选举过程。来说明一下具体的步骤。
时间哨兵1(S1)哨兵2(S2)哨兵3(S3)
T1给自己投一票Y,向S2、S3发出投票请求,说明我想称为leader
T2给自己投一票Y,向S1、S2发出投票请求,说明我想称为leader
T3收到S1的投票请求,回复Y收到S1的投票请求,回复N
T4收到S3的投票请求,回复N
T52票Y,1票N1票Y,2票 N
  1. T1时刻,S1判断Master主观下线,它想称为leader,就先给自己投一个赞成票,然后分别向S2和S3发送命令,就说我想称为leader,你们同意吗?
  2. 在T2时刻,S3也判断Master主观下线,它也想称为leader,于是它也给自己投了一个赞成票,同时再分别向S1和S2发送命令,就说我S3也想称为leader,你们同意吗?
  3. 在T3时刻,S3收到了S1的请求,由于S3已经投了自己一个赞成票,所以它会对S1说NO,投一个反对票N;S2也收到了S1的请求,因为 S2 之前没有投过票,它会给第一个向它发送投票请求的哨兵回复 Y,给后续再发送投票请求的哨兵回复 N,所以,在 T3 时,S2 回复 S1,同意 S1 成为 Leader。
  4. 在T4时刻,S2收到了S3在T2时刻发出的请求,但是S2已经给S1投了赞成票,所以S2给S3说NO,回复N,
  5. 在T5时刻,S1收到2票Y,1票N;S3收到1票Y,2票N;S2是0票Y;此时S1收到2票Y,并且达到了quorum 2的数量,所以S1它最终成为了 Leader。S1就会开始执行选主操作,选定新主库后,会给其他从库和客户端通知新主库的信息。

上面我们说的过程是一个理想的过程,假设S2也同时认定了Master主观下线,也向S1和S3发出想称为leader的请求,那么三个哨兵就各得到一票,那么这一轮的选举就轮空,哨兵集群会等待一段时间(也就是哨兵故障转移超时时间的 2 倍),再重新选举。因为,哨兵集群能够进行成功投票,很大程度上依赖于选举命令的正常网络传播。如果网络压力较大或有短时堵塞,就可能导致没有一个哨兵能拿到半数以上的赞成票。所以,等到网络拥塞好转之后,再进行投票选举,成功的概率就会增加。
需要特别注意的一点,哨兵集群至少要部署3个哨兵实例,一定要切记。如果哨兵集群只有 2 个实例,此时,一个哨兵要想成为 Leader,必须获得 2 票,而不是 1 票。此时如果有个哨兵挂掉了,那么,此时的集群是无法进行主从库切换的。因此,通常我们至少会配置 3 个哨兵实例

这里我想出三个思考题,我们一起探讨一下,加深一下对哨兵集群的理解:
1.上面我们说,哨兵集群至少要配置3个哨兵实例,那是不是哨兵集群中,哨兵实例越多越好呢?

答:但是是错。因为哨兵在判定“主观下线”和选举leader时,都需要和其他节点进行通信,交换信息,哨兵实例越多,通信的次数也就越多,而且部署多个哨兵时,会分布在不同机器上,节点越多带来的机器故障风险也会越大,这些问题都会影响到哨兵的通信和选举,出问题时也就意味着选举时间会变长,切换主从的时间变久。

2.Redis 1主4从,5个哨兵,哨兵配置quorum为2,如果3个哨兵故障,当主库宕机时,哨兵能否判断主库“客观下线”?能否自动切换?

答:1.哨兵集群可以可以判断主库客观下线。由于quorum=2,所以当一个哨兵判断主库“主观下线”后,询问另外一个哨兵后也会得到同样的结果,2个哨兵都判定“主观下线”,达到了quorum的值,因此,哨兵集群可以判定主库为“客观下线”。
   2.哨兵不能完成主从切换。哨兵标记主库“客观下线后”,在选举leader时,一个哨兵必须拿到超过多数的选票(5/2+1=3票)。但目前只有2个哨兵活着,无论怎么投票,一个哨兵最多只能拿到2票,永远无法达到多数选票的结果。

3.配置哨兵时,我们会设置一个down-after-milliseconds的值,这个值是哨兵判断主观下线的是时间阀值。那么请问:如果调大这个值,对哨兵的误判会有好处吗?

答:有好处,适当调大down-after-milliseconds值,当哨兵与主库之间网络存在短时波动时,可以降低误判的概率。但是调大down-after-milliseconds值也意味着主从切换的时间会变长,对业务的影响时间越久,我们需要根据实际场景进行权衡,设置合理的阈值。

2.怎么选择新的主库

当选定了哨兵的leader后,这个leader就开始选主的过程。下面我们再来回答怎么选,及选择的标准和依据是什么?
哨兵选择新主库的过程是一个筛选和打分的过程,即从多个从库中,先按照一定的筛选条件,把不符合条件的从库去掉,然后按照一定的规则,给剩下的从库打分,谁的的分最高,就选择谁作为主库。

1.筛选

哨兵要按照什么样的条件来进行筛选呢?基本有以下几个条件:

  1. 从库当前的在线状态。首先要保证所选的从库仍然在线运行,如果这是从库已经挂掉了,就不可能作为新Master的备选;
  2. 判断从库之前的网络状态。down-after-milliseconds:主从库断连的最大连接超时时间。如果在 down-after-milliseconds 毫秒内,主从节点都没有通过网络联系上,我们就可以认为主从节点断连了。如果发生断连的次数超过了 10 次,就说明这个从库的网络状况不好,不适合作为新主库。
2.打分
  1. 按照从库配置的优先级。redis集群管理员可以根据每台服务器的性能,配置不同的 slave-priority 配置项,给不同的从库设置不同优先级。选主时,哨兵会把优先级高的从库打高分,哪个从库的优先级最高,那么它就是新的Master了。如果每个从库的优先级都一样,那就要进行下步的打分;
  2. 选择与旧的主库同步程度最接近的从库作为Master。上文介绍过,主从库同步时有个命令传播,主库会用master_repl_offset记录当前最新写操作在repl_backlog_buffer中的位置,从库会用slave_repl_offset记录从库的同步位置,如果在所有从库中,有从库的 slave_repl_offset 最接近 master_repl_offset,那么它的得分就最高,可以作为新主库;如果从库的slave_repl_offset 的最大值存在两个都一样的,那么这两个就要进行下一步的打分;
  3. 选择ID号最小的从库作为新主库。每个实例都会有一个 ID,这个 ID 就类似于这里的从库的编号。目前,Redis 在选主库时,有一个默认的规定:在优先级和复制进度都相同的情况下,ID 号最小的从库得分最高,会被选为新主库。

这样,哨兵leader通过筛选和打分的过程,就完成了选新Master的过程。

4.通知

哨兵的leader选出新的主库之后,需要通知从库,新的Master已经选出来,并且把新Master的相关连接信息发送给各个从库,让从库重新执行replicaof ,连接到新的主上,这样整个Redis集群就又完全恢复了正常,对外提供读写服务了。但是到此为止,我们其实还缺少一个知识点,就是虽然我们的Redis集群恢复正常了,但是我们的客户端了?客户端并不知道Redis已经更换了主,所以这时候客户端是没办法进行写操作的。那怎么办呢?我们之前,哨兵可以通过 pub/sub机制来通知redis的哨兵集群,那它是不是也可以来发布消息,然后让客户端订阅呢?如果可以的话,客户端知道收到Redis集群更换了新主的消息,客户端就能自主更换了。答案是可以的,redis也确实是利用了这个机制来通知客户端。这就是基于 pub/sub 机制的客户端事件通知。
可能有些同学会说,我JAVA在利用哨兵模式的时候,并没有单独写一个订阅的程序来进行自动主切换啊?其实这是因为我们引入的Jedis或者lettuce等api已经帮我们实现好了,我们只需要进行调用就可以,其实Jedis等才是我们的这里所说的客户端。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值