浅谈Redis的主从复制和Sentinel

1,251 阅读11分钟

image-20200906175435466

一、Redis的主从复制模式

Redis可以通过SLAVEOF <ip> <port>指令或者配置文件slavof <ip> <port>的方式,让本服务器去复制另外一个服务器。

被复制的服务器称为主服务器,复制的服务器则称为从服务器,以此形成一种主从关系(Master-Slave)。

建立主从关系之后,从服务器不再(也不能)执行写命令,而是完全同步主服务器的数据(就算发现AOF或者RDB文件中有过期的键也不能删除,只能等主服务器的同步)。

指令info replication,可以单独查看服务器此时的主从信息。如下:

image-20191109232601445

该种主从复制模型非常适合读多写少的环境,而且仅从主服务器写入一定程度上也不需要担心数据一致性问题。

另外的复制相当于为主服务器中的数据创建多个复本,也算是一种容错策略。

需要注意的是这里的一致性都是指最终一致性,因为命令的扩散也会有延迟,卡着延迟从从服务器中读取就会有数据不一致的问题。

因此如果对一致性的要求很高,或者必须要强一致性,建议还是不要从从服务器读取。

复制模式可以分为数据同步以及命令传播两个阶段。

数据同步就是从服务器刚开始连接时的操作,全盘同步主服务器上的数据。

命令传播就是主服务器将本地执行过的命令再发送到从服务器(相当于主服务器以客户端的身份)

Redis中对应数据同步的命令有两个SYNCPSYNC

旧版的数据同步

旧版的数据同步就是依托于Sync命令,由从服务器发送给主服务器表示开启同步数据。

image-20191109154905723

主服务器首次接收到Sync命令之后,会执行BGSAVE命令生成RDB文件,并在此时开启命令缓冲区,记录之后所有执行的写命令。

BGSAVE执行完之后,会将生成的RDB文件发送给从服务器(此时如果于多个从服务器连接,RDB文件可以直接共享)。

从服务器在接收到主服务器发送的RDB文件之后,会清空本地的所有数据,载入RDB文件中的数据。

之后主服务器还会将缓冲区中的数据发送到从服务器,从服务器执行完缓冲区中的写命令,就达到了和主服务器的完全一致。

新版本的数据同步

PSYNC命令是对SYNC命令的进一步优化,主要是SYNC只能进行全量同步。

就算是因为网络抖动造成的秒断秒连的情况可能都会需要重新生成RDB文件并同步,效率真的就不高,为此在Redis2.8版本之后,新增加了一个PSYNC命令。

PSYNC命令完整的形式是PSYNC <runid> <offest>,在全量同步的基础上增加了一个增量同步的过程判断。

下面是增量同步相关的一些概念:

  1. 复制偏移量

    按照字面意思也很好理解,是主从服务器各自维护的以字节为单位的属性,表示复制的进度。

    比如当前主服务器的复制偏移量为10000,在发送了50个字节的内容之后,就变为了10050,可以认为是主从服务器数据不一致性程度的表示。

    在Sentinel执行故障转移的时候也会以复制偏移量作为主要的参考依据。

  2. 复制缓冲区

    复制缓冲区是由主服务器维护的一个固定长度的FIFO队列。

    该队列会缓存近期主服务器所执行的写命令。

  3. 主服务器 run ID

    run ID唯一标识一个Redis服务器。

    实际上不论主从在服务器启动时都会生成一个run ID,由40位随机的16进制字符组成。

    此处的run ID是在从服务器连接到主服务器是由主服务器下发的自身的run ID。

PSYNC的执行流程如下:

接收到PSYNC命令之后,主服务器会判断,如果runId不符合,说明主服务器已经改变,则会直接走旧版SYNC的逻辑。

如果是断线重连的情况则会根据双方的复制偏移量以及复制缓冲区长度的对比,来决定是部分还是完整重同步。

复制积压缓冲区里的每个字节都会有自己的偏移量属性,主服务器会判断PSYNC传过来的从服务器复制偏移量是否还在缓冲区中。

如果此时缓冲区中最小的偏移量都大于从服务器的复制偏移量,则还是会走旧版的SYNC逻辑。

如果从服务器的复制偏移量仍存在于积压缓冲区则进行部分重同步。

部分重同步简单来说就是发送积压缓冲区中从服务器复制偏移量之后的所有数据。

Redis的主从模式允许服务器横向扩展,增加容错性以及读的性能。

但整个的存储上限仍然受单台服务器的限制,主从模式保存相同的数据仅仅是一个数据备份,

更重要的是单主结构会带来单点问题,如果主服务器宕机,基本全完。

二、 Sentinel 哨兵模式

Sentinel是Redis的高可用解决方案,仅仅依靠主从复制在主服务器宕机之后主服务器所在的整个模块就会进入只读状态(Redis不存在主主复制的模式),无法写入或更新数据。

Sentinel模式就是指一个或者多个Sentinel服务器,监视一个或多个的主服务器及其从服务器。

在主服务器宕机时,激活故障转移功能,从该主服务器下的从服务器中选取一台作为新的主服务器,从而保证整个系统在短时间内恢复可用。

Sentinel就是特殊的Redis服务器,和普通的Redis服务器共用一部分代码,但又有一套自己的数据结构和指令集(所以GET/SET等命令也就不可用)。

Sentinel建立的相关流程

1. 根据配置,初始化Master服务器连接

Sentinel服务器初始化时就会在自身的sentinelRedisInstance.masters中记录下所监视的所有主服务器信息。

连接服务器时,Sentinel不仅仅会建立普通的命令连接,也会建立一个订阅连接,用于创建并订阅主服务器的_sentinel_:hello频道。

额外创建订阅连接的原因除了订阅之外,是因为Redis的发布订阅模块并不提供确保到达的机制,消息发送时如果因为网络抖动而未到达接收方,那就会直接丢失这条消息。

完成第一步骤后,Sentinel就有了包含主服务器在内的集群拓扑。

2. 获取从服务器信息

Sentinel会以一定的频率向所监视的主服务器发送INFO命令,来获取对应主服务器的消息。

获取到的信息里面包括主从消息Replication,因此也就获取到了从服务器的信息。

3. 连接从服务器

在上一步获取到从服务器的信息以后,Sentinel同样也会和每个从服务器建立两条连接。

并按一定频率发送INFO命令,获取从服务器的具体信息。

至此Sentinel的当前网络结构拓扑中新增了从服务器。

4. 向订阅的所有主从服务器的_sentinel:hello_频道发送消息
PUBLIC _sentinel:hello_  "s_ip,s_port,s_runId,s_epoch,m_name,m_ip,m_port,m_epoch"

消息中包含的分别是Sentinel的ip,端口,runId,年代,主服务器的name,ip,端口,年代

注意,Sentinel向从服务器发送的消息,是他们所复制的主服务器的信息。

同样的消息是以一定的频率循环发送的。

因为所有的Sentinel服务器都会定于这个频道,所以也就相当于发送给所有的Sentinel服务器。

5. 接收_sentinel:hello_频道的消息,发现新加入的Sentinel

通过频道的订阅,Sentinel会收到急群众其他Sentinel的消息。

以此为根据Sentinel会在该主节点的数据实例中记录下所有监听他的Sentinel的相关信息。

6. 和其他的Sentinel建立命令连接

Sentinel之间不会建立订阅连接,仅仅只有命令连接。

至此,Sentinel系统完整的拓扑图构造完成。

和主服务器建立连接没得说,监视的就是主服务器的情况。

和从服务器建立连接则是为了故障转移之后的选主,需要和从服务器交互。

和其他的Sentinel则是因为选主,不可能在失效之后每个Sentinel各自选择一个从服务器升为主服务器。

image-20191109233459918

故障转移流程

1. 主观下线检测

Sentinel每秒都会向命令连接(包括主从服务器以及其他Sentinel)发送一个PING命令,并通过返回确定对方当前的状态。

有效返回有以下几种:PONGLOADINGMASTERDOWN。(这里暂时忽略有效返回代表的意思)

如果对方服务器在down-after-milliseconds毫秒内(配置文件中指明),没有返回一个有效返回,则当前服务器认定对方为主观下线,并修改实例对象中的状态。

如果是主服务器则进入客观下线检测的流程。

如果是从服务器,那么在修改完对象中的状态后,就不会有别的操作。

从服务器重新开启在线状态就需要通过Sentinel向其主服务器发送的INFO命令中的返回信息。

2. 客观下线检测

Sentinel会发送如下命令询问别的Sentinel服务器是否认为该主服务器已经下线:

SENTINEL is-master-down-by-addr ip port epoch runId

在接收到超过quorum(配置文件配置,一般是1/2)的确认下线之后,当前Sentinel才会认为目标服务器却是下线了,并开启以下的故障转移流程。

前三个参数都是下线的Master的,但是run Id不是。

run Id可以为符号*表示此次仅为客观下线检测,也可以为当前Sentinel的run Id,该参数用于此后的选举过程,表示希望选举自己为头节点。

对于以上命令,接收的Sentinel会回复三个参数:

  1. down_state - 目标主服务器下线状态,1为已下线,0为未下线
  2. leader_runid - 表示当前Sentinel选定的头节点的run id,*表示此次仅为客观下线检测
  3. leader_epoch - 表示选定的头节点的 epoch
3. 选举头节点

首先,监视下线节点的所有Sentinel会协商,选举出一个Leader,来完成接下来的故障转移功能。

Redis的选举算法是对Raft算法简单实现。

Raft算法是一种分布式日志共识算法,如果不了解Raft算法,可以参考Raft算法的动画演示做一个简单了解。

选举有以下规则:

  1. 每次选举不论是否成功,当前epoch自增一次。
  2. 每次选举,每个Sentinel都只能选举一个头节点,且不能修改。
  3. 每个Sentinel都有资格成为头节点。
  4. 每个发现客观下线的SENTINEL都会要求其他节点选举自己为头节点。
  5. 如果超过半数以上的Sentinel选了同一个Sentinel,那么选举成功,该Sentinel成为头节点。
  6. 给定时限之内没有结果,则当前选举失败,开启下一轮选举。
  7. SENTINEL is-master-down-by-addr作为Sentinel节点间的通信。
4. 故障转移

由上一步选举产生的头节点执行故障转移操作。

选择新的主服务器

从下线主服务器的所有从服务器中挑选一个作为新的主服务器,发送SLAVEOF NO ONE命令,并以每秒一次的频率发送INFO命令,监控该从服务器当前的状态。

SLAVEOF NO ONE并不一定能很快到达并执行,所以需要状态监控。

image-20191109233931424

就是上图中的role字段从slave变为master,表示从服务器已经变为主服务器。

修改其他从服务器的复制目标

直接发送SLAVEOF ip port命令,将其他从服务器的复制目标指向新的主服务器

将旧主服务器降为从服务器

和第二步的发送命令不同,此时旧服务器可能还处于掉线状态并没办法接受到SLAVEOF命令,所以此处的修改仅仅在Sentinel内部的数据结构中。