分布式系统中存在一个非常关键的问题 – 单点问题;单点问题指服务器程序只有一个节点,即只使用一个物理服务器来部署服务。这会导致一些问题:
为了解决单点问题,通常会把数据复制多个副本部署到其他服务器,以满足故障恢复和负载均衡等需求。其中,Redis 存在以下三种部署方式:主从模式、主从 + 哨兵模式 以及 集群模式。本节我们来学习 Redis 中主从模式的实现方式及其原理。
主从模式是指将 Redis 服务部署到多个物理服务器上,然后让其中的一个节点作为 “主节点”,让其他节点作为 “从节点”。其中主节点与从节点的关系如下:
通过主从复制的方式我们能够有效的解决单点问题:
要配置 Redis 主从结构,首先需要启动多个 Redis 服务,且这些 Redis 服务需要部署在不同服务器上,这样才能起到分布式的效果。但由于本人只有一台云服务器,因此在一台服务器上启动多个 redis-server 进程来模拟。
如下,服务器上启动了三个 redis-server:
[root@VM-8-13-centos redis]# pwd
/etc/redis
[root@VM-8-13-centos redis]# ll *.conf
-rw-r----- 1 redis root 61888 Aug 25 14:19 redis.conf
-rw-r----- 1 redis root 9837 Oct 25 2021 redis-sentinel.conf
-rw-r----- 1 root root 61888 Sep 1 14:42 redis_slave1.conf
-rw-r----- 1 root root 61888 Sep 1 14:43 redis_slave2.conf
[root@VM-8-13-centos redis]# redis-server redis.conf --port 6379
[root@VM-8-13-centos redis]# redis-server redis_slave1.conf --port 6380
[root@VM-8-13-centos redis]# redis-server redis_slave2.conf --port 6381
[root@VM-8-13-centos redis]# ps axj | grep redis
1 6022 6022 6022 ? -1 Ssl 0 0:00 redis-server 0.0.0.0:6379
1 7521 7521 7521 ? -1 Ssl 0 0:00 redis-server 0.0.0.0:6380
1 7585 7585 7585 ? -1 Ssl 0 0:00 redis-server 0.0.0.0:6381
3429 7637 7636 3240 pts/0 7636 S+ 0 0:00 grep --color=auto redis
目前这几个节点并没有构成主从结构,而是三个独立的节点,而配置主从结构的方法有三种:
slaveof {masterHost} {masterPort}
,此时配置随 Redis 启动生效。--slaveof {masterHost} {masterPort}
生效。slaveof {masterHost} {masterPort}
生效。其中,后面两种方法都是临时配置,即 redis-server 重启后需要重新执行 slaveof
命令,而第一种方式是永久生效的,不过修改配置文件后需要重启 redis-server 才能让配置生效。
下面我们演示第一种方法:
首先,修改配置文件 redis_slave1.conf
与 redis_slave2.conf
,追加 salveof
配置。
# 主从复制
slaveof 127.0.0.1 6379
然后,重新启动端口号为 6380 与 6381 的节点,观察其与 6379 节点的关系:
如上,Redis 子节点与从节点之间会建立两个 TCP 连接,一个用于接收主节点命令的命令连接(主节点将所有的写命令发送到这个连接,从节点接收这些命令并执行,以此来保证数据的一致性),一个用于支持发布/订阅功能的订阅连接。
此时,我们在主节点上进行的任何数据修改操作,从节点都会同步,且从节点不允许修改:
127.0.0.1:6380> set k1 111
(error) READONLY You can't write against a read only replica.
127.0.0.1:6380>
同时,我们可以通过 info replication
命令查看主节点与从节点的复制信息:
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=2954,lag=0
slave1:ip=127.0.0.1,port=6381,state=online,offset=2968,lag=0
master_replid:12acce366cb00f35ebb02447c42ce61af38b3528
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:2968
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:2968
127.0.0.1:6379>
127.0.0.1:6380> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:8
master_sync_in_progress:0
slave_repl_offset:2982
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:12acce366cb00f35ebb02447c42ce61af38b3528
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:2982
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:2982
127.0.0.1:6380>
对于已经建立好的主从复制关系,我们可以使用 slave no one
将其断开,此命令由从节点执行,执行后从节点将变成一个独立的节点,且无法再同步主节点修改的数据,但从节点中原有的数据并不会丢弃。
需要注意的是:在从节点中执行 slave no one
命令是临时性的,即从节点重启后仍然会复制主节点;想要永久生效需要删除或修改配置文件中的 slaveof
配置项。
另外,slave no one
命令配合 slaveof
命令使用可以实现切主效果,即将当前从节点的数据源切换到另一个主节点。切主操作的主要流程如下:
主从复制还有一些其他相关的特性,如下:
requirepass
参数进行密码验证,这时所有的客户端访问必须使用 auth
命令实行校验。从节点与主节点的复制连接是通过一个特殊标识的客户端来完成,因此需要配置从节点的 masterauth
参数与主节点密码保持一致,这样从节点才可以正确地连接到主节点并发起复制流程。
slave-read-only yes
配置为只读模式。由于复制只能从主节点到从节点,对于从节点的任何修改主节点都无法感知,因此修改从节点会造成主从数据不一致,所以不建议修改从节点的只读模式。
repl-disable-tcp-nodelay
参数用于控制是否关闭 TCP_NODELAY
,默认为 no
,即开启 tcp-nodelay
功能,说明如下 (TCP 内部的 nagle 算法):
为 no 即不关闭 TCP_NODELAY
时,主节点产生的命令数据无论大小都会及时地发送给从节点,这样主从之间延迟会变小,但增加了网络带宽的消耗。适用于主从之间的网络环境良好的场景,如 同机房部署。
为 yes 即关闭 TCP_NODELAY
时,主节点会合并较小的 TCP 数据包从而节省带宽。默认发送时间间隔取决于 Linux 的内核,一般默认为 40 毫秒。这种配置节省了带宽但增大主从之间的延迟。适用于主从网络环境复杂的场景,如跨机房部署。
Redis 拓扑结构指 Redis 主从节点之间按照怎样的方式来组织连接。Redis 的复制拓扑结构可以支持单层或多层复制关系,根据拓扑复杂性可以分为以下三种:一主一从、一主多从、树状主从结构。
一主一从结构
一主一从结构是最简单的复制拓扑结构,用于主节点出现宕机时从节点提供故障转移支持。
如下图所示,当应用写命令并发量较高且需要持久化时,可以只在从节点上开启 AOF,这样保证数据安全性的同时也避免了持久化对主节点的性能干扰。但这种设定有一个严重缺陷,即主节点宕机后不能让其自动重启,而是需要先从从节点获取 AOF 文件,然后使用该 AOF 文件进行重启与数据恢复,否则会导致数据丢失。
一主多从结构
一主多从结构使得应用端可以利用多个从节点实现读写分离,从而提高并发量。(实际开发中,一般读请求都要远多于写请求)
如下图所示,对于读比重较大的场景,可以把读命令负载均衡到不同的从节点上来分担压力,同时一些耗时的读命令可以指定一台专门的从节点执行,避免破坏整体的稳定性。但缺点在于,对于写并发量较高的场景,多个从节点 会导致主节点写命令的多次发送 从而加重主节点的负载。
树状主从结构
树形主从结构使得从节点不但可以复制主节点数据,同时可以作为其他从节点的主机节点 (主机节点而不是主节点,其他从节点的主节点仍然是根节点) 继续向下层复制。通过引入复制中间层,可以有效降低主节点 “写” 负载和需要传送给从节点的数据量。
如下图所示,数据写入节点 A 之后会同步给 B 和 C 节点,B 节点进一步把数据同步给 D 和 E 节点。当主节点需要挂载多个从节点时为了避免对主节点的性能干扰,可以采用这种拓扑结构。但它的缺点在于,由于主节点数据需要逐层向下同步,因此同步的延时比较高。
Redis 复制建立以及运行的流程如下:
ping
,主节点返回 pong
。(在应用层面上验证主节点能够正常工作)requirepass
参数,需要进行密码验证。Redis 使用 psync
命令完成主从数据同步,同步分为全量复制和部分复制:
从节点在与主节点建立好主从关系后会自动执行 psync 进行数据同步,不需要我们手动执行。
psync
命令的语法如下:
PSYNC replicationid offset
info replication
中的 master_repl_offset
指标中;从节点也会每秒钟上报自身的复制偏移量给主节点 (同步进度),并保存在 slave 行的 offset
字段中。?
,offset 的值为 -1
,此时表示进行全量复制。重连时,如果 replid 与 offset 为某个具体的值,则表示进行部分复制。我们可以通过 info replication
命令查看主从节点相关的复制信息:
主节点:
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6380,state=online,offset=114282,lag=1
slave1:ip=127.0.0.1,port=6381,state=online,offset=114282,lag=1
master_replid:12acce366cb00f35ebb02447c42ce61af38b3528
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:114282
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:114282
如上,主节点修改命令字节数 master_repl_offset
为 114282,从节点 slave0 和 slave1 的同步进度 offset 都为 114282,表示数据全部都已经同步了。
关于 master_replid
和 master_replid2
:
从
info replication
的输出可以看到,每个节点都记录了两组master_replid
,这是为了预防网络抖动。考虑下面的场景:
master_replid
。
master_replid
。此时就会使用 master_replid2
来保存之前 A 的 master_replid
。
master_replid2
找回之前的主节点,然后进行数据的部分同步。续如果网络没有恢复,B 就按照新的 master_replid
自成一派,继续处理后续的数据。
psync
命令的运行流程如下:
PSYNC
命令给主节点,且replid
和 offset
的默认值分别是 ?
和 -1
。PSYNC
参数和自身数据情况决定响应结果: +FULLRESYNC
,则从节点需要进行全量复制流程。+CONTINUE
,从节点进行部分复制流程。-ERR
,说明 Redis 主节点版本过低,不支持 PSYNC
命令。从节点可以使用 SYNC
命令进行全量复制。(二者的区别在于,sync 会阻塞 redis-server,psync 则不会阻塞)全量复制流程如下:
PSYNC
命令给主节点进行数据同步,由于是第一次进行复制,从节点没有主节点的运行 ID 和复制偏移量,所以发送 PSYNC ? -1
。+FULLRESYNC
响应。BGSAVE
进行 RDB 文件的持久化。BGREWRITE
操作,得到最近的 AOF 文件。有磁盘复制 VS 无磁盘复制:
Redis 从 2.8.18 版本开始支持无磁盘复制。即主节点在执行 RDB 生成流程时,不会生成 RDB 文件到磁盘中,而是直接把生成的 RDB 数据通过网络发送给从节点,然后从节点直接直接将收到的 RDB 数据进行加载,这样就节省了一系列的写硬盘和读硬盘的操作开销。 但即使是这样,全量复制的开销也很大,因为要将所有的数据通过网络进行传输,而网络传输的代价比写磁盘要高很多。
部分复制流程如下:
repl-timeout
时间,主节点会认为从节点故障并终止复制连接。repl-backlog-buffer
中。replicationId
和复制偏移量作为 PSYNC
的参数发送给主节点,请求进行部分复制。PSYNC
请求后,进行必要的验证。然后根据 offset
去 repl-backlog-buffer
查找合适的数据,并响应 +CONTINUE
给从节点。特别注意:
master_replid2
中会保存主节的 replid,然后自己成为主节点。当网络恢复二者重连时,从节点向主节点发送 psync master_replid2 offset
,主节点根据 replid 是否相同以及 offset 与复制积压缓冲区的情况来决定是否进行部分复制。
因此,在主从复制中起作用的是 replid,runid 作用于哨兵。主从节点在建立复制连接后,会进行实时复制 (实时数据同步)。主节点会把自己收到的修改命令,通过 TCP 长连接的方式,源源不断地传输给从节点 (注意发送的是命令而不是二进制数据)。从节点会根据这些请求来同时修改自身的数据,以保持和主节点数据的一致性。
另外,这样的长连接,需要通过心跳包的方式来维护连接状态:(这里的心跳是指应用层自己实现的心跳,而不是 TCP 自带的心跳)
replconf ack {offset}
命令,给主节点上报自身当前的复制偏移量。如果主节点发现从节点通信延迟超过 repl-timeout
配置的值(默认 60 秒),则判定从节点下线,断开复制客户端连接。从节点恢复连接后,心跳机制继续进行。