Redis 复制
概述
注意:以下会交替使用“从实例”和“副本”,他们表示相同的含义。
Redis 最基础的复制(Replication)功能,建立在一主多从的 Redis 实例关系上,在所有需要作为从属实例中执行 slaveof/replicaof
命令指定主实例的 ip 和端口,即可完成主从关系的建立。主从实例之间采用异步复制,保证主从的数据最终一致性。复制的目的是为了实现高可用、高性能。当主挂了,可以将从实例切换(提升)为主实例,实现了高可用。另外将读请求发送到从实例上,提升 Redis 的处理能力。
复制主要依赖以下三种机制:
- 当主从实例之间连接良好,主实例会持续的将任何会改变数据的命令(如:客户端的发送到主实例的写命令、键过期或被驱逐等)发送给从实例。
- 当主从之间连接由于网络或其他任何原因断开或者超时了,副本会尝试重新连接主实例并尝试部分同步(partial resynchronization),部分同步仅仅会同步断开这段时间内没有收到的命令。
- 从实例如果断开过长时间,可能无法完成部分同步。这个其实也容易理解,因为主实例内存中能够记录的命令是有限的,超限了之后,副本连接后就只能进行全量复制。
Redis 默认使用异步复制,异步复制具有高性能低延时的优点,能够满足大多数的应用场景。主实例发送命令给副本但是并不会阻塞等待命令返回,副本则周期性的向主实例反馈收到的数据(这个地方的数据其实是命令?)。
客户端可以使用 WAIT 命令同步复制,wait 会阻塞客户端直至指定数量的副本收到 wait 命令之前的写命令或者超时,不论成功还是失败,wait 都会返回实际达到的副本数,因此客户端需要自行判断返回的数量是否大于或等于 wait 指定的数量。即便使用 wait 也不能保证不丢数据,在故障转移过程中,依然可能会丢失数据。
数据复制你应该知道的东东
- Redis 使用异步复制,副本会定期向主实例反馈接收到的数据;
- 一个主可以有多个副本;
- 副本还可以接受其他副本的连接,这样可以一直层叠组成树状结构,Redis 4.0 之后所有的子副本数据最后都会和主实例保持一致;
- 复制在主实例这边是异步的,也就说复制的同时,主实例依然可以处理请求;
- 复制在副本这边很大程度上说也是异步的。你可以配置副本在初始同步的时候使用老的数据处理请求,但是在初始同步完成之后,必须删除老的数据集并加载新的数据集,这个删除和加载过程会使副本阻塞,Redis 4.0 可以配置删除在其他线程处理,但是加载新数据集依然会阻塞副本;
- 复制可以用来做横向扩展,我们可以使用多个副本来处理读请求以分担主实例的压力,或者配置高可用、提升数据安全。
- 可以关闭主实例的持久化以去除主实例写数据的开销,转而开启副本的持久化。但是这么做需要注意数据安全问题,具体看下一节。
主实例关闭持久化的安全问题
假设我们有一个主实例 M (M 关闭了数据持久化)和一个副本 C,且 M 有监控程序 P,一旦 P 发现 M 挂了就会自动重启 M,我们分析下会发生什么: M 一旦挂了,P 就会自动重启 M,但是 M 关闭了持久化,也就是说 M 重启之后是没有数据的,此时副本会同步 M 的数据,导致副本的数据被清空。即便使用了 sentinel 的自动故障转移,依然可能有问题,因为故障的检测需要时间,假设故障还没检测出来,P 已经率先重启了 M,那么副本的数据还一样会因为同步主实例数据而被清空。
复制是如何工作的
每个主实例都有一个 “Replication Id”(以下用 rid 代替),这是一个非常大的伪随机字符串用来唯一标识一个实例数据集,但是仅有 rid 还不够,数据集是随时间变化的,所以数据集还有一个 offset,offset 用来标识数据集的新旧程度。这个 offset 随着数据集的改变一直增加,即便是没有副本连接到主实例,offset 也会随着数据集的改变而增加。所以一个数据集可以表示为以下二元组:
Replication Id, Offset
注:每当一个新的主实例启动的时候,或者一个副本被提升为主实例的时候,会生成 rid,副本连接主实例,在握手完成之后会继承主实例的 rid。当两个实例的 rid 相同并且 offset 相同,那么他们的数据必定完全相同。
全量同步
全量同步的细节:
- 主实例开启一个后台进程生成 RDB 文件,与此同时缓存来自客户端的写命令;
- 当 RDB 文件生成完毕,将该文件传送给副本。副本会将收到文件放到磁盘上,之后载入内存;
- 主实例将缓存的写命令发送给副本。副本应用这些命令就可以达到和主实例相同的状态。
部分同步
由于主从之间的连接不稳定,可能导致主从连接断开一段时间,或者从实例挂了一段时间,当副本重新连接上从实例的时候,可能会执行部分同步。为什么说可能?重新连上之后,如果副本的 offset 在主实例的 backlog 缓存记录的范围之内,那么就可以执行部分同步,否则只能做全量同步。
注:可以将 backlog 缓存理解为一个内存中一块大小有限的 buffer,记录最近的一段时间范围内的写命令。
rid 的细节
其实一个实例有两个 rid,一个主 id,一个次 id(以下使用 sid 代替次 id)。这主要是为了故障转移之后,副本连接新的主实例只需要部分同步,而不需要全量同步。我们一起来看为什么需要两个 rid:
副本(C)被提升为主实例的之后,C 会将 sid 设置为之前主实例的 rid,并记住之前主实例的 offset(记为 OldOffset),同时 C(此时已经是主实例)会生成一个新的 rid,用于记录新的数据变化历史。当其他副本连接 C 之后,首先会根据 C 的 sid 和 OldOffset 进行部分同步,部分同步完成后,会尝试切换到 C 的主 id 继续部分同步。可见发生故障转移后,副本和新主实例之间的部分同步分为两部分,一部分可以认为是和前主实例的部分同步,另一部分是和当前主实例的部分同步。
注1:部分同步不支持 aof 文件,然而我们可以在关闭副本之前转换为 RDB 方式,重启副本并部分同步完成之后,再次开启 aof。
注2:可以使用 shutdown
命令优雅地关闭副本。
不依赖磁盘复制
之前提到,全量同步依赖主实例生成 rdb 文件并写入磁盘,之后发送到副本,副本接收 rdb 也是先写到磁盘再加载到内存。如果磁盘很慢,对双方都产生很大开销,Redis 2.8.18 之后,主实例支持在子进程中直接通过 socket 发送 rdb 到副本而不需经过磁盘作为中转。
我们可以修改 redis.conf 开启无盘复制,配置如下:
# yes 启用无盘复制
repl-diskless-sync yes
repl-diskless-sync-delay 5
repl-diskless-sync-delay 用来控制无盘复制的等待时间,等待更多的副本连接上主实例之后才开始复制,因为一旦复制开始,之后连上的副本复制请求只能被放到等待队列中,等到当前复制完成后才能开始。
副本配置
在副本配置文件加以下配置,指定主实例的 ip 和 端口,即可完成主从关系的建立。也可直接连接副本,发送 replicaof 命令。
replicaof 192.168.1.1 6379
配置只读副本:
replica-read-only yes
注:副本的作用是同步主实例的数据,我们不应该向副本写入数据,这这会导致主从实例数据不一致。另外在 4.0 之后往副本写入数据,不会被同步到连接该副本的副本。
配置副本连接到主实例的所需的密码:
# 通过 redis-cli 连副本,并执行 config set 命令运行时修改连接密码。
config set masterauth <password>
# 通过配置文件配置连接密码
masterauth <password>
数据安全
我们可以配置 Redis 主实例至少有 N 个副本连接良好的情况下,才接收写,否则拒绝写入。工作原理如下:
- 副本每隔一秒向主实例发送 ping,通知主实例当前复制到哪;
- 主实例则记录上次收到副本发来的 ping 的时间点(上次时间点记为 lt);
- 如果当前至少有 N 个(N 可配置)副本与主实例的延迟(延迟 = 当前时间 - lt)小于用户配置的最大延迟时间,则接受写入,否则拒绝。
通过这样的策略,Redis 尽最大努力保证数据的安全性,但是由于复制是异步的,仍然不能保证数据百分百不丢。具体配置如下:
# 配置当前至少有 N 个副本延迟小于指定值
min-replicas-to-write <number of replicas>
# 配置副本最大延迟时间,只有小于该延迟时间才认为副本可用
min-replicas-max-lag <number of seconds>
副本如何处理 key 过期
通过 ttl 我们可以给一个 key 设置过期时间,对于这种类型的 key,Redis 处理方式如下:
- 副本自身不会主动删除过期 key,当主实例删除过期 key 的时候,会发送一个 DEL 命令给副本,副本接收到该命令才删除 key;
- 在这种主实例主导删除的模式下,副本中有些 key 可能实际上已经过期,但是主实例还没来得及发送 DEL 命令过来,此时副本会依据自身的逻辑时钟判定 key 过期,因此不会导致客户端读取到过期 key 的问题。
- 在 lua 脚本执行期间,从逻辑上讲,主实例上的时间是冻结的。换句话说在脚本执行期间,不会发生 key 过期。
INFO 和 ROLE 命令
这两个命令可以提供主从实例的复制信息。其中 ROLE 命令对计算机友好,INFO 命令对人友好。
副本的 maxmemory 配置
默认情况下,副本不支持 maxmemory 配置,因为副本的 key 过期依赖主实例的 del 命令,副本可能在一个极短的时间窗口内比主实例占用更多的内存。不建议设置副本的 maxmemory,如果头铁一定要设置也不是不可以:
replica-ignore-maxmemory no
温馨提示:反馈需要登录