前言
虽然 Redis 的性能非常优秀,能快速处理请求,但它有时也会衰老为步履蹒跚的老人,比如面对以下情况:
- ① 存储过多元素: 若涉及的元素达到上万个甚至上百万个时,命令执行耗时可能需要以秒来进行计算
- ② 单机性能瓶颈:即使一个命令只需要花费 10 ms 就能完成,单个 Redis 实例 1s 也只能处理 100 个命令
试问谁不想拥有青春永驻,充满活力呢?
Redis 也妄想如此,但可惜的是它只是一项技术工具,无法自适应极端场景。不过没关系,程序员作为投骰子的那个人,完全可以将其扩展,为这头猛虎安上翅膀。
面对情况 ①,可能是业务场景数据结构使用的不合理,可视具体代码考虑优化,本文不做详细讨论。
面对情况 ②,是否可以将单台 Redis 实例增加为多台,将第一台 Redis 实例中的数据复制到其他实例中呢?
Redis 本身考虑到了第 ② 点,因此实现了一个功能——主从复制。
简介
在 Redis 中,用户通过执行slaveof
命令或者设置配置文件slaveof
选项的方式,让一个服务器(从机)去复制(replicate
)另一个服务器(主机)数据的过程,称为主从复制。
进行复制后的从机最终将和主机将保存相同的数据,此现象被称为数据库状态一致或一致。
特性
主从复制将带来以下好处:
- 性能提升:通过衍生的读写分离功能,可让主机专心处理客户端传来的写操作(当然主机是可以进行读操作的),而只让从机处理客户端的读操作,不同服务器各司其职,压力分摊,极大的提高了程序运行的效率
- 数据安全:数据保存了多份,即使遇到主服务器磁盘坏掉的情况,其他从服务器还保留着相关的数据,不至于数据全部丢失
- 高可用:当有了主从同步之后,当主服务器节点宕机之后,从节点可上位为主节点,保证写请求可用
除此之外,后续文章讨论的哨兵安全机制也基于此功能进行实现。
搭建
机器规划
主机 | redis 路径 |
---|---|
192.168.1.8 | /data/software/redis/ |
192.168.1.12 | /data/software/redis/ |
192.168.1.13 | /data/software/redis/ |
配置多台 Redis
1 | # 准备目录 |
1 | bind 0.0.0.0 |
启动多台 Redis
分别启动三台 Redis 服务器:
1 | redis-server /data/software/redis/conf/master_slave.conf |
输出以下信息:1
2
3
4
5
6
7
8
96799:C 26 Dec 2022 04:29:13.281 # Redis version=6.2.6, bits=64, commit=00000000, modified=0, pid=6799, just started
6799:C 26 Dec 2022 04:29:13.281 # Configuration loaded
6799:M 26 Dec 2022 04:29:13.282 * Increased maximum number of open files to 10032 (it was originally set to 1024).
6799:M 26 Dec 2022 04:29:13.282 * monotonic clock: POSIX clock_gettime
6799:M 26 Dec 2022 04:29:13.283 * Running mode=standalone, port=6379.
6799:M 26 Dec 2022 04:29:13.283 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
6799:M 26 Dec 2022 04:29:13.283 # Server initialized
6799:M 26 Dec 2022 04:29:13.283 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
6799:M 26 Dec 2022 04:29:13.283 * Ready to accept connections
连接多台 Redis
启动完成后,我们开启 3 个终端来分别连接 3 个服务器,使用redis-cli -p [port]
命令连接指定端口:
1 | redis-cli -p 6379 |
查看多台 Redis 信息
现在,我们可以通过info replication
命令查看当前服务器的主从信息,详解见注释:
1 | 127.0.0.1:6379> info replication |
由于我们并没有进行特殊操作,所以开启的 3 个服务器都是主机,因此它们的信息和上面的基本相同。
那么,如何使这些服务器形成主从模式呢?
开启主从模式
要想使这些服务器形成主从模式,需要使用slaveof [ip] [port]
命令指定某个 Redis 成为另一个 Redis 的从机。
现在,我们尝试指定192.168.1.12:6379
节点成为192.168.1.8:6379
节点的从机:
1 | # 192.168.1.12:6379 机器执行 |
命令完成之后,我们来查看下192.168.1.12:6379
节点现在的信息情况:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24# 192.168.1.12:6379 机器
127.0.0.1:6379> info replication
# Replication
role:slave // 从机
master_host:192.168.1.8 // 主机IP
master_port:6379 // 主机端口
master_link_status:up // 连接状态
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_read_repl_offset:28
slave_repl_offset:28
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:abadda1edad8e5a7f6d1156916af3e8b30ff95ea // 服务器唯一 ID
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:28 // 复制偏移量
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:28
显而易见,192.168.1.12:6379
节点的信息发生了变化,它成为了从机,我们再看看主机节点192.168.1.8:6379
的信息:
1 | # 192.168.1.8:6379 机器 |
可以看到,主机的信息也发生了变化,并且它们的偏移量(后面介绍)基本保持相同。
测试
最开始提到过主机和从机的状态总保持一致,即主机从机数据总要保持一致,那么我们下面进行一个实验:写入一些数据进入主机,然后观察从机是否也存在相应数据。
写入数据到主机
1 | # 192.168.1.8:6379 机器 |
观察从机数据情况
前面我们写入了 2 个键到主机,那么从机应该也有这两个键,那么查看一下:
1 | # 192.168.1.12:6379 机器 |
验证结果
结果是符合预期的,但这又是怎么做到的呢?
工作原理
从前一小节实验可知:从机可以将主机的数据复制过来,那么这个过程具体是怎么实现的?
过程揭秘
当从机开始执行slaveof
命令,从机内部将逐步进行以下步骤:
- ① 设置主机的地址和端口;
- ② 建立与主机的套接字连接;
- ③ 向主机发送 PING 命令;
- ④ 进行身份验证;
- ⑤ 发送端口信息;
- ⑥ 同步主机数据;
- ⑦ 进入命令传播阶段
前五个步骤用于建立连接,本节探讨的主要是 ⑥ 和 ⑦,也就是主从复制的过程。
旧新复制
需要注意的是,在 Redis 中,主从复制存在新旧两个版本,高版本的新版复制是在旧版复制功能上的增强,因此,我们首先需要研究下旧版复制。
旧版复制
对 Redis 的复制操作而言,分为两个步骤:
- ① 同步(
sync
):用于将从机的数据库状态更新至主机当前所处的数据库状态 - ② 命令传播(
command propagata
):当主机的数据库状态被修改,导致主从机的数据库状态出现不一致时,重新调节主从机数据库为一致状态
步骤
① 同步
当客户端在从机发送slaveof [ip] [port]
命令,要求从机复制主机时,从机首先需要执行同步操作。
同步操作执行过程及作用说明如下:
- ① 从机向主机发送
sync
命令; - ② 收到
sync
命令的主机执行bgsave
命令,在后台生成一个 RDB 文件,并使用一个缓冲区记录从现在开始执行的所有写命令; - ③ 当主机的
bgsave
命令执行完毕时,会将该命令生成的 RDB 文件发送给从机,从机接受并载入这个文件,更新自己的数据库状态至主机执行bgsave
命令时的数据库状态; - ④ 主机将记录在缓冲区里的所有写命令发送给从机,从机执行这些写命令,将自己的数据库状态更新至主机数据库当前所处状态
② 命令传播
当同步完成之后,主从机的数据库状态将保持一致,但是这种状态并非一成不变,每当主机执行客户端发送的写命令时,主机的数据库都可能被更改,导致主从机状态不再一致。
举个例子,当主从机刚刚完成同步操作,它们数据库都保存了 3 个相同的键k1
、k2
、k3
。
若这时,客户端向主机发送命令del k2
,那么主机执行完该命令后主从机数据库将不再一致:主机数据库已删除了键k2
,但从机还存在该键。
为了让主从机再回到一致状态,主机需要对从机执行命令传播操作:主机会将自己执行的造成主从机不一致的那条写命令,发送给从机执行,之后主从机才能再次回到一致状态。
那么对示例而言:主机删除k2
命令后还会将该命令发送给从机执行,最后主从机都只剩 2个键k1
和k3
,状态达成一致。
缺陷
对旧版复制而言,是存在了一定缺陷的,为什么这么说呢?
由于 Redis 中的主从复制可以分为以下两种情况:
- 初次复制:从机从来没复制过任何主机,或者从机当前复制的主机和上次复制的主机不同
- 断线后重复制:处于命令传播阶段的主从机因为网络原因而中断了复制,但从机通过自动重连重新连接了主机,并继续复制主机
其中,断线后重复制的效率十分低下,因为它重连后是重新复制整个主机,而不是从断线后的状态接着复制。因此断线重连就需要从机重新发送sync
同步命令,而该命令是十分耗费资源的(主机重新生成 RDB 文件,占用其 CPU 、内存、磁盘等资源)
断线后重复制的情况,类似于我们平常游戏刷副本,刷到一半掉线,你再次上线却回到了出发点,需要重新刷,耗时耗力,也不符合常理。
那这样肯定不行的啊,我刷副本刷一半掉线立马重连你告诉我要重新刷,毫无用户体验啊!
对主从复制同样,因此 Redis 后来引入了新版复制功能来解决该问题。
新版复制
为了解决旧版缺陷,2.8 版本后的 Redis 服务器开始使用psync
命令来代替sync
命令执行复制时的同步操作。
sync
命令可根据不同的同步类型进行不同的流程步骤以应对不同情况,比如:
- 完整重同步(
full resynchronization
):用于处理初次复制情况,执行步骤基本和旧版sync
命令同步的执行步骤相同 - 部分重同步(
partial resynchronization
):用于处理断线后重复制情况,当从机在断线后重新连接主机时,若条件允许,主机可以将主从机连接断开期间执行的写命令发送给从机,从机只要接受并执行这些写命令,就可以将数据库更新至主机当前所处的状态。
由于执行部分重同步所需的资源比执行 sync
命令所需的资源少很多,速度也更快,不再需要重新生成、传送和载入整个 RDB 文件,只需将从机缺少的写命令发送给从机执行即可。
这样,通过psync
命令的部分重同步,就解决了旧版复制功能处理断线后重复制的低效率情况。
部分重同步具体实现
部分重同步功能主要通过新增以下属性作用实现:
- 服务器的运行 ID (
run ID
) - 主机的复制偏移量(
replication offset
)和从机的复制偏移量 - 主机的复制积压缓冲区(
replication backlog
)
服务器运行 ID
部分重同步会借助一个属性——服务器运行 ID (run ID
)。
这个属性是什么呢?
每个 Redis 服务器,不论主还是从机,都有自己的运行 ID,其在服务器启动时自动生成,由 40 个随机的十六进制字符组成,如前面 master_replid
属性中的737e987880d95facb14d0d614b0a25d6871345d1
当从机对主机进行初次复制时,主机会将自己的运行 ID 传送给从机,从机则将这个 ID 保存。
当从机断线并重新连接上一个主机时,从机将向当前连接的主机发送之前保存的运行 ID:
- 保存 ID 和当前主机运行 ID 相同,说明从机断线之前复制的就是这个主机,主机可以继续尝试执行部分重同步操作;
- 相反的,若 2 个服务器 ID 不同,说明从机断线之前复制的不是这个主机,主机将对从机执行完整重同步操作
复制偏移量
执行复制的双方——主机和从机分别维护一个复制偏移量:
- 主机每次向从机传播 N 个字节的数据时,就将自己的复制偏移量的值加上 N
- 从机每次收到主机传来的 N 个字节的数据时,就将自己的复制偏移量加上 N
通过对比主从机的复制偏移量,程序就很容易地知道主从机是否处于一致状态:
- 若主从机处于一致状态,那么主从机两者的偏移量总是相同的;
- 相反,若主从机两者的偏移量并不相同,那么说明主从机并未处于一致状态
我们可以在服务器通过info replication
命令查看这个复制偏移量,前面使用过该命令,可以其信息看到这么一条:
1 | master_repl_offset:14 |
复制积压缓冲区
复制积压缓冲区是由主机维护的一个固定长度(fixed-size
)先进先出(FIFO
)队列,默认大小为 1MB
当主机进行命令传播时,它不仅会将写命令发送给所有从机,还会将写命令入队到复制积压缓冲区队列里面,如下图:
因此,主机的复制积压缓冲区里面会保存这一部分最近传播的写命令,并且它会为队列中的每个字节记录相应的复制偏移量,如下表:
当从机重新连上主机时,从机会通过命令将自己的复制偏移量发送给主机,主机会根据这个复制偏移量来决定对从机执行何种同步操作:
- 若
offset
偏移量之后的数据(即偏移量 offset+1开始的数据)仍然存在于复制积压缓冲区中,则主机将对从机执行部分重同步操作 - 相反,若
offset
偏移量之后的数据已经不在复制积压缓冲区,那么主机将对从机执行完整重同步操作
小提示
:Redis 的默认复制积压缓冲区为 1MB ,可以根据需要调整相应的复制积压缓冲区大小。
扩展——心跳检测
在命令传播阶段,从机会以每秒一次的频率,向服务器发送命令:
1 | replconf ack <replication_offset> // replication_offset 代表从机当前的复制偏移量 |
发送replconf ack
命令对于主从机有以下三个作用:
- 检测命令丢失
- 检测主从机的网络连接状态
- 辅助实现
min-slaves
选项
检测命令丢失
若因为网络故障,导致主机传播给从机的写命令在半路丢失,那么当从机向主机发送replconf ack
命令时,主机将发觉从机当前的复制偏移量少于自己的复制偏移量,然后主机就会根据从机提交的复制偏移量,在复制积压缓冲区里找到从机缺少的数据,并将这些数据重新发送给从机。
检测主从机的网络连接状态
主从机可以通过发送和接受replconf ack
命令来检查两者之间的网络连接是否正常:若主机超过一秒钟没有收到从机的发送的该命令,则说明主从连接出现问题了。
我们可以通过info replication
命令查看主机的lag
属性显示从机最后一次向主机发送replconf ack
命令距现在过了多少秒:
1 | // 1 秒之前发送过 replconf ack 命令 |
一般情况下,lag
的值应该在 0 秒或者 1 秒之间跳动,若超过 1 秒则说明主从机之间连接出现了故障。
辅助实现 min-slaves 选项
Redis 的min-slaves-to-write
和min-slaves-max-lag
两个选项可以防止服务器在不安全的情况下执行写操作。
比如,若我们向主机配置文件加入下面配置:
1 | min-slaves-to-write 3 |
那么在从机的数量少于 3 个,或者 3 个从机的延迟值(info replication
命令的lag
值)值都大于或等于 10 秒时,主机将拒绝执行写命令。
疑问清单
本文虽然实现了主从模式的 Redis,但我们肯定会有一些疑问。
① 新的从机如何复制主机数据?
Question
:若主机已有大量数据,此时新加入的从机是从切入时开始复制数据还是从头复制主机所有数据?
Answer
:从头复制所有数据。
Action
:由于前面我们向主机写了几条命令,现在让192.168.1.13:6379
的 Redis 服务器成为192.168.1.8 6379
主机的从机,再查看相关数据即可:
1 | # 192.168.1.13:6379 机器 |
Result
:结论很明显,数据都复制到192.168.1.13:6379
机器当中了。
② 从机是否可写?
Question
:从机是否可写,即能否执行 set 等写操作相关的命令?
Answer
:从机只可执行读命令,无法写入数据。
Action
:下面随便找个从机实验来看看:1
2
3# 192.168.1.13:6379 机器
127.0.0.1:6379> set msg "hello world"
(error) READONLY You can't write against a read only replica.
Result
:很显然,对从机进行数据的写入会报错,根据无法写入。
③ 从机是否可以拥有从机?
Question
:从机是否可以拥有从机?
Answer
:可以的,这么做可以分担压力,提升整体服务器性能。
Action
:现在有一个主机(192.168.1.8:6379
)及它的两个从机(192.168.1.12:6379
、192.168.1.13:6379
),那么让192.168.1.13:6379
变为192.168.1.12:6379
的从机,看看会发生什么。
首先执行命令使192.168.1.13:6379
变为192.168.1.12:6379
的从机:1
2# 192.168.1.13:6379 机器
127.0.0.1:6379> slaveof 192.168.1.12 6379
其次在192.168.1.12:6379
查看现在的信息:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25# 192.168.1.12:6379 机器
127.0.0.1:6379> info replication
# Replication
role:slave
master_host:192.168.1.8
master_port:6379
master_link_status:up
master_last_io_seconds_ago:2
master_sync_in_progress:0
slave_read_repl_offset:1163
slave_repl_offset:1163
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:1
slave0:ip=192.168.1.13,port=6379,state=online,offset=1163,lag=1
master_failover_state:no-failover
master_replid:abadda1edad8e5a7f6d1156916af3e8b30ff95ea
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1163
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:1163
Result
:192.168.1.12:6379
现在是 192.168.1.8:6379
的从机,还是192.168.1.12:6379
的主机。
④ 当主机意外宕机后,从机会如何变化?
Question
:当主机意外宕机后,从机是上位变为主机还是原地待命?
Answer
:无特殊配置的话,则原地待命。
Action
:比如现在有一个主机(192.168.1.8:6379
)及它的两个从机( 192.168.1.12:6379
、192.168.1.13:6379
),现在让192.168.1.8:6379
执行shutdown
命令模拟宕机场景:
1 | # 192.168.1.8:6379 机器 |
然后在从机192.168.1.12:6379
查看状态:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29# 192.168.1.12:6379 机器
127.0.0.1:6379> info replication
# Replication
role:slave
master_host:192.168.1.8
master_port:6379
master_link_status:down // 主机挂了
master_last_io_seconds_ago:-1
master_sync_in_progress:0
slave_read_repl_offset:1555
slave_repl_offset:1555
master_link_down_since_seconds:25
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:abadda1edad8e5a7f6d1156916af3e8b30ff95ea
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1555
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:1555
# 此时当然是无法执行写命令的
127.0.0.1:6379> set a 1
(error) READONLY You can't write against a read only replica.
当主机重启完成,从机将重新连接主机:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24# 192.168.1.12:6379 机器
127.0.0.1:6379> info replication
128.# Replication
role:slave
master_host:192.168.1.8
master_port:6379
master_link_status:up // 主机重新连接成功
master_last_io_seconds_ago:3
master_sync_in_progress:0
slave_read_repl_offset:0
slave_repl_offset:0
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:a7399ed63c77fe6ae5e44f4f14dd612bd6984d69
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:0
Result
:从机原地待命。
当然这种情况下,我们也可以通过slaveof no one
命令手动使从机变为主机。
主机宕机后,在从机192.168.1.12:6379
执行slaveof no one
命令:
1 | # 192.168.1.12:6379 机器 |
Result
:可以发现从机192.168.1.12:6379
又变为主机了。
⑤ 主机宕机重启新增的记录,从机能否复制?
Question
:主机宕机重启后回来新增的记录,从机能否顺利复制?
Answer
:可以,主机和从机会对比偏移量来进行复制操作。
⑥ 从机宕机后如何变化?
Question
:从机宕机后恢复能跟上主机吗?
Answer
:从机恢复后就不在是从机了,默认情况下它会再次变为孤独的主机(当然我们可以在配置文件设置它启动即为某主机的从机),因此答案是和配置文件有关,加了信息则能跟上原主机。
Action
:我们将其中一个从机shutdown
后在查看它的相关信息:
1 | # 192.168.1.13:6379 机器 |
1 | # 重启 |
1 | redis-cli -p 6379 |
显而易见,192.168.1.13:6379
重新变为主机了。
当然,我们可以在其配置文件(master_slave.conf
)进行配置指定其永远为某服务器的从机:
1 | slaveof 192.168.1.8 6379 |
修改完成后,我们关闭当前实例并进行重新启动:
1 | redis-server /data/software/redis/conf/master_slave.conf |
Result
:192.168.1.13:6379
又变为从机了,而且数据也复制过来了。
参考
- 黄健宏 Redis 设计与实现 [M]. 机械工业出版社,2014
文章信息
时间 | 说明 |
---|---|
2019-04-14 | 初稿 |
2022-12-26 | 部分重构 |