主从复制

发布时间 2023-12-19 10:22:43作者: 梅丹隆

一、什么是主从复制

1、简介

  • 在分布式环境中,数据副本 _**(Replica) **_复制 _**(Replication)**__** **_作为提升系统可用性和读写性能的有效手段被大量应用系统设计中,Redis 也不例外。
  • Redis 作为单机数据库使用时,适用常见有限且存在单点宕机问题,无法维持高可用。
  • 因此 Redis 允许通过 _**SLAVEOF**_ 命令或者 _**slaveof**_ 配置项来让一个 Redis server 复制另一个 Redis server 的数据集和状态,我们称之为主从复制,主服务器下文称 _**master**_,从服务器下文称 _**slave**_
  • Redis 采用异步的复制机制。

复制机制的运行依靠三个特性:

  1. 当一个 _**master**_ 和一个 _**slave**_ 连接正常时,_**master**_ 会发送一连串的命令流来保持对 _**slave**_ 的更新,以便于将自身数据集的变更复制给 _**slave**_ :包括客户端的写入、key 的过期或被逐出等
  2. _**master**_slave 之间的连接断开后(断开的原因可能是网络问题或者连接超时) _**slave**_ 重连上 _**master**_ 并尝试进行部分重同步,这意味着它只会尝试获取在断开连接期间内丢失的命令流
  3. 当无法进行部分重同步时, _**slave**_ 会请求进行全量重同步。这会涉及到一个更复杂的过程,例如 _**master**_ 需要创建所有数据的快照,将之发送给 _**slave**_ ,之后在数据集更改时持续发送命令流到 _**slave**_

2、主从复制的优点

  • _**master**_ 可以关闭持久化机制,减少不必要的 IO 操作且降低延迟,对于以性能著称的组件来说极为重要
  • _**slave**_ 虽然不能处理写请求,但是可以处理读请求,从而增加读取操作的吞吐量。但由于复制机制的原因,主从数据存在不一致的时间窗口
  • 使得 Redis 可以告别单机版本的单点风险,采用副本形式提高可用性,在 _**master**_ 宕机时可以将 _**slave**_ 提升为 _**master**_ 继续向外提供服务,也为 Redis 集群模式的诞生奠定了技术基础

这里需要注意的是 Redis 2.8 版本之前与之后采用的复制方式不尽相同,主要区别在将成本极高的 _**sync**_ 替换为 _**psync**_,增加了断线重连情况下根据主从保存的 offset 即复制偏移量进行增量同步的功能,考虑到目前 Squirrel 线上集群绝大部分收敛至 3.2.8 版本,因此本文不再赘述旧版复制机制

3、主从复制和集群

有时我们会混淆这两个概念,主从复制也是采用了多个 Redis 节点,和 Redis 集群表面上看很接近,那二者究竟有什么区别呢?

Replication
- 复制机制中包含了一个 _**master**_ 和若干个 _**slave**_
- 其中写请求只能 _**master**_ 来处理,数据的变更转化为数据流异步发送给 slaves 进行更新
- 读请求则可以根据使用场景来规定是否由 _**slave**_ 处理从而增加系统的读吞吐量
- 一旦 _**master**_ 发生故障,_**slave**_ 可以被提升为 _**master**_ 从而继续提供服务
- 因此总结起来,_**slave**_ 在复制机制的场景下,可以提供故障恢复、分担读流量和数据备份的功能。
Cluster
- 集群机制的使用意味着你的数据量较大
- 数据会根据 _**Key**_ 计算出的 _**slot**_ 值自动在多个分片上进行分区_(_**Partitioning**_)_
- 客户端对某个 _**Key**_ 的请求会被转发到持有那个 _**Key**_ 的分片上。
- 分片由一个 _**master**_ 和若干个 _**slave**_ 组成,二者间通过复制机制同步数据。
- 因此总结来看,集群模式更像分区和复制机制的组合。

二、如何开启主从复制

需要注意,主从复制的开启,完全是在 _**slave**_ 发起的;不需要我们在 _**master**_ 做任何事情。
_**slave**_ 开启主从复制,有三种方式:

# 配置文件,在从服务器的配置文件中加入:
slaveof <masterip> <masterport>

#启动命令,Redis server 启动命令后加入:
--slaveof <masterip> <masterport>

# 客户端命令 Redis server 启动后,直接通过客户端执行命令:
slaveof <masterip> <masterport>,则该 Redis 实例成为 slave。

三、主从复制机制的演变

Redis 2.64.0 开发人员对复制流程进行逐步的优化,以下是演进过程:

  • 2.8 版本之前 Redis 复制采用 _**sync**_ 命令,无论是第一次主从复制还是断线重连后再进行复制都采用全量同步,成本高
  • 2.8 ~ 4.0 之间复制采用 _**psync**__** **_命令,这一特性主要添加了 Redis 在断线重连时候可通过 offset 信息使用部分同步
  • 4.0 版本之后也采用 _**psync**_,相比于 2.8 版本的 _**psync**_ 优化了增量复制,这里我们称为 _**psync2**_2.8 版本的 psync 可以称为 _**psync1**_

我们先介绍 _**psync1**__**psync2**_ 通用的复制原理,然后再细谈二者的区别和优化点,至于旧版 sync 的机制本文不再赘述。

四、主从复制的原理

主从复制过程可分为三个阶段:复制初始化数据同步命令传播

1、复制初始化阶段

  • 当执行完 _**slaveof**_ 命令后

  • _**slave**_ 根据指明的 _**master**_ 地址向 master 发起 socket 连接

  • master 收到 socket 连接之后将连接信息保存,此时连接建立完成

  • socket 连接建立完成以后,_**slave**__**master**_ 发送 _**PING**_ 命令,以确认 _**master**_ 是否存活

  • 此时的结果返回如果是 _**PONG**_ 则代表 _**master**_ 可用

  • 否则可能出现超时或者 _**master**_ 此时在处理其他任务阻塞了,那么此时 slave 将断开 socket 连接,然后进行重试;

  • 如果 _**master**_ 连接设置了密码,则 _**slave**_ 需要设置 _**masterauth**_ 参数

  • 此时 _**slave**_ 会发送 _**auth**_ 命令,命令格式为 _**auth + 密码**_ 进行密码验证,其中密码为 _**masterauth**_ 参数配置的密码

  • 需要注意的是如果 _**master**_ 设置了密码验证,从库未配置 _**masterauth**_ 参数则会报错,socket 连接断开。

  • 当身份验证完成以后,_**slave**_ 发送自己的监听端口,_**master**_ 保存其端口信息

  • 此时进入下一个阶段:数据同步阶段。

2、数据同步阶段

  • _**master**__**slave**_ 都确认对方信息以后,便可开始数据同步
  • 此时 _**slave**_ 向主库发送 _**psync**_ 命令(需要注意的是 redis 4.0 对 2.8 版本的 psync 做了优化),主库收到该命令后判断是进行增量同步还是全量同步,然后根据策略进行数据的同步
  • _**master**_ 有新的写操作时候,此时进入复制第三阶段:命令传播阶段。

3、命令传播阶段

  • 当数据同步完成以后,在此后的时间里 _**master-slave**_ 之间维护着心跳检查来确认对方是否在线
  • 每隔一段时间(默认10秒,通过 repl-ping-slave-period 参数指定)_**master**__**slave**_ 发送 PING 命令判断 _**slave**_ 是否在线
  • _**slave**_ 每秒一次向 _**master**_ 发送 _**REPLCONF ACK**_ 命令命令格式为:REPLCONF ACK {offset} ,其中 _**offset**__**slave**_ 保存的复制偏移量,作用有:
    • 汇报自己复制偏移量,_**master**_ 会对比复制偏移量向 _**slave**_ 发送未同步的命令
    • 判断 _**master**_ 是否在线
  • _**slave**_ 接送命令并执行,最终实现与主库数据相同

五、PSYNC1 和 PSYNC2

1、PSYNC1

  • 为了解决旧版 _**SYNC**_ 在处理断线重连复制场景下的低效问题
  • Redis 2.8 采用 _**PSYNC**_ 代替 _**SYNC**_** 命令。**
  • _**PSYNC**__** **_命令具有全量同步和部分同步两种模式

1.1、全量重同步

  • 前者和 SYNC 大致相同
  • 都是让 _**master**_ 生成并发送 RDB 文件
  • 然后再将保存在缓冲区中的写命令传播给 _**slave**_ 来进行同步
  • 相当于只有同步和命令传播两个阶段

1.2、部分重同步

  • 部分同步适用于断线重连之后的同步
  • _**slave**_ 只需要接收断线期间丢失的写命令就可以,不需要进行全量同步。
  • 为了实现部分同步,引入了复制偏移量__**offset**_、复制积压缓冲区_**replication backlog buffer**_和运行 ID _**run_id**__三个概念
    | 复制偏移量 |
    - 执行主从复制的双方都会分别维护一个复制偏移量
    - _**master**_ 每次向 _**slave**_ 传播 _**N**_个字节,自己的复制偏移量就增加 N
    - 同理 _**slave**_ 接收 _**N**_ 个字节,自身的复制偏移量也增加 _**N**_
    - 通过对比主从之间的复制偏移量就可以知道主从间的同步状态。
    |
    | --- | --- |
    | 复制积压缓冲区 |
    - 复制积压缓冲区是 _**master**_ 维护的一个固定长度的 FIFO 队列,默认大小为 1MB。
    - 当 _**master**_ 进行命令传播时,不仅将写命令发给 _**slave**_ 还会同时写进复制积压缓冲区
    - 因此 _**master**_ 的复制积压缓冲区会保存一部分最近传播的写命令。
    - 当 _**slave**_ 重连上 _**master**_ 时会将自己的复制偏移量通过 _**PSYNC**_ 命令发给 _**master**_
    - _**master**_ 检查自己的复制积压缓冲区,如果发现这部分未同步的命令还在自己的复制积压缓冲区中的话就可以利用这些保存的命令进行部分同步
    - 反之如果断线太久这部分命令已经不在复制缓冲区中了,那没办法只能进行全量同步。
    |
    | 运行 ID |
    - 令人疑惑的是上述逻辑看似已经很圆满了,这个 run_id 是做什么用呢?
    - 其实这是因为 _**master**_ 可能会在 _**slave**_ 断线期间发生变更
    - 例如可能超时失去联系或者宕机导致断线重连的是一个崭新的 master,不再是断线前复制的那个了。
    - 自然崭新的 _**master**_ 没有之前维护的复制积压缓冲区,只能进行全量同步。
    - 因此每个 Redis server 都会有自己的运行 ID,由 40 个随机的十六进制字符组成。
    - 当 _**slave**_ 初次复制 _**master**_ 时,_**master**_ 会将自己的运行 ID 发给 _**slave**_ 进行保存
    - 这样 _**slave**_重连时再将这个运行 ID 发送给重连上的 _**master**_
    - _**master**_ 会接受这个 ID 并于自身的运行 ID 比较进而判断是否是同一个 _**master**_
    |

1.3、PSYNC1的流程图

image.png

  • 如果 slave 以前没有复制过任何 master,或者之前执行过 SLAVEOF NO ONE 命令,那么 slave 在开始一次新的复制时将向主服务器发送 PSYNC ? -1 命令,主动请求 master 进行完整重同步(因为这时不可能执行部分重同步)。

  • 相反地,如果 slave 已经复制过某个 master,那么 slave 在开始一次新的复制时将向 master 发送 PSYNC 命令:

    • 其中 runid 是上一次复制的 master 的运行ID,
    • offset 则是 slave 当前的复制偏移量
    • 接收到这个命令的 master 会通过这两个参数来判断应该对 slave 执行哪种同步操作。
  • 根据情况,接收到 PSYNC 命令的_** master _会向 _slave**_ 返回以下三种回复的其中一种:

    • 如果 master 返回 +FULLRESYNC 回复,那么表示 master 将与 slave 执行完整重同步操作:其中 runid 是这个 master 的运行 ID,slave 会将这个 ID 保存起来,在下一次发送 PSYNC 命令时使用;而 offset 则是 master 当前的复制偏移量,slave 会将这个值作为自己的初始化偏移量
    • 如果 master 返回 +CONTINUE 回复,那么表示 master 将与 slave 执行部分同步操作,slave 只要等着 master 将自己缺少的那部分数据发送过来就可以了
    • 如果 master 返回 -ERR 回复,那么表示 master 的版本低于 Redis 2.8,它识别不了 psync 命令,slave 将向 master 发送 SYNC 命令,并与 master 执行完整同步操作
  • 由此可见 psync 也有不足之处

  • slave 重启以后 master runid 发生变化,也就意味者 slave 还是会进行全量复制

  • 而在实际的生产中进行 slave 的维护很多时候会进行重启

  • 而正是有由于全量同步需要 master 执行快照,以及数据传输会带不小的影响。因此在 4.0 版本,psync 命令做了改进,我们称之为 psync2

2、PSYNC2

  • Redis 4.0 版本新增 混合持久化,还优化了_psync_(以下称 psync2
  • psync2 最大的变化支持两种场景下的部分重同步
    • 一个场景是 slave _提升为 _master _后,其他 _slave _可以从新提升的 _master _进行部分重同步,这里需要 _slave 默认开启复制积压缓冲区;
    • 另外一个场景就是 slave 重启后,可以进行部分重同步。这里要注意和 psync1 的运行 ID 相比,这里的复制 ID 有不一样的意义。

2.1、优化细节

  • Redis 4.0 引入另外一个变量 _**master_replid 2**_ 来存放同步过的 _**master**_复制 ID
  • 同时复制 ID_**slave**_ 上的意义不同于之前的运行 ID复制 ID_**master**_ 的意义和之前运行 ID 仍然是一样的
  • 但对于_** _**slave**_ **_来说,它保存的复制 ID(即 replid) 表示当前正在同步的 _**master**_复制 ID
  • _**master_replid 2**_ 则表示前一个 _**master**_复制 ID(如果它之前没复制过其他的 _**master**_,那这个字段没用),这个在主从角色发生改变的时候会用到。
struct redisServer {
    ...
    /* Replication (master) */                                        
    char replid[CONFIG_RUN_ID_SIZE+1];  /* My current replication ID. */
    char replid2[CONFIG_RUN_ID_SIZE+1]; /* replid inherited from master*/ 
  • slave 在意外关闭前会调用 _**rdbSaveInfoAuxFields**_ 函数把当前的复制 ID(即关闭前正在复制的 _**master**_replid,因为 _**slave**_ 中的 replid 字段保存的是 _**master**_复制 ID) 和复制偏移量一起保存到 RDB 文件中
  • 后面该 _**slave**_ 重启的时候,就可以从 RDB 文件中读取复制 ID 和复制偏移量,然后重连上 _**master**__**slave**_ 将这两个值发送给 _**master**__**master**_ 会如下判断是否允许 _**psync**_
// 如果 slave 发送过来的复制 ID 是当前 master 的复制 ID, 说明 master 没变过
if (strcasecmp(master_replid, server.replid) &&   
    // 或者和现在的新 master 曾经属于同一 master
    (strcasecmp(master_replid, server.replid2) ||      
     // 但同步进度不能比当前 master 还快
     psync_offset > server.second_replid_offset)) {
  ... ...
}

// 判断同步进度是否已经超过范围
if (!server.repl_backlog ||                                                        
    psync_offset < server.repl_backlog_off ||                                      
    psync_offset > (server.repl_backlog_off + server.repl_backlog_histlen)) {                                                                                  
    ... ...
}  
  • 另外当节点从 _**slave**_ 提升为 _**master**_ 后,会保存两个复制 ID(之前角色是 slave 的时候 _**replid2**_ 没用,现在要派上用场了),分别是 _**replid**__**replid**_** 2**
  • 其他 _**slave**_ 复制的时候可以根据第二个复制 ID 来进行部分重同步。对应上述代码中第二行判断的情况。