在这之前,主要复习了Redis
的AOF
日志还有RDB
快照。它是高可用的一个重要保障,就是数据尽量少丢失。而高可用还有一点就是服务尽量少中断,那么这里就需要通过增加副本的冗余了。也就是Redis
的主从架构了。
Redis
提供了主从库模式,采用读写分离模式:
Redis
启动多个实例之后,开启主从库关系也是比较简单的。实例之间可以通过 replicaof
命令来链接。例如我们在实例B
上执行以下命令:
replicaof 实例A 6379
那么此时实例B
就成为了实例A
的从库。并会从实例A
上复制数据。两个实例之间第一次的数据同步有三个阶段,如图:
阶段一:建立连接,协商同步。
这一个阶段,也就是主从库之间建立起连接,并为全量复制做准备。我们可以看到,从库主要是向主库发送了一条 psync
命令。该命令的目的:表示要进行数据同步。 该命令包含两个参数:
runID
:实例的唯一ID
,启动的时候会自动随机生成。由于主从库之间此时是第一次连接,因此从库无法得知主库的runID
,因此这里是一个问号。offset
:偏移量,可以看做数据复制的进度。 -1
代表是第一次复制。同样地,从库发送了这样的请求,主库也要发回去,类似于TCP
的三次握手。主库接收到 psync
命令后,通过 FULLRESYNC
响应命令同样带上这两个参数:主库 runID
和主库目前的复制进度 offset
,返回给从库。
FULLRESYNC
:表示采用全量复制,将主库当前的所有数据都复制给从库。那么问题来了,主从库之间是如何进行数据的传输的?那就来到了第二阶段了。
阶段二:主库同步数据给从库。
这个过程依赖于RDB
文件。大概流程如下:
bgsave
命令,生成了RDB
文件,然后发送给从库。RDB
文件后,先清空当前数据库,然后加载RDB
文件。其中,有这么几个注意点:
RDB
文件中。而是保存在主库内存中的 replication buffer
。准确来说, replication buffer
中保存了以下三种情形下的数据更新操作:
bgsave
命令,产生快照的过程中。RDB
文件到从库,网络传输的过程中。RDB
文件恢复到本地内存,即数据恢复的过程中。阶段三:主库将第二阶段中新的数据更新操作发送给从库。
此时从库已经将RDB
文件恢复到自己的内存,数据已经基本完成同步了。但是第二阶段中,主库的写操作功能依旧能够使用,而主库将这类数据更新操作都记录到了 replication buffer
中。因此在最后一个阶段,从库还需要执行这些操作,从而实现主从库之间数据的完全同步。
从上面我们可以得知,主库之间如何建立主从关系,以及第一次建立关系的过程。我们也可以想到,这个过程中最耗时的两个操作:
RDB
文件的生成。RDB
文件的传输。但是这种简单单一的主- 从设计模式有着一定的缺陷:
RDB
文件。但是从库万一有很多个呢?RDB
文件是主库通过fork
一个子进程然后写入的。fork
的过程中,主库是阻塞的。那从库很多的情况下,就容易发生主库阻塞的情况。为了解决上述问题,主要可以通过减缓主库的压力。那么只要将某一个从库变成其他从库的主库就好了。也就是二级主库(我自己命名的)。看图会更直观点,主从从架构:
背景:
虽然主从架构一旦完成,并且实例之间都已经建立起了长连接,那么读写分离的功能也就有了,而且高可用。但是这类架构有个问题是无法避免的:如果实例之间的网络连接断了怎么办?
Redis 2.8
之前,如果主从库在命令传播时出现了网络闪断,则会重新进行一次全量复制。 开销太大。Redis 2.8
之后,如果主从库在命令传播时出现了网络闪断,则会重新进行一次增量复制。 期间主要通过 repl_backlog_buffer
缓冲区,实现增量数据的同步的。接下来说下增量同步的原理:
首先主从库断开连接之后,主库会将期间收到的写操作命令写入缓冲区:
repl_backlog_buffer
。一种环形缓冲区,用于为增量同步做保障
。主库记录自己写到的数据偏移量。从库记录自己读到的数据偏移量。对于主库而言,不断接收新的写操作,那么在repl_backlog_buffer
缓冲区中的偏移量就会越来越大,叫做master_repl_offset
。对应的,从库也在不断地复制写命令,在缓冲区的读位置也在不断地增大,对应的叫做master_repl_offset
。正常情况下,两者基本保持相同。示意图大致如下:
在了解完repl_backlog_buffer
这个缓冲区之后,回到正轨,那么在主从连接恢复的那一刻起,主从之间的数据是如何保证同步的呢?这个阶段和主从之间建立第一次连接比较相似:
psync
命令,并把自己当前的 slave_repl_offset
发给主库,主库计算 master_repl_offset
和 slave_repl_offset
之间的差值。一般主位移 > 从位移。(因为主库还能够有写操作)repl_backlog_buffer
这个值最好设置尽量大一点,否则,倘若主从库断开时间太久,主库上写操作太多,将这个区域写满,那么从库就只能进行一次全量同步操作。因为没有位置可以存放从库读的位置。因此配置大一点可以降低主从断开后开启全量同步的概率。repl_backlog_buffer
是一个环形缓冲区,因此在缓冲区写满后,主库会继续写入,此时就会覆盖掉之前写入的操作。如果从库的读取速度比较慢,就有可能导致从库还未读取的操作被主库新写的操作覆盖了,这会导致主从库间的数据不一致。repl_backlog_buffer
存在于主节点,其他的从库的offset
偏移量都是相对于该缓冲区而言的。replication buffer
存在于各个从节点。用于主节点与各个从节点间数据的批量交互。无论全量还是增量同步,数据都通过其来进行交互。到目前为止主要说了Redis
的主从库同步的基本原理:
Redis
的实例不要太大,否则在RDB
文件传输和创建的过程会比较耗时。同时避免只有一个主库和多个从库建立主从关系,可以建立主从从模式来缓解主库的压力。Redis
主要通过增量复制来保证数据的一致性。重点在于repl_backlog_buffer
这个圆形缓冲区。记录了主从库的写/读偏移量,通过两者的差值可以计算出断开连接期间从库缺失的动作。题外话:为何主从全量同步使用的是RDB
文件而不是AOF
日志呢?
RDB
文件的内容是经过压缩的二进制数据。而AOF
文件记录的则是每一次操作命令,不仅冗余而且容易变得很大。因此全量数据同步的时候,传输RDB
文件可以降低对网络贷款的消耗。同时从库在加载RDB
文件的时候,读取和解析速度都要快于AOF
。(文件小 + 二进制内容)AOF
日志作为全量同步,意味着必须打开AOF
功能,随之而来的也必须设置AOF
的文件刷盘策略。倘若选择不当,就会严重影响Redis
性能。相反,RDB
快照只需要主从同步数据的时候以及定时备份的时候才会触发生成。一般都业务场景足以应付,因此没有开启AOF
的必要。主从架构中,如果从库挂掉了,实质上的影响不是很大。因为写操作,是由主库来提供的。而读操作,主从库都有提供。但是如果主库挂掉了,该怎么办呢?数据往哪写是个问题。因此这种时候需要新选出一个主库,将某个从库切换为主库。这个过程就使用到了哨兵机制。
哨兵主要负责三个任务:
PING
命令,检查它们是否正常运行。倘若主从库没有在规定的时间内响应PING
命令,那么将会被认定为下线状态。若是主库被认定为下线,那么就开始切换主库。replicaof
命令,和新主库建立连接,并进行数据复制。同时哨兵还负责将新主库的连接信息告知客户端,让新的读写请求发送到新主库上。由于在实际生产过程中,难免遇到网络波动,或者是主库在某一时间内的压力比较大,因此无法及时的响应哨兵发送的PING
命令,从而被认定为下线状态,开启了新主库的选举。但实际上,主库并没有挂掉。因此为了防止这样的误判发生,哨兵机制通常采用了多实例组成的集群模式进行部署,即哨兵集群。
不仅如此,由于主库的地位比较高,对于其下线状态的判断也是比较特殊的。只有大多数的哨兵实例都判断主库已经主观下线了,那么主库才会被标记为客观下线。这时才会真正触发新主库的选举以及后续流程。
down-after-milliseconds
设置的规定时间内若没有响应PING
,则认为主观下线。 配置时间越短,代表哨兵越敏感。哨兵负责任务中,尤其重要的就是第二点,关于新主库的选举,上文提到过,其会以一定的规则来选举出一个新的实例。主要流程分为两大步:
首先第一点,哪些从库是不满足条件的:
PING
命令没有及时回应。其次是打分环节:
slave-priority
,给从库设置不同的优先级,数字越小代表优先级越大。repl_backlog_buffer
环形缓冲区中分别有着master_repl_offset
和slave_repl_offset
。那么两者差值最小的,也就是从库同步程度最接近主库的,其分数也会最高。ID
号打分:每个实例都会有一个随机生成的唯一ID
。在优先级和复制进度都相同的情况下,ID
号最小的从库得分最高,会被选为新主库。提问:哨兵在操作主从切换的过程中,客户端能否正常地进行请求操作?
回答:若客户端使用了读写分离的前提下,读操作是可以在从库上正常执行的。但此时对于写操作。因为主库已经挂了,并且还没有选举出新的主库,因此这期间的写请求就会失败。持续时间 = 哨兵切换主从的时间 + 客户端感知到新主库的时间
。
上文主要提到了监控阶段的几个要点:主观下线、客观下线等。以及选举新主库的一个大致流程。那么最后的通知部分还是没有讲的。当哨兵完成主从切换后,客户端需要及时感知到主库发生了变更,然后把缓存的写请求写入到新库中,保证后续写请求不会再受到影响。
提问:哨兵在完成主从切换后,还做了什么事情?
回答:
pubsub
中。 (下文详细介绍)pubsub
,当其有数据的时候,客户端就能感知到主库发生了变更,同时拿到了最新的主库地址。那么后续的写请求往该地址发送即可。注意:因为主库可能会挂,然后通过哨兵机制选举出一个新的主库,因此再客户端访问主从库的时候,不能将地址写死,一般从哨兵集群中获取最新的地址。(SDK
也有提供)
提问:哨兵集群中有实例挂了,会影响主库状态判断和选主吗?
回答:只要集群中大多数节点状态正常,集群依旧可以对外提供服务。
提问:哨兵集群多数实例达成共识,判断出主库“客观下线”后,由哪个实例来执行主从切换?
回答:由哨兵领导者来完成主从切换。哨兵领导者这部分用了Raft
算法。选举过程大致如下(下文详细介绍):
哨兵的配置如下:
sentinel monitor <主库名称> <主库ip> <端口号> <quorum(下文会说)>
每个哨兵配置的时候,只有主库相关的信息,而哨兵和哨兵之间却没有配置,那么哨兵实例彼此不知道对方的地址,那么如何组成集群的呢?主要通过pubsub
机制,即Published
以及Subscribe
。发布订阅机制。
哨兵集群组成的原理:
IP
和端口等),同样可以从主库上订阅消息,获得其他哨兵发布的信息。 也因此哨兵和哨兵之间能够知道彼此的端口和IP
地址。Kafka
中Topic
主题的概念。哨兵除了要和各个哨兵实例之间建立连接,搭建哨兵集群以外。还要和从库建立连接。不然,当主库挂掉的时候,哨兵又怎能去从从库当中选举新主库呢?那么哨兵又是如何知道从库的IP地址和端口的呢?
哨兵和从库之间建立连接的原理:
INFO
命令。主库接收到后,将从库列表数据返回给哨兵。接下来就需要了解哨兵和客户端之间的通知问题了,上文提到了订阅的消息频道,在同一个频道下的不同实例(哨兵。主从库)之间是可以互相通知的。这里面有几个Redis
中重要的事件和对应的频道:
这样客户端就可以从哨兵这里订阅到各种各样的消息,流程如下:
# 所有实例进入客观下线状态的事件
SUBSCRIBE +odown
# 订阅所有的事件
PSUBSCRIBE *
上文我们提到过,当主库挂掉了,由哨兵集群来选举出一个新的主库,而主从库切换过程则交给一个特殊的哨兵 — 领导者哨兵 来执行。同时哨兵集群倘若要判定主库客观下线,需要有一定数量的实例都认为该主库已经主观下线。这时候才能开启主从的选举和切换。大概的流程如下:
当任何一个哨兵判断主库主观下线后,就会给其他哨兵发送 is-master-down-by-addr
命令。
其他哨兵接收到后,会根据自己和主库的连接情况,做出 Y(赞成)
或者 N(反对)
的响应。
若某个哨兵A,获得了一定数量的赞成票数,并且超过了仲裁所需的阈值,即quorum
配置(配置哨兵的时候,最后一个参数),那么此时就可以标记主库为客观下线。
此时,该哨兵A就会发送命令给其他哨兵,表明希望由自己来执行主从切换,并让所有其他哨兵进行投票。即领导者哨兵的选举过程。
备注:将主库标记为客观下线的哨兵可以继续发起让自己来执行主从切换的投票。
成为领导者哨兵需要满足两个条件:
majority
值。注意:
Y/N
和领导者选举的Y/N
含义是不同的,希望大家做出区分。前者是用来判断主库是否和某个哨兵连接正常。后者是用来选举领导者哨兵的一个投票机制。 切记,这是两个不一样的事情。down-after-milliseconds
最后一点做个区分:
majority
:允许哨兵进行主从库切换的最少哨兵数量。先拿到这个数量的哨兵就是领导者哨兵。quorum
:确认主库客观下线的最少哨兵数量。提问:Redis
1主4从,5个哨兵,哨兵配置quorum
为2,如果3个哨兵故障,当主库宕机时,哨兵能否判断主库“客观下线”?能否自动切换?
回答:
quorum
的值。因此哨兵集群此时可以判定主库是客观下线的。5/2 +1 = 3
,而存活的哨兵只有2个,因此无论怎么样也达不到这样的票数要求。因此无法完成主从切换。到这里讲的内容主要有这么几点:
pub/sub
机制来组成。INFO
命令和所有从库建立连接。pub/sub
机制完成消息的订阅和事件通知。quorum
值数量的哨兵认定主库为主观下线。认定了主库为客观下线的哨兵有资格开始成为领导者哨兵的候选人,并发起投票。