在分布式系统中为了解决单点问题,通常会把数据复制多个副本部署到其他服务器,满足故障恢复和负载均衡等需求。Redis 也是如此,它为我们提供了复制的功能,实现了相同数据的多个 Redis 副本。复制功能是高可用 Redis 的基础,哨兵和集群都是在复制的基础上构建的。
参与复制的 Redis 实例划分为主节点(master)和从节点(slave)。每个从结点只能有一个主节点, 而一个主节点可以同时具有多个从结点。复制的数据流是单向的,只能由主节点到从节点。配置复制的方式有以下三种:
接下来,将 redis.conf 配置文件复制⼀份 redis-slave.conf,并且修改其 daemonize 为 yes。这里为了方便,采用的是同一台主机,不同的端口号来模拟多台主机。
# By default Redis does not run as a daemon. Use 'yes' if you need it.
# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
daemonize yes
port 6380
slaveof 127.0.0.1 6379 #配置主节点 同一主机,不同端口号
接下来,默认启动的 redis 作为主 Redis,重新通过命令行启动⼀个 Redis 实例作为从 Redis:
[root@host ~]# sudo redis-server slave.conf
[root@host ~]# ps axj | grep redis
1 915 915 915 ? -1 Ssl 130 0:08 /usr/bin/redis-server 0.0.0.0:6379
1 4544 4544 4544 ? -1 Ssl 0 0:00 redis-server 0.0.0.0:6380
2440 4566 4565 2440 pts/3 4565 S+ 1000 0:00 grep --color=auto redis
注意: 修改配置主要是修改从机的配置,主机配置不变
通过 netstat -nlpt 确保两个 Redis 均已正确启动。
[root@host ~]# netstat -nlpt
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 127.0.0.1:6379 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:6380 0.0.0.0:* LISTEN
通过 redis-cli 可以连接主 Redis 实例,通过 redis-cli -p 6380 连接从 Redis。并且观察复制关系。
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> get hello
"world"
127.0.0.1:6380> get hello
"world"
从运行结果中看到复制已经工作了,针对主节点 6379 的任何修改都可以同步到从节点 6380 中
可以通过 info replication 命令查看复制相关状态。
1)主节点 6379 复制状态信息
127.0.0.1:6379> info replication
# Replication
role:master #这表明当前的Redis服务器是一个主节点(master),它可以有多个从节点(slave)复制数据。
connected_slaves:1 #表示有一个从节点(slave)与这个主节点(master)连接。
slave0:ip=127.0.0.1,port=6380,state=online,offset=858,lag=0 #这是关于连接的从节点的详细信息。从节点的IP地址是127.0.0.1,端口号是6380,目前的状态是在线(online),复制偏移量是858,延迟(lag)是0秒。
master_replid:2f3da62b142c50ebbe870fc5114c57c81ba258ae #这是主节点复制ID,是复制流的唯一标识符。
master_replid2:0000000000000000000000000000000000000000 #这是第二个主节点复制ID,用于故障转移后的复制链。
master_repl_offset:858 #这是主节点的当前复制偏移量。从节点需要与这个偏移量保持同步,以确保数据一致性。
second_repl_offset:-1 #这是第二个复制偏移量,通常在进行故障转移或者其他复制相关操作时使用。
repl_backlog_active:1 #复制积压缓冲区是否激活(1表示激活)。这是一个固定大小的缓冲区,用来保存主节点上最近的更改,以便从节点可以断线后继续复制。
repl_backlog_size:1048576 #复制积压缓冲区的大小,单位是字节。
repl_backlog_first_byte_offset:1 #积压缓冲区中的第一个字节在整个复制流中的偏移量。
repl_backlog_histlen:858 #积压缓冲区中的数据长度,也就是目前缓冲区中保存的数据量大小。
2)从节点 6380 复制状态信息
127.0.0.1:6380> info replication
# Replication
role:slave #当前的Redis实例是一个从节点。它从主节点同步数据。
master_host:127.0.0.1 #主节点的IP地址,这里是本地地址。
master_port:6379 #主节点的端口号
master_link_status:up #与主节点的连接状态为上线(up),这意味着从节点与主节点之间的连接是活动的。
master_last_io_seconds_ago:2 #这表示最后一次从主节点接收到输入/输出(即数据同步或心跳信号)是在2秒前。
master_sync_in_progress:0 #表示当前没有进行主节点同步的操作。0表示没有正在进行的同步。
slave_repl_offset:844 #表示从节点的复制偏移量,这是从节点最后一次知道的已处理的数据偏移量。
slave_priority:100 #这是从节点的优先级,用于故障转移选举。数值越低,成为新主节点的优先级越高。
slave_read_only:1 #指示从节点是否为只读模式,1表示是。这是正常的,因为从节点通常不接受写操作。
connected_slaves:0 #从节点自己不会有其他的从节点,所以这个数字是0。
master_replid:2f3da62b142c50ebbe870fc5114c57c81ba258ae
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:844
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:844
slaveof 命令不但可以建立复制,还可以在从节点执行 slaveof no one 来断开与主节点复制关系。 例如在 6380 节点上执行 slaveof no one 来断开复制。
127.0.0.1:6380> slaveof no one
OK
127.0.0.1:6380> info replication
# Replication
role:master
connected_slaves:0
master_replid:b8fed86234a5cb796792da82ebbbf488bd86b52d
master_replid2:2f3da62b142c50ebbe870fc5114c57c81ba258ae
master_repl_offset:2202
second_repl_offset:2203
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:2202
虽然通过 slaveof no one 断开了主从关系,但是我们是通过配置文件进行配置的,所以重启后依然还是维持主从关系,如果需要永久的断开,只需要将配置文件 redis.conf 中的 slaveof 信息删除即可。
断开复制主要流程:
1)断开与主节点复制关系。
2)从节点晋升为主节点。
从节点断开复制后并不会抛弃原有数据,只是无法再获取主节点上的数据变化。
通过 slaveof 命令还可以实现切主操作,将当前从节点的数据源切换到另⼀个主节点。执行 slaveof {newMasterIp} {newMasterPort} 命令即可。也可以在配置文件中修改。
在启动一个从节点,端口号为6381
[root@host ~]# ps axj | grep redis
1 915 915 915 ? -1 Ssl 130 0:13 /usr/bin/redis-server 0.0.0.0:6379
2440 5227 5227 2440 pts/3 5227 S+ 1000 0:00 redis-cli -p 6379
1 5949 5949 5949 ? -1 Ssl 0 0:00 redis-server 0.0.0.0:6380
1 6400 6400 6400 ? -1 Ssl 0 0:00 redis-server 0.0.0.0:6381
5275 6424 6423 5275 pts/4 6423 S+ 1000 0:00 grep --color=auto redis
127.0.0.1:6381> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_repl_offset:2720
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:2f3da62b142c50ebbe870fc5114c57c81ba258ae
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:2720
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:2049
repl_backlog_histlen:672
我们将 6381 的主从 6379 切换到 6380,观察一下
127.0.0.1:6381> slaveof 127.0.0.1 6380
OK
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:9
master_sync_in_progress:0
slave_repl_offset:2832
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:2f3da62b142c50ebbe870fc5114c57c81ba258ae
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:2832
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:2049
repl_backlog_histlen:784
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:0
master_sync_in_progress:0
slave_repl_offset:2874
slave_priority:100
slave_read_only:1
connected_slaves:1
slave0:ip=127.0.0.1,port=6381,state=online,offset=2860,lag=1 # 从节点
master_replid:2f3da62b142c50ebbe870fc5114c57c81ba258ae
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:2874
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:2049
repl_backlog_histlen:826
可以看到 6380 中的节点信息不仅有主节点 6379 的信息,还有从节点 6381 的信息
切主操作主要流程:
1)断开与旧主节点复制关系。
2)与新主节点建立复制关系。
3)删除从节点当前所有数据。
4)从新主节点进行复制操作。
对于数据比较重要的节点,主节点会通过设置 requirepass 参数进行密码验证,这时所有的客户端访问必须使用 auth 命令实行校验。从节点与主节点的复制连接是通过⼀个特殊标识的客户端来完成,因此需要配置从节点的masterauth 参数与主节点密码保持⼀致,这样从节点才可以正确地连接到主节点并发起复制流程。
配置主节点:
在主节点的配置文件中(redis.conf
),设置requirepass
参数为密码:
requirepass password
配置从节点:
在从节点的配置文件中,设置masterauth
参数为主节点相同的密码:
masterauth password
重启Redis服务:
对于配置更改生效,需要重启Redis服务。这通常通过以下命令来实现:
sudo systemctl restart redis
默认情况下,从节点使用 slave-read-only=yes 配置为只读模式。由于复制只能从主节点到从节点,对于从节点的任何修改主节点都无法感知,修改从节点会造成主从数据不一致。所以建议线上不要修改从节点的只读模式。
主从节点一般部署在不同机器上,复制时的网络延迟就成为需要考虑的问题,Redis 为我们提供 了 repl-disable-tcp-nodelay 参数用于控制是否关闭 TCP_NODELAY,默认为 no,即开启 tcpnodelay 功能,说明如下:
Redis 的复制拓扑结构可以支持单层或多层复制关系,根据拓扑复杂性可以分为以下三种:一主一从、一主多从、树状主从结构。
一主一从结构是最简单的复制拓扑结构,用于主节点出现宕机时从节点提供故障转移支持。当应用写命令并发量较高且需要持久化时,可以只在从节点上开启AOF,这样既可以保证数据安全性同时也避免了持久化对主节点的性能干扰。但需要注意的是,当主节点关闭持久化功能时,如果主节点宕机要避免自动重启操作。
一主一从拓扑
一主多从结构(星形结构)使得应用端可以利用多个从节点实现读写分离。对于读比重较大的场景,可以把读命令负载均衡到不同的从节点上来分担压力。同时一些耗时的读命令可以指定一台专门的从节点执行,避免破坏整体的稳定性。对于写并发量较高的场景,多个从节点会导致主节点写命令的多次发送从而加重主节点的负载。
一主多从拓扑
树形主从结构(分层结构)使得从节点不但可以复制主节点数据,同时可以作为其他从节点的主节点继续向下层复制。通过引入复制中间层,可以有效降低住系钦按负载和需要传送给从节点的数据量,如图所示。数据写入节点A之后会同步给B和C节点,B 节点进一步把数据同步给D和E节点。当主节点需要挂载等多个从节点时为了避免对主节点的性能干扰,可以采用这种拓扑结构。
树形拓扑
主从节点建立复制流程图
1)保存主节点(master) 的信息。开始配置主从同步关系之后,从节点只保存主节点的地址信息,此时建立复制流程还没有开始在从节点 6380 执行info replication 可以看到如下信息:
master_host: 127.0.0.1
master_port: 6379
master_link_status: down
从统计信息可以看出,主节点的 ip 和 port 被保存下来,但是主节点的连接状态 (master_link_status)是下线状态。
2)从节点(slave)内部通过每秒运行的定时任务维护复制相关逻辑,当定时任务发现存在新的主节点后,会尝试与主节点建立基于 TCP 的网络连接。如果从节点无法建立连接,定时任务会无限重试直到连接成功或者用户停止主从复制。
3)发送 ping 命令。连接建立成功之后,从节点通过 ping 命令确认主节点在应用层上是工作良好的。 如果 ping 命令的结果 pong 回复超时,从节点会断开 TCP 连接,等待定时任务下次重新建立连接。
4)权限验证。如果主节点设置了 requirepass 参数,则需要密码验证,从节点通过配置 masterauth 参数来设置密码。如果验证失败,则从节点的复制将会停止。
5)同步数据集。对于首次建立复制的场景,主节点会把当前持有的所有数据全部发送给从节点,这步操作基本是耗时最长的,所以又划分称两种情况:全量同步和部分同步。
6)命令持续复制。当从节点复制了主节点的所有数据之后,针对之后的修改命令,主节点会持续的把命令发送给从节点,从节点执行修改命令,保证主从数据的一致性。
Redis 提供了 psync 命令,完成数据同步的过程。psync 不需要手动执行,Redis 服务器会在建立号主从同步关系之后,自动执行 pysnc。同步过程分为:全量复制和部分复制。
PSYNC 的语法格式
PSYNC replicationid offset
如果 replicationid 设为 ? 并且 offset 设为 -1 此时就是在尝试进行全量复制.
如果 replicationid offset 设为了具体的数值, 则是尝试进行部分复制.
主节点的复制 id. 主节点重新启动, 或者从节点晋级成主节点, 都会生成⼀个 replicationid (同⼀个节1点, 每次重启, 生成的 replicationid 也会变化)。
从节点在和主节点建立连接之后, 就会获取到主节点的 replicationid。
通过 info replication 即可看到 replicationid
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:0
master_replid:1da596acecf5a34b4b2aae45bd35be785691ae69
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
关于 master_replid 和 master_replid2
每个节点需要记录两组 master_replid ,这个设定解决的问题场景是这样的:
比如当前有两个节点 A 和 B,A 为 master,B 为 slave。此时 B 就会记录 A 的 master_replid。如果网络出现抖动,B 以为 A 挂了,B 自己就会成为主节点。于是 B 给自己分配了新的 master_replid。此时就会使用 master_replid2 来保存之前 A 的 master_replid.
参与复制的主从节点都会维护自身复制偏移量。主节点 (master)在处理完写入命令后,会把命令的字节长度做累加记录,统计信息在 info replication 中的 master_repl_offset 指标中。
127.0.0.1:6379> info replication
# Replication
role:master
...
master_repl_offset:1055130
从节点每秒钟上报自身的复制偏移量给主节点,因此主节点也会保存从节点的复制偏移量,统计指标如下:
127.0.0.1:6379> info replication
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=1055214,lag=1
...
从节点在接受到主节点发送的命令后,也会累加记录自身的偏移量。统计信息在 info replication 中的 slave_repl _offset 指标中:
127.0.0.1:6380> info replication
# Replication
role:slave
...
slave_repl_offset:1055214
通过对比主从节点的复制偏移量,可以判断主从节点数据是否一致
replid + offset 共同标识了⼀个 “数据集”。 如果两个节点,他们的 replid 和 offset 都相同,则这两个节点上持有的数据,就⼀定相同
1)从节点发送 psync 命令给主节点,replid 和 offset 的默认值分别是 ? 和 -1
2)主节点根据 psync 参数和自身数据情况决定响应结果:
psync ⼀般不需要手动执行,Redis 会在主从复制模式下自动调用执行
sync 会阻塞 redis server 处理其他请求;psync 则不会
全量复制是 Redis 最早支持的复制方式,也是主从第一次建立复制时必须经历的阶段。
全量复制流程
从节点发送 psync 命令给主节点进行数据同步,由于是第一次进行复制,从节点没有主节点的运行 ID 和复制偏移量,所以发送 psync ? -1。
主节点根据命令,解析出要进行全量复制,回复 +FULLRESYNC 响应。
从节点接收主节点的运行信息进行保存。
主节点执行 bgsave 进行 RDB 文件的持久化。
主节点发送 RDB 文件给从节点,从节点保存 RDB 数据到本地硬盘
主节点将从生成 RDB 到接收完成期间执行的写命令,写入缓冲区中,等从节点保存完 RDB 文件后,主节点再将缓冲区内的数据补发给从节点,补发的数据仍然按照 RDB 的二进制格式追加写入到收到的 RDB 文件中保持主从一致性。
从节点清空自身原有旧数据
从节点加载 RDB文件得到与主节点一致的数据。
如果从节点加载 RDB 完成之后,并且开启了 AOF 持久化功能,它会进行 bgrewrite 操作,得到最近的 AOF 文件。
通过分析全量复制的所有流程,我们会发现全量复制是一件高成本的操作:主节点 bgsave 的时间,RDB 在网络传输的时间,从节点清空旧数据的时间,从节点加载 RDB 的时间等。所以一般应该尽可能避免对已经有大量数据集的 Redis 进行全量复制。
有磁盘复制和无磁盘复制(diskless)
默认情况下,进行全量复制需要主节点生成 RDB 文件到主节点的磁盘中,再把磁盘上的 RDB 文件发送给从节点。
Redis 从 2.8.18 版本开始支持无磁盘复制主节点在执行 RDB 生成流程时,不会生成 RDB 文件到磁盘中了,而是直接把生成的 RDB 数据通过网络发送给从节点这样就节省了一系列的写硬盘和读硬盘的操作开销。
部分复制主要是 Redis 针对全量复制的过高开销做出的一种优化措施,使用 psync replicationid offset 命令实现。当从节点正在复制主节点时,如果出现网络闪断或者命令丢失等异常情况时,从节点会向主节点要求补发丢失的命令数据,如果主节点的复制积压缓冲区存在数据则直接发送给从节点。这样就可以保持主从节点复制的一致性。补发的这部分数据一般远远小于全量数据,所以开销很小。
部分复制流程
1)当主从节点之间出现网络中断时,如果超过 repl-timeout 时间,主节点会认为从节点故障并终端复制连接。
2)主从连接中断期间主节点依然响应命令,但这些复制命令都因网络中断无法及时发送给从节点,所以暂时将这些命令滞留在复制积压缓冲区中。
3)当主从节点网络恢复后,从节点再次连上主节点。
4)从节点将之前保存的 replicationid 和复制偏移量作为 psync 的参数发送给主节点,请求进行部分复制。
5)主节点接到 psync 请求后,进行必要的验证。随后根据 offset 去复制积压缓冲区查找合适的数据并响应 +CONTINUE 给从节点。
6)主节点将需要从节点同步的数据发送给从节点,最终完成一致性。
复制积压缓冲区
复制积压缓冲区是保存在主节点上的一个固定长度的队列,默认大小为 1MB,当主节点有连接的从节点(slave)时被创建,这时主节点 (master) 响应写命令时,不但会把命令发送给从节点,还会写入复制积压缓冲区。
由于缓冲区本质上是先进先出的定长队列,所以能实现保存最近已复制数据的功能,用于部分复制和复制命令丢失的数据补救。复制缓冲区相关统计信息可以通过主节点的 info replication 中:
127.0.0.1:6379> info replication
# Replication
role:master
...
repl_backlog_active:1 // 开启复制缓冲区
repl_backlog_size:1048576 // 缓冲区最⼤⻓度
repl_backlog_first_byte_offset:7479 // 起始偏移量,计算当前缓冲区可⽤范围
repl_backlog_histlen:1048576 // 已保存数据的有效⻓度
根据统计指标,可算出复制积压缓冲区内的可用偏移量范围:[repl_backlog_first_byte_ofset,repl backlog first byte offset + repl backlog histlen];这个相当于⼀个基于数组实现的环形队列,上述区间中的值就是 “数组下标”
如果当前从节点需要的数据,已经超出了主节点的积压缓冲区的范围,则无法进行部分复制,只能全量复制了。
主从节点在建立复制连接后,主节点会把自己收到的修改操作,通过 tcp 长连接的方式,源源不断的传输给从节点。从节点就会根据这些请求来同时修改自身的数据,从而保持和主节点数据的一致性。
另外,这样的长连接需要通过心跳包的方式来维护连接状态(这里的心跳是指应用层自己实现的心跳而不是TCP自带的心跳)
1)主从节点彼此都有心跳检测机制,各自模拟成对方的客户端进行通信。
2)主节点默认每隔 10 秒对从节点发送 ping命令,判断从节点的存活性和连接状态。
3)从节点默认每隔 1 秒向主节点发送 replconf ack {offset} 命令,给主节点上报自身当前的复制偏移量。
如果主节点发现从节点通信延迟超过 repl-timeout 配置的值(默认 60 秒),则判定从节点下线,断开复制客户端连接。从节点恢复连接后,心跳机制继续进行。
主从复制解决单点问题
主从复制的特点:
主从复制配置的过程:
主从复制的缺点: