什么是Redis的主从复制
在Redis中,用户通过执行slaveof
命令或者设置配置文件slaveof
选项的方式,让一个服务器(从服务器)去复制(replicate
)另一个服务器(主服务器),这个复制过程就叫做主从复制,如下图所示:
进行复制中的主从服务器双方的数据库将保存相同的数据,我们将这种现象称作数据库状态一致或一致。
一个主服务器可以拥有多个从服务器(从服务器也可以有从服务器),如下图:
为什么使用主从复制
主从复制可以实现读写分离功能,即让主服务器专心处理客户端传来的写操作(当然主服务器是可以进行读操作的),而只让从服务器处理客户端的读操作,不同服务器各司其职,可以达到性能扩展的效果。而且有数据备份功能,当主服务器意外宕机的话,从服务器也可以“上位”,变成主服务器,使容灾快速恢复。还有哨兵机制也在此功能上实现。
主从复制中服务器的配置
要想使用主从复制功能,需要使用多个Redis服务器,(因为笔者只有一台电脑)所以需要多创建几个新的Redis配置文件。这几个配置文件使用可以使用include
命令(可以根据默认服务器配置文件路径加载其内容),并可以设置新属性以覆盖默认文件的旧属性,下面为具体步骤:
- 创建服务器配置文件:
//创建存放主从服务器配置文件的文件夹
$ mkdir master-slave
$ cd master-slave/
//创建服务器配置文件
$ touch redis6380.conf
//创建服务器配置文件
$ touch redis6381.conf
//创建服务器配置文件
$ touch redis6382.conf
然后在这3个配置文件都追加include /usr/local/etc/redis.conf
内容以加载默认配置文件,更改daemonize
选项为yes
(默认配置文件若为yes
则不必更改),之后将配置文件的以下属性修改(机器多不用改,笔者只有1台电脑):
- 将
pidfile
文件名更改 - 端口名更改
- RDB文件名更改
- AOF选项关闭(开启也行,文件名也得改)
- 日志文件名更改(不过Redis一般不存储日志,所以了解即可)
下面为修改后的redis6380.conf
文件内容,另外2个文件在此基础上稍作修改即可:
include /usr/local/etc/redis.conf
port 6380
pidfile /var/run/redis_6380.pid
dbfilename dump6380.rdb
主从服务器相关命令
配置完相关文件后,我们分别以不同的配置文件启动这3个Redis服务器:
$ redis-server /usr/local/etc/master-slave/redis6380.conf
18784:C 09 Apr 2019 18:25:42.820 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
18784:C 09 Apr 2019 18:25:42.820 # Redis version=5.0.4, bits=64, commit=00000000, modified=0, pid=18784, just started
18784:C 09 Apr 2019 18:25:42.820 # Configuration loaded
$ redis-server /usr/local/etc/master-slave/redis6381.conf
18792:C 09 Apr 2019 18:25:52.072 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
18792:C 09 Apr 2019 18:25:52.072 # Redis version=5.0.4, bits=64, commit=00000000, modified=0, pid=18792, just started
18792:C 09 Apr 2019 18:25:52.072 # Configuration loaded
$ redis-server /usr/local/etc/master-slave/redis6382.conf
18800:C 09 Apr 2019 18:25:55.279 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
18800:C 09 Apr 2019 18:25:55.280 # Redis version=5.0.4, bits=64, commit=00000000, modified=0, pid=18800, just started
18800:C 09 Apr 2019 18:25:55.280 # Configuration loaded
之后我们开启3个终端来连接3个服务器,使用redis-cli -p [port]
命令连接指定端口:
//第一个终端
$ redis-cli -p 6380
127.0.0.1:6380>
//第二个终端
$ redis-cli -p 6381
127.0.0.1:6381>
//第三个终端
$ redis-cli -p 6382
127.0.0.1:6382>
我们可以通过info replication
命令查看当前服务器的主从信息,详解见注释
127.0.0.1:6380> info replication
# Replication
role:master //当前服务器是主机还是从机,主机显示master,从机显示slave
connected_slaves:0 //连接的从机数
master_replid:737e987880d95facb14d0d614b0a25d6871345d1 //服务器唯一id
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0 //复制偏移量
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
开启的3个服务器都是主机,所以它们的信息和上面的基本相同,不过我们可以使用slaveof [ip] [port]
命令指定当前服务器成为指定IP + 端口号的主机的从机,我们指定127.0.0.1:6382
服务器成为127.0.0.1:6380
服务器的从机:
127.0.0.1:6382> slaveof 127.0.0.1 6380
OK
127.0.0.1:6382> info replication
# Replication
role:slave //从机
master_host:127.0.0.1 //主机IP
master_port:6380 //主机端口
master_link_status:up //连接状态
master_last_io_seconds_ago:0
master_sync_in_progress:0
slave_repl_offset:14
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:6e24c2764430ed31bd8902907302ee962479e634 //服务器唯一ID
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:14 //复制偏移量
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:14
可以看到6382端口的服务器信息发生了变化,它成为了从机(具体信息见注释),让我们在看看它连接的主机信息:
127.0.0.1:6380> info replication
# Replication
role:master
connected_slaves:1 //连接从机数变为1
slave0:ip=127.0.0.1,port=6382,state=online,offset=14,lag=1 //从机IP,端口,连接状态等等信息
master_replid:6e24c2764430ed31bd8902907302ee962479e634
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:14
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:14
很显然,主机信息也发生了变化,并且它们的偏移量(后面介绍)基本保持相同。
最开始提到过主机和从机的状态总保持一致,即数据总保持一直,所以我们写点数据进主服务器看看:
127.0.0.1:6380> set age 16
OK
127.0.0.1:6380> set user tom
OK
127.0.0.1:6380> keys *
1) "user"
2) "age"
我们写了2个键进主服务器,那么从服务器应该也有这两个键,让我们查看一下:
127.0.0.1:6382> keys *
1) "age"
2) "user"
结果符合预期,可是这是怎么做到的呢?后文复制原理介绍。
主从服务器疑惑
①切入点问题,若主机已有大量数据,此时新加入的从机是从切入时开始复制数据还是从头复制主机所有数据?
答:
从头复制所有数据。
前面我们向主机写了几条命令,现在让6381端口的Redis服务器成为原先主机的从机机,之后查看相关数据即可:
127.0.0.1:6381> slaveof 127.0.0.1 6380
OK
127.0.0.1:6381> keys *
1) "age"
2) "user"
127.0.0.1:6381>
很显然,数据复制过来了。
②从机是否可以写,能执行set等写操作相关的命令吗?
答:
从机只可执行读命令,无法写入数据。
找个从机实验即可:
127.0.0.1:6382> set msg "hello world"
(error) READONLY You can't write against a read only replica.
很显然,在从机写入数据会报错,无法写入。
③从机是否可以拥有从机?有什么好处?
答:
可以,可以分担压力,提升整体服务器性能。
比如上面有一个主机(6380)及它的两个从机(6381、6382),现在让6382变为6381的从机:
slaveof 127.0.0.1 6381
就会发现6381虽然是6380的从机,但它还是6382的主机:
127.0.0.1:6381> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6380
master_link_status:up
master_last_io_seconds_ago:6
master_sync_in_progress:0
slave_repl_offset:406
slave_priority:100
slave_read_only:1
connected_slaves:1
slave0:ip=127.0.0.1,port=6382,state=online,offset=406,lag=0
master_replid:3d3954a80564580ff2e57f42a45c3188b5d1e249
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:406
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:406
④主机意外宕机后从机是上位变为主机还是原地待命?
答:
先原地待命,若有哨兵(第六章笔记再讲解
)监控则根据规则超出一定时间后(根据规则挑选一个从机)变为主机(当然也可以通过slaveof no one
命令使从机变为主机,其他从机会自动跟随上来),而原主机变为其从机(只在有哨兵下生效)。
比如现在有一个主机(6380)及它的两个从机(6381、6382),现在让6380执行shutdown
命令模拟宕机场景:
127.0.0.1:6380> shutdown
然后在从机6381执行slaveof no one
命令:
127.0.0.1:6381> slaveof no one
OK
127.0.0.1:6381> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6382,state=online,offset=756,lag=1
master_replid:0750d1ac57982d7280c4b586dfc5288b816d2942
master_replid2:3d3954a80564580ff2e57f42a45c3188b5d1e249
master_repl_offset:756
second_repl_offset:743
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:756
可以发现从机6381变为主机了,6382变成了它的从机,我们再重新开启6380Redis服务器看下:
127.0.0.1:6380> info replication
# Replication
role:master
connected_slaves:0
master_replid:f3ba631691f2334d720043761f92a9a9b24e2f76
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
可以发现它没有从机了,也不是任何主机的从机。若是被哨兵监控的话,会自动变为它宕机后上位从机(现在是主机)的从机。
⑤主机宕机后回来新增的记录,从机能否顺利复制?
答:
可以。主机从机会对比偏移量
⑥从机宕机后恢复能跟上主机吗?
答:
从机恢复后就不在是从机了,它又变为了主机(不过我们可以在配置文件设置它启动即为某主机的从机),所以说和配置文件有关,加了信息则能跟上原主机。
我们将其中一个从机shutdown
后在查看它的相关信息:
127.0.0.1:6381> shutdown
not connected> exit
$ redis-server /usr/local/etc/master-slave/redis6381.conf
19109:C 09 Apr 2019 19:38:40.739 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
19109:C 09 Apr 2019 19:38:40.739 # Redis version=5.0.4, bits=64, commit=00000000, modified=0, pid=19109, just started
19109:C 09 Apr 2019 19:38:40.739 # Configuration loaded
$ redis-cli -p 6381
127.0.0.1:6381> info replication
# Replication
role:master
connected_slaves:0
master_replid:ca84f409a9410de33831d815ad3ae691b7b9c296
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
127.0.0.1:6381>
很显然,它重新变为主机了,不过我们若在其配置文件(redis6381.conf
)加这句代码即可指定其永远为某服务器从机:
slaveof 127.0.0.1 6380
我们关闭重新启动一下:
$ redis-server /usr/local/etc/master-slave/redis6381.conf
19198:C 09 Apr 2019 19:42:25.993 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
19198:C 09 Apr 2019 19:42:25.993 # Redis version=5.0.4, bits=64, commit=00000000, modified=0, pid=19198, just started
19198:C 09 Apr 2019 19:42:25.993 # Configuration loaded
$ redis-cli -p 6381
127.0.0.1:6381> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6380
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_repl_offset:4607
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:6e24c2764430ed31bd8902907302ee962479e634
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:4607
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:4594
repl_backlog_histlen:14
127.0.0.1:6381> keys *
1) "age"
2) "user"
又变为从机了,而且数据也复制过来了。
主从复制原理
前面讲了那么多,只知道从服务器能复制主服务器,却不知道why
,下面将浅谈一下。
主从复制由低版本的旧版复制过渡到如今高版本的新版复制,新版复制在旧版复制功能上加强,下面分别介绍:
旧版复制
Redis的复制分为同步(sync
)和命令传播(command propagata
)两个操作:
- 同步:用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态
- 命令传播:用于在主服务器的数据库状态被修改,导致主从服务器的数据库状态出现不一致时,让主从服务器的数据库重新回到一直状态
同步
当客户端在从服务器发送slaveof [ip] [port]
命令,要求从服务器复制主服务器时,从服务器首先需要执行同步操作,该操作需要通过向主服务器发送sync
命令来完成,下面为具体步骤:
1)从服务器向主服务器发送sync
命令;
2)收到sync
命令的主服务器执行bgsave
命令,在后台生成一个RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令;
3)当主服务器的bgsave
命令执行完毕时,会将该命令生成的RDB文件发送给从服务器,从服务器接受并载入这个文件,更新自己的数据库状态至主服务器执行bgsave
命令时的数据库状态;
4)主服务器将记录在缓冲区里的所有写命令发送给从服务器,从服务器执行这些写命令,将自己的数据库状态更新至主服务器数据库当前所处状态。
命令传播
同步完成之后,主从服务器的数据库状态将保持一致,但这种状态并非不变的,每当主服务器执行客户端发送的写命令时,主服务器的数据库就可能被更改,导致主从服务器状态不再一致。
举个例子,当主从服务器刚刚完成同步操作,它们数据库都保存了3个相同的键k1
、k2
、k3
。如果这时,客户端向主服务器发送命令del k2
,那么主服务器执行完该命令后主从服务器数据库将不再一致:主服务器数据库已删除了键k2
,而从服务器还有该键。
为了让主从服务器再回到一致状态,主服务器需要对从服务器执行命令传播操作:主服务器会将自己执行的造成主从服务器不一致的那条写命令,发送给从服务器执行,之后主从服务器再次回到一致状态。即例子中主服务器删除k2
命令后将其发送给从服务执行,之后主从服务器都只剩2个键k1``k3
,状态达成一致。
旧版缺陷
Redis中主从复制可以分为下面2种情况:
- 初次复制:从服务器从来没复制过任何主服务器,或者从服务器当前复制的主服务器和上次复制的主服务器不同。
- 断线后重复制:处于命令传播阶段的主从服务器因为网络原因而中断了复制,但从服务器通过自动重连重新连接了主服务器,并继续复制主服务器。
需要注意的是:
断线后重复制效率十分低下,因为它重连后是重新复制整个主服务器,而不是从断线后的状态接着复制。类似于我们平常游戏刷副本,刷到一半掉线,你再次上线却回到了出发点,需要重新刷,耗时耗力。对于主从服务器而言,断线重连就需要从服务器重新发送sync
同步命令,而该命令是十分耗费资源的(主服务器重新生成RDB文件,占用其CPU、内存、磁盘等)。
那这样肯定不行的啊,我刷副本刷一半掉线立马重连你告诉我要重新刷,毫无用户体验啊!对主从复制也是一样,所以引入了新版复制功能来解决这个问题。
新版复制
为了解决上面的问题,2.8版本后的Redis服务器开始使用psync
命令代替sync
命令来执行复制时的同步操作,该命令具有完整重同步(full resynchronization
)和部分重同步(partial resynchronization
):
-
完整重同步:用于处理初次复制情况,执行步骤基本和旧版
sync
命令同步的执行步骤一样。 -
部分重同步:用于处理断线后重复制情况,当从服务器在断线后重新连接主服务器时,如果条件允许,主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接受并执行这些写命令,就可以将数据库更新至主服务器当前所处的状态。
psync
命令的部分重同步解决了旧版复制功能处理断线后重复制的低效率情况,类似于游戏断线重连还在副本中。这样执行部分重同步所需的资源比执行sync
命令所需的资源少很多,速度也更快,不再需要重新生成、传送和载入整个RDB文件,只需将从服务器缺少的写命令发送给从服务器执行即可,如下图:
部分重同步具体实现
部分重同步功能主要由以下3个部分构成:
- 主服务器的复制偏移量(
replication offset
)和从服务器的复制偏移量 - 主服务器的复制积压缓冲区(
replication backlog
) - 服务器的运行ID(
run ID
)
复制偏移量
执行复制的双方--主服务器和从服务器分别维护一个复制偏移量:
- 主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量的值加上N;
- 从服务器每次收到主服务器传来的N个字节的数据时,就将自己的复制偏移量加上N
通过对比主从服务器的复制偏移量,程序就很容易地知道主从服务器是否处于一致状态:
- 如果主从服务器处于一致状态,那么主从服务器两者的偏移量总是相同的;
- 相反,如果主从服务器两者的偏移量并不相同,那么说明主从服务器并未处于一致状态。
我们可以在服务器通过info replication
命令查看这个复制偏移量,前面使用过该命令,可以其信息看到这么一条:
master_repl_offset:14
复制积压缓冲区
复制积压缓冲区是由主服务器维护的一个固定长度(fixed-size
)先进先出(FIFO
)队列,默认大小为1MB。
当主服务器进行命令传播时,它不仅会将写命令发送给所有从服务器,还会将写命令入队到复制积压缓冲区队列里面,如下图:
因此,主服务器的复制积压缓冲区里面会保存这一部分最近传播的写命令,并且它会为队列中的每个字节记录相应的复制偏移量,如下表:
当从服务器重新连上主服务器时,从服务器会通过
psync
命令将自己的复制偏移量offset
发送给主服务器,主服务器会根据这个复制偏移量来决定对从服务器执行何种同步操作:
- 如果
offset
偏移量之后的数据(即偏移量offset+1开始的数据)仍然存在于复制积压缓冲区里面,则主服务器将对从服务器执行部分重同步操作 - 相反,如果
offset
偏移量之后的数据已经不再复制积压缓冲区,那么主服务器将对从服务器执行完整重同步操作。
注意:
Redis的默认复制积压缓冲区为1MB,可以根据需要调整相应的复制积压缓冲区大小。
服务器运行ID
实现部分重同步还需要服务器运行ID(run ID
):每个Redis服务器,不论主还是从服务器,都有自己的运行ID,其在服务器启动时自动生成,由40个随机的十六进制字符组成,如前面master_replid
属性中的:737e987880d95facb14d0d614b0a25d6871345d1
当从服务器对主服务器进行初次复制时,主服务器会将自己的运行ID传送给从服务器,从服务器则将这个ID保存。
当从服务器断线并重新连接上一个主服务器时,从服务器将向当前连接的主服务器发送之前保存的运行ID:
- 保存ID和当前主服务器运行ID相同,说明从服务器断线之前复制的就是这个主服务器,主服务器可以继续尝试执行部分重同步操作;
- 相反的,如果2个服务器ID不同,说明从服务器断线之前复制的不是这个主服务器,主服务器将对从服务器执行完整重同步操作。
补充:心跳检测
在命令传播阶段,从服务器会以每秒一次的频率,向服务器发送命令:
replconf ack <replication_offset> //replication_offset代表从服务器当前的复制偏移量
发送replconf ack
命令对于主从服务器有3个作用,等等分别介绍:
- 检测主从服务器的网络连接状态
- 辅助实现
min-slaves
选项 - 检测命令丢失
检测主从服务器的网络连接状态
主从服务器可以通过发送和接受replconf ack
命令来检查两者之间的网络连接是否正常:如果主服务器超过一秒钟没有收到从服务器的发送的该命令,则说明主从连接出现问题了。
我们可以通过info replication
命令查看主服务器的lag
属性显示从服务器最后一次向主服务器发送replconf ack
命令距现在过了多少秒:
slave0:ip=127.0.0.1,port=6382,state=online,offset=14,lag=1 //1秒之前发送过`replconf ack`命令
一般情况下,lag
的值应该在0秒或者1秒之间跳动,如果超过1秒则说明主从服务器之间连接出现了故障。
辅助实现min-slaves
选项
Redis的min-slaves-to-write
和min-slaves-max-lag
两个选项可以防止服务器在不安全的情况下执行写操作。
比如,若我们向主服务器配置文件加入下面配置:
min-slaves-to-write 3
min-slaves-max-lag 10
那么在从服务器的数量少于3个,或者3个从服务器的延迟值(lag
)值都大雨或等于10秒时,主服务器将拒绝执行写命令,延迟值即info replication
命令的lag
值。
检测命令丢失
如果因为网络故障,主服务器传播给从服务器的写命令在半路丢失,那么当从服务器向主服务器发送replconf ack
命令时,主服务器将发觉从服务器当前的复制偏移量少于自己的复制偏移量,然后主服务器就会根据从服务器提交的复制偏移量,在复制积压缓冲区里找到从服务器缺少的数据,并将这些数据重新发送给从服务器。
主从复制实现的具体步骤
当你在从服务器开始使用slaveof
命令,从服务器内部将按下面的步骤逐步进行,详细过程省略:
①设置主服务器的地址和端口;
②建立与主服务器的套接字连接;
③向主服务器发送PING命令;
④进行身份验证;
⑤发送端口信息;
⑥同步主服务器数据;
⑦进入命令传播阶段。
参考资料
《redis设计与实现》(第二版)