一、什么是主从复制
1、简介
- 在分布式环境中,
数据副本 _**(Replica) **_
和复制 _**(Replication)**_
_** **_作为提升系统可用性和读写性能的有效手段被大量应用系统设计中,Redis
也不例外。 Redis
作为单机数据库使用时,适用常见有限且存在单点宕机问题,无法维持高可用。- 因此
Redis
允许通过_**SLAVEOF**_ 命令
或者_**slaveof**_ 配置项
来让一个Redis server
复制另一个Redis server
的数据集和状态,我们称之为主从复制,主服务器下文称_**master**_
,从服务器下文称_**slave**_
- Redis 采用异步的复制机制。
复制机制的运行依靠三个特性:
- 当一个
_**master**_
和一个_**slave**_
连接正常时,_**master**_
会发送一连串的命令流来保持对_**slave**_
的更新,以便于将自身数据集的变更复制给_**slave**_
:包括客户端的写入、key 的过期或被逐出等 - 当
_**master**_
和 slave_**slave**_
重连上_**master**_
并尝试进行部分重同步,这意味着它只会尝试获取在断开连接期间内丢失的命令流 - 当无法进行部分重同步时,
_**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.6
到 4.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、全量重同步
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的流程图
-
如果 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 执行完整同步操作
- 如果 master 返回 +FULLRESYNC
-
由此可见 psync 也有不足之处
-
当 slave 重启以后 master runid 发生变化,也就意味者 slave 还是会进行全量复制
-
而在实际的生产中进行 slave 的维护很多时候会进行重启
-
而正是有由于全量同步需要 master 执行快照,以及数据传输会带不小的影响。因此在 4.0 版本,psync 命令做了改进,我们称之为 psync2。
2、PSYNC2
- Redis 4.0 版本新增 混合持久化,还优化了_psync_(以下称 psync2)
- psync2 最大的变化支持两种场景下的部分重同步
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 来进行部分重同步。对应上述代码中第二行判断的情况。