概述
常见的Redis高可用的方案包括持久化、主从复制(及读写分离)、哨兵和集群。其中持久化侧重解决的是Redis数据的单机备份问题(从内存到硬盘的备份);而主从复制则侧重解决数据的多机热备。此外,主从复制还可以实现负载均衡和故障恢复。使用中,我们可能会选择其中的一种或者多种组合来保证高可用,当然,所谓的高可用只是在一定程度上的高可用。
本文将详细介绍Redis的主从复制,包括:如何使用主从复制、主从复制的原理(重点是全量复制和部分复制、以及心跳机制)、实际应用中需要注意的问题(如数据不一致问题、复制超时问题、复制缓冲区溢出问题)、主从复制相关的配置(重点是repl-timeout、client-output-buffer-limit slave)等。
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。
默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。
主从复制的作用主要包括:
- 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
- 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
- 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
- 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
主从复制搭建
为方便理解主从复制,在介绍其内部原理之前,我们先来看看如何开启主从复制,这个过程其实是很简单的。
建立复制方法
需要注意,主从复制的开启,完全是在从节点发起的;主节点不需要任何操作。
从节点开启主从复制,有以下3种方式:
(1)配置文件
在从服务器的配置文件中加入:slaveof <masterip> <masterport>
masterauth (视主节点的情况决定是否配置)
(2)启动命令-如果master节点设置有密码,建议使用配置文件进行主从复制配置,此方法貌似不能配置同步密码
redis-server启动命令后加入 --slaveof
(3)客户端命令-如果master节点设置有密码,建议使用配置文件进行主从复制配置,此方法貌似也不能配置同步密码
Redis服务器启动后,直接通过客户端执行命令:slaveof ,则该Redis实例成为从节点。
上述3种方式是等效的,下面以配置文件的方式为例进行主从复制。
实例
准备工作:启动两个redis实例
本次实验在两台centos7 的服务器上分别部署了一个Redis实例(部署参考https://blog.csdn.net/margu_168/article/details/136282842
),注意版本是6.0.6,端口都为6379。注意,由于我的主节点m2(192.168.2.141)的redis设置了密码,所以从节点m1(192.168.2.140)上的redis需要相应的配置:
bash
# 从节点的登录密码可以单独设置
requirepass 12345678
replicaof 192.168.2.141 6379
masterauth 123456
#masteruser root #不用打开
然后在从节点上启动redis服务,可以从日志查看同步的情况。
bash
[root@k8s-m1 ~]# /usr/local/redis/src/redis-server /etc/redis/redis.conf
16085:C 13 Mar 2024 10:52:22.381 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
16085:C 13 Mar 2024 10:52:22.381 # Redis version=6.0.6, bits=64, commit=00000000, modified=0, pid=16085, just started
16085:C 13 Mar 2024 10:52:22.381 # Configuration loaded
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 6.0.6 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in standalone mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 6379
| `-._ `._ / _.-' | PID: 16085
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | http://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
16085:S 13 Mar 2024 10:52:22.385 # Server initialized
16085:S 13 Mar 2024 10:52:22.386 * Loading RDB produced by version 6.0.6
16085:S 13 Mar 2024 10:52:22.386 * RDB age 25 seconds
16085:S 13 Mar 2024 10:52:22.386 * RDB memory usage when created 1.79 Mb
16085:S 13 Mar 2024 10:52:22.386 * DB loaded from disk: 0.000 seconds
16085:S 13 Mar 2024 10:52:22.386 * Before turning into a replica, using my own master parameters to synthesize a cached master: I may be able to synchronize with the new master with just a partial transfer.
16085:S 13 Mar 2024 10:52:22.386 * Ready to accept connections
16085:S 13 Mar 2024 10:52:22.386 * Connecting to MASTER 192.168.2.141:6379
16085:S 13 Mar 2024 10:52:22.386 * MASTER <-> REPLICA sync started
16085:S 13 Mar 2024 10:52:22.387 * Non blocking connect for SYNC fired the event.
16085:S 13 Mar 2024 10:52:22.387 * Master replied to PING, replication can continue...
16085:S 13 Mar 2024 10:52:22.388 * Trying a partial resynchronization (request 36d09b8fd62156ead87de496a4b729a0c73947c7:1751).
16085:S 13 Mar 2024 10:52:22.388 * Successful partial resynchronization with master.
16085:S 13 Mar 2024 10:52:22.388 * MASTER <-> REPLICA sync: Master accepted a Partial Resynchronization.
测试:
下面验证一下,在主从复制建立后,主节点的数据会复制到从节点中。
(1)首先在从节点查询一个不存在的key:
bash
192.168.2.140:6379> EXISTS f
(integer) 0
(2)然后在主节点中增加这个key:
bash
192.168.2.141:6379> SEt f 89
OK
(3)此时在从节点中再次查询这个key,会发现主节点的操作已经同步至从节点:
bash
192.168.2.140:6379> EXISTS f
(integer) 1
192.168.2.140:6379> GET f
"89"
(4)然后在主节点删除这个key:
bash
192.168.2.141:6379> DEL f
(integer) 1
(5)此时在从节点中再次查询这个key,会发现主节点的操作已经同步至从节点:
bash
192.168.2.140:6379> GET f
(nil)
断开复制
可以直接通过slaveof no one断开,然后使用config rewrite 将配置写入到配置文件中;或者直接修改配置文件,将上面主从相关的配置注释重启即可。需要注意的是,从节点断开复制后,不会删除已经同步的数据,只是不再接受主节点新的数据变化。
从节点执行slaveof no one后,打印日志如下所示;可以看出断开复制后,从节点又变回为主节点(默认都是主机点)。
bash
16085:M 13 Mar 2024 11:18:44.812 # Connection with master lost.
16085:M 13 Mar 2024 11:18:44.812 * Caching the disconnected master state.
16085:M 13 Mar 2024 11:18:44.812 * Discarding previously cached master state.
16085:M 13 Mar 2024 11:18:44.812 # Setting secondary replication ID to 36d09b8fd62156ead87de496a4b729a0c73947c7, valid up to offset: 4020. New replication ID is e408a4ddbe808b5692e09b869dee738dadf5eb77
16085:M 13 Mar 2024 11:18:44.812 * MASTER MODE enabled (user request from 'id=5 addr=192.168.2.140:48078 fd=8 name= age=1549 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=34 qbuf-free=32734 obl=0 oll=0 omem=0 events=r cmd=slaveof user=default')
bash
192.168.2.140:6379> INFO replication
# Replication
role:master
connected_slaves:0
master_replid:635a927b9e4bb9d7710087f5a8dbf42f7a6a47f0
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
主节点打印日志如下:
bash
23761:M 13 Mar 2024 11:18:44.814 # Connection with replica 192.168.2.140:6379 lost.
主从复制的实现原理
上面介绍了主从复制的搭建配置;接下来将介绍主从复制的实现原理。
主从复制过程大体可以分为3个阶段:连接建立阶段(即准备阶段)、数据同步阶段、命令传播阶段;下面分别进行介绍。
连接建立阶段
该阶段的主要作用是在主从节点之间建立连接,为数据同步做好准备。
步骤1:保存主节点信息
从节点服务器内部维护了两个字段,即masterhost和masterport字段,用于存储主节点的ip和port信息。
需要注意的是,slaveof是异步命令,从节点完成主节点ip和port的保存后,向发送slaveof命令的客户端直接返回OK,实际的复制操作在这之后才开始进行。
这个过程中,可以看到从节点打印日志如下:
bash
26859:S 13 Mar 2024 11:33:00.175 * Before turning into a replica, using my own master parameters to synthesize a cached master: I may be able to synchronize with the new master with just a partial transfer.
26859:S 13 Mar 2024 11:33:00.176 * REPLICAOF 192.168.2.141:6379 enabled (user request from 'id=4 addr=192.168.2.140:33358 fd=7 name= age=578 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=47 qbuf-free=32721 obl=0 oll=0 omem=0 events=r cmd=slaveof user=default')
步骤2:建立socket连接
从节点每秒1次调用复制定时函数replicationCron(),如果发现了有主节点可以连接,便会根据主节点的ip和port,创建socket连接。如果连接成功,则:
从节点:为该socket建立一个专门处理复制工作的文件事件处理器,负责后续的复制工作,如接收RDB文件、接收命令传播等。
主节点:接收到从节点的socket连接后(即accept之后),为该socket创建相应的客户端状态,并将从节点看做是连接到主节点的一个客户端,后面的步骤会以从节点向主节点发送命令请求的形式来进行。
这个过程中,从节点打印日志如下:
bash
26859:S 13 Mar 2024 11:33:00.278 * Connecting to MASTER 192.168.2.141:6379
26859:S 13 Mar 2024 11:33:00.278 * MASTER <-> REPLICA sync started
步骤3:发送ping命令
从节点成为主节点的客户端之后,发送ping命令进行首次请求,目的是:检查socket连接是否可用,以及主节点当前是否能够处理请求。
从节点发送ping命令后,可能出现3种情况:
(1)返回pong:说明socket连接正常,且主节点当前可以处理请求,复制过程继续。
(2)超时:一定时间后从节点仍未收到主节点的回复,说明socket连接不可用,则从节点断开socket连接,并重连。
(3)返回pong以外的结果:如果主节点返回其他结果,如正在处理超时运行的脚本,说明主节点当前无法处理命令,则从节点断开socket连接,并重连。
在主节点返回pong情况下,从节点打印日志如下:
bash
26859:S 13 Mar 2024 11:33:00.279 * Non blocking connect for SYNC fired the event.
26859:S 13 Mar 2024 11:33:00.279 * Master replied to PING, replication can continue...
步骤4:身份验证
如果从节点中设置了masterauth选项,则从节点需要向主节点进行身份验证;没有设置该选项,则不需要验证。从节点进行身份验证是通过向主节点发送auth命令进行的,auth命令的参数即为配置文件中的masterauth的值。
如果主节点设置密码的状态,与从节点masterauth的状态一致(一致是指都存在,且密码相同,或者都不存在),则身份验证通过,复制过程继续;如果不一致,则从节点断开socket连接,并重连。
步骤5:发送从节点端口信息
身份验证之后,从节点会向主节点发送其监听的端口号(前述例子中为6379),主节点将该信息保存到该从节点对应的客户端的port字段中;该端口信息除了在主节点中执行info Replication时显示以外,没有其他作用。
数据同步阶段
主从节点之间的连接建立以后,便可以开始进行数据同步,该阶段可以理解为从节点数据的初始化。具体执行的方式是:从节点向主节点发送psync命令(PSYNC replicationid offset,Redis2.8以前是sync命令),开始同步。
数据同步阶段是主从复制最核心的阶段,根据主从节点当前状态的不同,可以分为全量复制和部分复制,后面会专门讲解这两种复制方式以及psync命令的执行过程。
需要注意的是,在数据同步阶段之前,从节点是主节点的客户端,主节点不是从节点的客户端;而到了这一阶段及以后,主从节点互为客户端。原因在于:在此之前,主节点只需要响应从节点的请求即可,不需要主动发请求,而在数据同步阶段和后面的命令传播阶段,主节点需要主动向从节点发送请求(如推送缓冲区中的写命令),才能完成复制。
命令传播阶段
数据同步阶段完成后,主从节点进入命令传播阶段;在这个阶段主节点将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性。
在命令传播阶段,除了发送写命令,主从节点还维持着心跳机制:PING和REPLCONF ACK。后续会单独介绍
延迟与不一致
需要注意的是,命令传播是异步的过程,即主节点发送写命令后并不会等待从节点的回复;因此实际上主从节点之间很难保持实时的一致性,延迟在所难免。数据不一致的程度,与主从节点之间的网络状况、主节点写命令的执行频率、以及主节点中的repl-disable-tcp-nodelay配置等有关。
repl-disable-tcp-nodelay no:该配置作用于命令传播阶段,控制主节点是否禁止与从节点的TCP_NODELAY;默认no,即不禁止TCP_NODELAY。当设置为yes时,TCP会对包进行合并从而减少带宽,但是发送的频率会降低,从节点数据延迟增加,一致性变差;具体发送频率与Linux内核的配置有关,默认配置为40ms。当设置为no时,TCP会立马将主节点的数据发送给从节点,带宽增加但延迟变小。
一般来说,只有当应用对Redis数据不一致的容忍度较高,且主从节点之间网络状况不好时,才会设置为yes;多数情况使用默认值no。
【数据同步阶段】全量复制和部分复制
在Redis2.8以前,从节点向主节点发送sync命令请求同步数据,此时的同步方式是全量复制;在Redis2.8及以后版本,从节点可以发送psync命令请求同步数据,此时根据主从节点当前状态的不同,同步方式可能是全量复制或部分复制。后文介绍以Redis6.0.6版本为例进行说明。
全量复制:用于初次复制或其他无法进行部分复制的情况,将主节点中的所有数据都发送给从节点,是一个非常重型的操作。
部分复制:用于网络中断等情况后的复制,只将中断期间主节点执行的写命令发送给从节点,与全量复制相比更加高效。需要注意的是,如果网络中断时间过长,导致主节点没有能够完整地保存中断期间执行的写命令,则无法进行部分复制,仍使用全量复制。
全量复制
Redis通过psync命令进行全量复制的过程如下:
(1)从节点判断无法进行部分复制,向主节点发送全量复制的请求;或从节点发送部分复制的请求,但主节点判断无法进行部分复制;具体判断过程需要在讲述了部分复制原理后再介绍。
(2)主节点收到全量复制的命令后,执行bgsave,在后台生成RDB文件,并使用一个缓冲区(称为复制缓冲区,用于记录新的写入数据)记录从现在开始执行的所有写命令
(3)主节点的bgsave执行完成后,将RDB文件发送给从节点;从节点首先清除自己的旧数据,然后载入接收的RDB文件,将数据库状态更新至主节点执行bgsave时的数据库状态
(4)主节点将前述复制缓冲区中的所有写命令发送给从节点,从节点执行这些写命令,将数据库状态更新至主节点的最新状态
(5)如果从节点开启了AOF或者RDB,则会触发相关的操作(bgsave/bgrewriteaof),从而保证持久化文件更新至主节点的最新状态
下面是执行全量复制时,主从节点打印的日志;可以看出日志内容与上述步骤是完全对应的。
主节点的打印日志如下:
bash
23761:M 13 Mar 2024 14:14:29.929 * Replica 192.168.2.140:6379 asks for synchronization
23761:M 13 Mar 2024 14:14:29.929 * Full resync requested by replica 192.168.2.140:6379
23761:M 13 Mar 2024 14:14:29.929 * Starting BGSAVE for SYNC with target: disk
23761:M 13 Mar 2024 14:14:29.929 * Background saving started by pid 19270
19270:C 13 Mar 2024 14:14:29.942 * DB saved on disk
19270:C 13 Mar 2024 14:14:29.942 * RDB: 0 MB of memory used by copy-on-write
23761:M 13 Mar 2024 14:14:30.039 * Background saving terminated with success
23761:M 13 Mar 2024 14:14:30.040 * Synchronization with replica 192.168.2.140:6379 succeeded
从节点打印日志如下图所示:
bash
8365:S 13 Mar 2024 14:14:29.924 * Partial resynchronization not possible (no cached master)
8365:S 13 Mar 2024 14:14:29.925 * Full resync from master: 36d09b8fd62156ead87de496a4b729a0c73947c7:17207
8365:S 13 Mar 2024 14:14:30.035 * MASTER <-> REPLICA sync: receiving 358 bytes from master to disk
8365:S 13 Mar 2024 14:14:30.035 * MASTER <-> REPLICA sync: Flushing old data
8365:S 13 Mar 2024 14:14:30.035 * MASTER <-> REPLICA sync: Loading DB in memory
8365:S 13 Mar 2024 14:14:30.035 * Loading RDB produced by version 6.2.6
8365:S 13 Mar 2024 14:14:30.035 * RDB age 1 seconds
8365:S 13 Mar 2024 14:14:30.035 * RDB memory usage when created 1.85 Mb
8365:S 13 Mar 2024 14:14:30.036 * MASTER <-> REPLICA sync: Finished with success
其中,有几点需要注意:从节点接收了来自主节点的358 个字节的数据;从节点在载入主节点的数据之前要先将老数据清除;从节点在同步完数据后,调用了bgsave。
通过全量复制的过程可以看出,全量复制是一个重型的操作:
(1)主节点通过bgsave命令fork子进程进行RDB持久化,该过程是非常消耗CPU、内存(页表复制)、硬盘IO的;
(2)主节点通过网络将RDB文件发送给从节点,对主从节点的带宽都会带来很大的消耗
(3)从节点清空老数据、载入新RDB文件的过程是阻塞的,无法响应客户端的命令;如果从节点执行bgsave/bgrewriteaof,也会带来额外的消耗
部分复制
由于全量复制在主节点数据量较大时效率太低,因此Redis2.8开始提供部分复制,用于处理网络中断时的数据同步。
部分复制的实现,依赖于三个重要的概念:
(1)复制偏移量
主节点和从节点分别维护一个复制偏移量(offset),代表的是主节点向从节点传递的字节数;主节点每次向从节点传播N个字节数据时,主节点的offset增加N;从节点每次收到主节点传来的N个字节数据时,从节点的offset增加N。
offset用于判断主从节点的数据库状态是否一致:如果二者offset相同,则一致;如果offset不同,则不一致,此时可以根据两个offset找出从节点缺少的那部分数据。例如,如果主节点的offset是1000,而从节点的offset是600,那么部分复制就需要将offset为601-1000的数据传递给从节点。而offset为601-1000的数据存储的位置,就是下面要介绍的复制积压缓冲区。
(2)复制积压缓冲区
复制积压缓冲区是由主节点维护的、固定长度的、先进先出(FIFO)队列,默认大小1MB;当主节点开始有从节点时创建,其作用是备份主节点最近发送给从节点的数据。注意,无论主节点有一个还是多个从节点,都只需要一个复制积压缓冲区。
在命令传播阶段,主节点除了将写命令发送给从节点,还会发送一份给复制积压缓冲区,作为写命令的备份;除了存储写命令,复制积压缓冲区中还存储了其中的每个字节对应的复制偏移量(offset)。由于复制积压缓冲区定长且是先进先出,所以它保存的是主节点最近执行的写命令;时间较早的写命令会被挤出缓冲区。
由于该缓冲区长度固定且有限,因此可以备份的写命令也有限,当主从节点offset的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制。反过来说,为了提高网络中断时部分复制执行的概率,可以根据需要增大复制积压缓冲区的大小(通过配置repl-backlog-size);例如如果网络中断的平均时间是60s,而主节点平均每秒产生的写命令(特定协议格式)所占的字节数为100KB,则复制积压缓冲区的平均需求为6MB,保险起见,可以设置为12MB,来保证绝大多数断线情况都可以使用部分复制。
从节点将offset发送给主节点后,主节点根据offset和缓冲区大小决定能否执行部分复制:
- 如果offset偏移量之后的数据,仍然都在复制积压缓冲区里,则执行部分复制;
- 如果offset偏移量之后的数据已不在复制积压缓冲区中(数据已被挤出),则执行全量复制。
(3)服务器运行ID(runid)
每个Redis节点(无论主从),在启动时都会自动生成一个随机ID(每次启动都不一样),由40个随机的十六进制字符组成;runid用来唯一识别一个Redis节点。通过info Server命令,可以查看节点的runid:
bash
[root@k8s-m1 src]# ./redis-cli -h 192.168.2.140 -a 12345678 info |grep run_id
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
run_id:0e958cf7fdf9f7d7c8a12f05ab20f72ac8af6a58
[root@k8s-m1 src]#
主从节点初次复制时,主节点将自己的runid发送给从节点,从节点将这个runid保存起来;当断线重连时,从节点会将这个runid发送给主节点;主节点根据runid判断能否进行部分复制:
- 如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制(到底能不能部分复制还要看offset和复制积压缓冲区的情况);
- 如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的Redis节点并不是当前的主节点,只能进行全量复制。
psync命令的执行
在了解了复制偏移量、复制积压缓冲区、节点运行id之后,下面将介绍psync命令的参数和返回值,从而说明psync命令执行过程中,主从节点是如何确定使用全量复制还是部分复制的。
psync命令的执行过程可以参见下图(图片来源:《Redis设计与实现》):
(1)首先,从节点根据当前状态,决定如何调用psync命令:
- 如果从节点之前未执行过slaveof或最近执行了slaveof no one,则从节点发送命令为psync ? -1,向主节点请求全量复制;
- 如果从节点之前执行了slaveof,则发送命令为psync ,其中runid为上次复制的主节点的runid,offset为上次复制截止时从节点保存的复制偏移量。
(2)主节点根据收到的psync命令,及当前服务器状态,决定执行全量复制还是部分复制:
- 如果主节点版本低于Redis2.8,则返回-ERR回复,此时从节点重新发送sync命令执行全量复制;
- 如果主节点版本够新,且runid与从节点发送的runid相同,且从节点发送的offset之后的数据在复制积压缓冲区中都存在,则回复+CONTINUE,表示将进行部分复制,从节点等待主节点发送其缺少的数据即可;
- 如果主节点版本够新,但是runid与从节点发送的runid不同,或从节点发送的offset之后的数据已不在复制积压缓冲区中(在队列中被挤出了),则回复+FULLRESYNC ,表示要进行全量复制,其中runid表示主节点当前的runid,offset表示主节点当前的offset,从节点保存这两个值,以备使用。
部分复制示例
在下面的示例中,网络中断几分钟后恢复,断开连接的主从节点进行了部分复制;为了便于模拟网络中断,本例中的主从节点在局域网中的两台机器上。
网络中断
用root用户,执行以下restart_network.sh脚本即可完成模拟Linux服务器网络中断一分钟,根据需要设置时长。切记执行命令要使用nohup,即:nohup sh restart_network.sh &
bash
[root@k8s-m2 src]# cat restart_network.sh
logFile=/tmp/restart_network.log
echo "`date` to shutdown ens32" >> ${logFile}
ifdown ens32
echo "sleep 600s" >> ${logFile}
sleep 600s
echo "`date` to start ens32"
ifup ens32
systemctl restart network
网络中断一段时间后,主节点和从节点都会发现失去了与对方的连接(关于主从节点对超时的判断机制,后面会有说明);此后,从节点便开始执行对主节点的重连,由于此时网络还没有恢复,重连失败,从节点会一直尝试重连。
主节点日志如下:
bash
23761:M 13 Mar 2024 15:50:28.670 # Connection with replica 192.168.2.140:6379 lost.
从节点日志如下:
bash
8365:S 13 Mar 2024 15:50:19.374 # Connection with master lost.
8365:S 13 Mar 2024 15:50:19.374 * Caching the disconnected master state.
8365:S 13 Mar 2024 15:50:19.374 * Connecting to MASTER 192.168.2.141:6379
8365:S 13 Mar 2024 15:50:19.374 * MASTER <-> REPLICA sync started
8365:S 13 Mar 2024 15:51:20.714 # Timeout connecting to the MASTER...
8365:S 13 Mar 2024 15:51:20.714 * Connecting to MASTER 192.168.2.141:6379
8365:S 13 Mar 2024 15:51:20.714 * MASTER <-> REPLICA sync started
8365:S 13 Mar 2024 15:51:21.213 # Error condition on socket for SYNC: Operation now in progress
8365:S 13 Mar 2024 15:51:21.717 * Connecting to MASTER 192.168.2.141:6379
8365:S 13 Mar 2024 15:51:21.717 * MASTER <-> REPLICA sync started
8365:S 13 Mar 2024 15:51:27.227 # Error condition on socket for SYNC: Operation now in progress
8365:S 13 Mar 2024 15:51:27.755 * Connecting to MASTER 192.168.2.141:6379
8365:S 13 Mar 2024 15:51:27.755 * MASTER <-> REPLICA sync started
网络恢复
十分钟后网络恢复,从节点连接主节点成功,并请求进行部分复制,主节点接收请求后,二者进行部分复制以同步数据。
主节点日志如下:
bash
23761:M 13 Mar 2024 15:59:32.171 * Replica 192.168.2.140:6379 asks for synchronization
23761:M 13 Mar 2024 15:59:32.172 * Partial resynchronization request from 192.168.2.140:6379 accepted. Sending 98 bytes of backlog starting from offset 25370.
从节点日志如下:
bash
8365:S 13 Mar 2024 15:59:29.294 * Connecting to MASTER 192.168.2.141:6379
8365:S 13 Mar 2024 15:59:29.294 * MASTER <-> REPLICA sync started
8365:S 13 Mar 2024 15:59:32.160 * Non blocking connect for SYNC fired the event.
8365:S 13 Mar 2024 15:59:32.161 * Master replied to PING, replication can continue...
8365:S 13 Mar 2024 15:59:32.167 * Trying a partial resynchronization (request 36d09b8fd62156ead87de496a4b729a0c73947c7:25370).
8365:S 13 Mar 2024 15:59:32.168 * Successful partial resynchronization with master.
8365:S 13 Mar 2024 15:59:32.168 * MASTER <-> REPLICA sync: Master accepted a Partial Resynchronization.
【命令传播阶段】心跳机制
在命令传播阶段,除了发送写命令,主从节点还维持着心跳机制:PING和REPLCONF ACK。心跳机制对于主从复制的超时判断、数据安全等有作用。
1.主->从:PING
每隔指定的时间,主节点会向从节点发送PING命令,这个PING命令的作用,主要是为了让从节点进行超时判断。
PING发送的频率由repl-ping-slave-period参数控制,单位是秒,默认值是10s。
关于该PING命令究竟是由主节点发给从节点,还是相反,有一些争议;因为在Redis的官方文档中,对该参数的注释中说明是从节点向主节点发送PING命令,如下所示:
bash
# Replicas send PINGs to server in a predefined interval. It's possible to
# change this interval with the repl_ping_replica_period option. The default
# value is 10 seconds.
#
# repl-ping-replica-period 10
但是根据该参数的名称(含有ping-slave),以及代码实现,它的意思貌似该是PING命令是主节点发给从节点的。有兴趣的可以自行阅读一下源代码,但是其实也可以不用太深究。
- 从->主:REPLCONF ACK
在命令传播阶段,从节点会向主节点发送REPLCONF ACK命令,频率是每秒1次;命令格式为:REPLCONF ACK {offset},其中offset指从节点保存的复制偏移量。REPLCONF ACK命令的作用包括:
(1)实时监测主从节点网络状态:该命令会被主节点用于复制超时的判断。此外,在主节点中使用info Replication,可以看到其从节点的状态中的lag值,代表的是主节点上次收到该REPLCONF ACK命令的时间间隔,在正常情况下,该值应该是0或1,如下所示:
bash
[root@k8s-m2 src]# ./redis-cli -h 192.168.2.141 -a 123456 info replication|grep lag
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
slave0:ip=192.168.2.140,port=6379,state=online,offset=28519,lag=0
[root@k8s-m2 src]#
(2)检测命令丢失:从节点发送了自身的offset,主节点会与自己的offset对比,如果从节点数据缺失(如网络丢包),主节点会推送缺失的数据(这里也会利用复制积压缓冲区)。注意,offset和复制积压缓冲区,不仅可以用于部分复制,也可以用于处理命令丢失等情形;区别在于前者是在断线重连后进行的,而后者是在主从节点没有断线的情况下进行的。
(3)辅助保证从节点的数量和延迟:Redis主节点中使用min-slaves-to-write和min-slaves-max-lag参数,来保证主节点在不安全的情况下不会执行写命令;所谓不安全,是指从节点数量太少,或延迟过高。例如min-slaves-to-write和min-slaves-max-lag分别是3和10,含义是如果从节点数量小于3个,或所有从节点的延迟值都大于10s,则主节点拒绝执行写命令。而这里从节点延迟值的获取,就是通过主节点接收到REPLCONF ACK命令的时间来判断的,即前面所说的info Replication中的lag值。
应用中的问题
读写分离及其中的问题
在Redis的主从复制架构中支持读写分离,可以实现Redis的读负载均衡:由主节点提供写服务,由一个或多个从节点提供读服务(多个从节点既可以提高数据冗余程度,也可以最大化读负载能力);在读负载较大的应用场景下,可以大大提高Redis服务器的并发量。下面介绍在使用Redis读写分离时,需要注意的问题。
(1)延迟与不一致问题
前面已经讲到,由于主从复制的命令传播是异步的,延迟与数据的不一致不可避免。如果应用对数据不一致的接受程度程度较低,可能的优化措施包括:优化主从节点之间的网络环境(如在同机房部署);监控主从节点延迟(通过offset)判断,如果从节点延迟过大,通知应用不再通过该从节点读取数据;使用集群同时扩展写负载和读负载等。
在命令传播阶段以外的其他情况下,从节点的数据不一致可能更加严重,例如连接在数据同步阶段,或从节点失去与主节点的连接时等。从节点的slave-serve-stale-data参数便与此有关:它控制这种情况下从节点的表现;如果为yes(默认值),则从节点仍能够响应客户端的命令,如果为no,则从节点只能响应info、slaveof等少数命令。该参数的设置与应用对数据一致性的要求有关;如果对数据一致性要求很高,则应设置为no。
(2)数据过期问题
在单机版Redis中,存在两种删除策略:
- 惰性删除:服务器不会主动删除数据,只有当客户端查询某个数据时,服务器判断该数据是否过期,如果过期则删除。
- 定期删除:服务器执行定时任务删除过期数据,但是考虑到内存和CPU的折中(删除会释放内存,但是频繁的删除操作对CPU不友好),该删除的频率和执行时间都受到了限制。
在主从复制场景下,为了主从节点的数据一致性,从节点不会主动删除数据,而是由主节点控制从节点中过期数据的删除。由于主节点的惰性删除和定期删除策略,都不能保证主节点及时对过期数据执行删除操作,因此,当客户端通过Redis从节点读取数据时,很容易读取到已经过期的数据。
Redis 3.2以后,从节点在读取数据时,增加了对数据是否过期的判断:如果该数据已过期,则不返回给客户端;将Redis版本升级到3.2以后即可解决数据过期问题。
(3)故障切换问题
在没有使用哨兵的读写分离场景下,应用针对读和写分别连接不同的Redis节点;当主节点或从节点出现问题而发生更改时,需要及时修改应用程序读写Redis数据的连接;连接的切换可以手动进行,或者自己写监控程序进行切换,但前者响应慢、容易出错,后者实现复杂,成本都不算低。
(4)总结
在使用读写分离之前,可以考虑其他方法增加Redis的读负载能力:如尽量优化主节点(减少慢查询、减少持久化等其他情况带来的阻塞等)提高负载能力;使用Redis集群同时提高读负载能力和写负载能力等。如果使用读写分离,可以使用哨兵,使主从节点的故障切换尽可能自动化,并减少对应用程序的侵入。
复制超时问题
主从节点复制超时是导致复制中断的最重要的原因之一,下面单独说明超时问题,后面也会说明其他会导致复制中断的问题。
超时判断意义
在复制连接建立过程中及之后,主从节点都有机制判断连接是否超时,其意义在于:
(1)如果主节点判断连接超时,其会释放相应从节点的连接,从而释放各种资源,否则无效的从节点仍会占用主节点的各种资源(输出缓冲区、带宽、连接等);此外连接超时的判断可以让主节点更准确的知道当前有效从节点的个数,有助于保证数据安全(配合前面讲到的min-slaves-to-write等参数)。
(2)如果从节点判断连接超时,则可以及时重新建立连接,避免与主节点数据长期的不一致。
判断机制
主从复制超时判断的核心,在于repl-timeout参数,该参数规定了超时时间的阈值(默认60s),对于主节点和从节点同时有效;主从节点触发超时的条件分别如下:
(1)主节点:每秒1次调用复制定时函数replicationCron(),在其中判断当前时间距离上次收到各个从节点REPLCONF ACK的时间,是否超过了repl-timeout值,如果超过了则释放相应从节点的连接。
(2)从节点:从节点对超时的判断同样是在复制定时函数中判断,基本逻辑是:
- 如果当前处于连接建立阶段,且距离上次收到主节点的信息的时间已超过repl-timeout,则释放与主节点的连接;
- 如果当前处于数据同步阶段,且收到主节点的RDB文件的时间超时,则停止数据同步,释放连接;
- 如果当前处于命令传播阶段,且距离上次收到主节点的PING命令或数据的时间已超过repl-timeout值,则释放与主节点的连接。
需要注意的坑
下面介绍与复制阶段连接超时有关的一些实际问题:
(1)数据同步阶段:在主从节点进行全量复制bgsave时,主节点需要首先fork子进程将当前数据保存到RDB文件中,然后再将RDB文件通过网络传输到从节点。如果RDB文件过大,主节点在fork子进程+保存RDB文件时耗时过多,可能会导致从节点长时间收不到数据而触发超时;此时从节点会重连主节点,然后再次全量复制,再次超时,再次重连......这是个悲伤的循环。为了避免这种情况的发生,除了注意Redis单机数据量不要过大,另一方面就是适当增大repl-timeout值,具体的大小可以根据bgsave耗时来调整。
(2)命令传播阶段:如前所述,在该阶段主节点会向从节点发送PING命令,频率由repl-ping-slave-period控制;该参数应明显小于repl-timeout值(后者至少是前者的几倍)。否则,如果两个参数相等或接近,网络抖动导致个别PING命令丢失,此时恰巧主节点也没有向从节点发送数据,则从节点很容易判断超时。
(3)慢查询导致的阻塞:如果主节点或从节点执行了一些慢查询(如keys *或者对大数据的hgetall等),导致服务器阻塞;阻塞期间无法响应复制连接中对方节点的请求,可能导致复制超时。
复制中断问题
主从节点超时是复制中断的原因之一,除此之外,还有其他情况可能导致复制中断,其中最主要的是复制缓冲区溢出问题。
复制缓冲区溢出
前面说过,在全量复制阶段,主节点会将执行的写命令放到复制缓冲区中,该缓冲区存放的数据包括了以下几个时间段内主节点执行的写命令:bgsave生成RDB文件、RDB文件由主节点发往从节点、从节点清空老数据并载入RDB文件中的数据。当主节点数据量较大,或者主从节点之间网络延迟较大时,可能导致该缓冲区的大小超过了限制,此时主节点会断开与从节点之间的连接;这种情况可能引起全量复制->复制缓冲区溢出导致连接中断->重连->全量复制->复制缓冲区溢出导致连接中断......的循环。
复制缓冲区的大小由client-output-buffer-limit slave {hard limit} {soft limit} {soft seconds}配置,6.0.6版本中默认值为normal 0 0 0 slave 268435456 67108864 60 pubsub 33554432 8388608 60",其解释如下:
- 对于普通客户端来说,限制为0,也就是不限制。因为普通客户端通常采用阻塞式的消息应答模式,何谓阻塞式呢?如:发送请求,等待返回,再发送请求,再等待返回。这种模式下,通常不会导致Redis服务器输出缓冲区的堆积膨胀;
- 对于Pub/Sub客户端(也就是发布/订阅模式),大小限制是8M,当输出缓冲区超过8M时,会关闭连接。持续性限制是,当客户端缓冲区大小持续60秒超过2M,则关闭客户端连接;
- 对于slave客户端来说,大小限制是256M,持续性限制是当客户端缓冲区大小持续60秒超过64M,则关闭客户端连接。
上述三种规则都是可以修改的。可以通过CONFIG SET 命令设置立即生效或者直接修改redis.conf
需要注意的是,复制缓冲区是客户端输出缓冲区的一种,主节点会为每一个从节点分别分配复制缓冲区;而复制积压缓冲区则是一个主节点只有一个,无论它有多少个从节点。 注意两个缓冲区的区别。
各场景下复制的选择及优化技巧
在介绍了Redis复制的种种细节之后,现在我们可以来总结一下,在下面常见的场景中,何时使用部分复制,以及需要注意哪些问题。
(1)第一次建立复制
此时全量复制不可避免,但仍有几点需要注意:如果主节点的数据量较大,应该尽量避开流量的高峰期,避免造成阻塞;如果有多个从节点需要建立对主节点的复制,可以考虑将几个从节点错开,避免主节点带宽占用过大。此外,如果从节点过多,也可以调整主从复制的拓扑结构,由一主多从结构变为树状结构(中间的节点既是其主节点的从节点,也是其从节点的主节点);但使用树状结构应该谨慎:虽然主节点的直接从节点减少,降低了主节点的负担,但是多层从节点的延迟增大,数据一致性变差;且结构复杂,维护相当困难。
(2)主节点重启
主节点重启可以分为两种情况来讨论,一种是故障导致宕机,另一种则是有计划的重启。
主节点宕机
主节点宕机重启后,runid会发生变化,因此不能进行部分复制,只能全量复制。
实际上在主节点宕机的情况下,应进行故障转移处理,将其中的一个从节点升级为主节点,其他从节点从新的主节点进行复制;且故障转移应尽量的自动化,redis的哨兵模式便可以进行自动的故障转移。
安全重启:debug reload
在一些场景下,可能希望对主节点进行重启,例如主节点内存碎片率过高,或者希望调整一些只能在启动时调整的参数。如果使用普通的手段重启主节点,会使得runid发生变化,可能导致不必要的全量复制。
为了解决这个问题,Redis提供了debug reload的重启方式:重启后,主节点的runid和offset都不受影响,避免了全量复制。
如下所示,debug reload重启后runid和offset都未受影响:
bash
[root@k8s-m2 src]# ./redis-cli -h 192.168.2.141 -a 123456 info server|grep run_id
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
run_id:794a299f89057dbabb11de4649f9d04964e7efa4
#通过debug reload重启
[root@k8s-m2 src]# ./redis-cli -h 192.168.2.141 -a 123456 debug reload
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
OK
#查看run_id,未发生变化
[root@k8s-m2 src]# ./redis-cli -h 192.168.2.141 -a 123456 info server|grep run_id
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
run_id:794a299f89057dbabb11de4649f9d04964e7efa4
#重启redis,发现run_id发生变化
[root@k8s-m2 src]# systemctl restart redis
[root@k8s-m2 src]# ./redis-cli -h 192.168.2.141 -a 123456 info server|grep run_id
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
run_id:eeacc52f1285982ad7c38eda1a862db05ce7392e
[root@k8s-m2 src]#
但debug reload是一柄双刃剑:它会清空当前内存中的数据,重新从RDB文件中加载,这个过程会导致主节点的阻塞,因此也需要谨慎。
(3)从节点重启
从节点宕机重启后,其保存的主节点的runid会丢失,因此即使再次执行slaveof,也无法进行部分复制。
(4)网络中断
如果主从节点之间出现网络问题,造成短时间内网络中断,可以分为多种情况讨论。
第一种情况:网络问题时间极为短暂,只造成了短暂的丢包,主从节点都没有判定超时(未触发repl-timeout);此时只需要通过REPLCONF ACK来补充丢失的数据即可。
第二种情况:网络问题时间很长,主从节点判断超时(触发了repl-timeout),且丢失的数据过多,超过了复制积压缓冲区所能存储的范围;此时主从节点无法进行部分复制,只能进行全量复制。为了尽可能避免这种情况的发生,应该根据实际情况适当调整复制积压缓冲区的大小;此外及时发现并修复网络中断,也可以减少全量复制。
第三种情况:介于前述两种情况之间,主从节点判断超时,且丢失的数据仍然都在复制积压缓冲区中;此时主从节点可以进行部分复制。
复制相关的配置
这一节总结一下与复制有关的配置,说明这些配置的作用、起作用的阶段,以及配置方法等;通过了解这些配置,一方面加深对Redis复制的了解,另一方面掌握这些配置的方法,可以优化Redis的使用。
配置大致可以分为主节点相关配置、从节点相关配置以及与主从节点都有关的配置,下面分别说明。
(1)与主从节点都有关的配置
首先介绍最特殊的配置,它决定了该节点是主节点还是从节点:
-
slaveof :Redis启动时起作用;作用是建立复制关系,开启了该配置的Redis服务器在启动后成为从节点。该注释默认注释掉,即Redis服务器默认都是主节点。
-
repl-timeout 60:与各个阶段主从节点连接超时判断有关,见前面的介绍。
(2)主节点相关配置
-
repl-diskless-sync no:作用于全量复制阶段,控制主节点是否使用diskless复制(无盘复制)。所谓diskless复制,是指在全量复制时,主节点不再先把数据写入RDB文件,而是直接写入slave的socket中,整个过程中不涉及硬盘;diskless复制在磁盘IO很慢而网速很快时更有优势。需要注意的是, repl-diskless-sync no参数默认是关闭的。
-
repl-diskless-sync-delay 5:该配置作用于全量复制阶段,当主节点使用diskless复制时,该配置决定主节点向从节点发送之前停顿的时间,单位是秒;只有当diskless复制打开时有效,默认5s。之所以设置停顿时间,是基于以下两个考虑:(1)向slave的socket的传输一旦开始,新连接的slave只能等待当前数据传输结束,才能开始新的数据传输 (2)多个从节点有较大的概率在短时间内建立主从复制。
-
client-output-buffer-limit normal 0 0 0 slave 268435456 67108864 60 pubsub 33554432 8388608 60:与全量复制阶段主节点的缓冲区大小有关,见前面的介绍。
-
repl-disable-tcp-nodelay no:与命令传播阶段的延迟有关,见前面的介绍。
-
masterauth :与连接建立阶段的身份验证有关,见前面的介绍。
-
repl-ping-slave-period 10:与命令传播阶段主从节点的超时判断有关,见前面的介绍。
-
repl-backlog-size 1mb:复制积压缓冲区的大小,见前面的介绍。
-
repl-backlog-ttl 3600:当主节点没有从节点时,复制积压缓冲区保留的时间,这样当断开的从节点重新连进来时,可以进行部分复制;默认3600s。如果设置为0,则永远不会释放复制积压缓冲区。
-
min-slaves-to-write 0与min-slaves-max-lag 10:规定了主节点的最小从节点数目,及对应的最大延迟,见前面的介绍。
(3)从节点相关配置
-
slave-serve-stale-data yes:与从节点数据陈旧时是否响应客户端命令有关,见前面的介绍。
-
slave-read-only yes:从节点是否只读;默认是只读的。由于从节点开启写操作容易导致主从节点的数据不一致,因此该配置尽量不要修改。
单机内存大小限制
在 前面章节我们说到fork操作对Redis单机内存大小的限制。实际上在Redis的使用中,限制单机内存大小的因素非常之多,下面总结一下在主从复制中,单机内存过大可能造成的影响:
(1)切主:当主节点宕机时,一种常见的容灾策略是将其中一个从节点提升为主节点,并将其他从节点挂载到新的主节点上,此时这些从节点只能进行全量复制;如果Redis单机内存达到10GB,一个从节点的同步时间在几分钟的级别;如果从节点较多,恢复的速度会更慢。如果系统的读负载很高,而这段时间从节点无法提供服务,会对系统造成很大的压力。
(2)从库扩容:如果访问量突然增大,此时希望增加从节点分担读负载,如果数据量过大,从节点同步太慢,难以及时应对访问量的暴增。
(3)缓冲区溢出:(1)和(2)都是从节点可以正常同步的情形(虽然慢),但是如果数据量过大,导致全量复制阶段主节点的复制缓冲区溢出,从而导致复制中断,则主从节点的数据同步会全量复制->复制缓冲区溢出导致复制中断->重连->全量复制->复制缓冲区溢出导致复制中断......的循环。
(4)超时:如果数据量过大,全量复制阶段主节点fork+保存RDB文件耗时过大,从节点长时间接收不到数据触发超时,主从节点的数据同步同样可能陷入全量复制->超时导致复制中断->重连->全量复制->超时导致复制中断......的循环。
此外,主节点单机内存除了绝对量不能太大,其占用主机内存的比例也不应过大:最好只使用50%-65%的内存,留下30%-45%的内存用于执行bgsave命令和创建复制缓冲区等。
info Replication
在Redis客户端通过info Replication可以查看与复制相关的状态,对于了解主从节点的当前状态,以及解决出现的问题都会有帮助。
bash
[root@k8s-m2 src]# ./redis-cli -h 192.168.2.141 -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
192.168.2.141:6379> INFO replication
# Replication
role:master
connected_slaves:1
slave0:ip=192.168.2.140,port=6379,state=online,offset=2156,lag=1
master_failover_state:no-failover
master_replid:b621a3eb35dce37b7542a440b7ccc92aea16ffec
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:2156
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:2156
192.168.2.141:6379>
从节点:
bash
192.168.2.140:6379> INFO replication
# Replication
role:slave
master_host:192.168.2.141
master_port:6379
master_link_status:up
master_last_io_seconds_ago:7
master_sync_in_progress:0
slave_repl_offset:2212
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:b621a3eb35dce37b7542a440b7ccc92aea16ffec
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:2212
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:2212
192.168.2.140:6379>
对于从节点,上半部分展示的是其作为从节点的状态,从connectd_slaves开始,展示的是其作为潜在的主节点的状态。
总结
下面回顾一下本文的主要内容:
1、主从复制的作用:宏观的了解主从复制是为了解决什么样的问题,即数据冗余、故障恢复、读负载均衡等。
2、主从复制的操作:即slaveof命令。
3、主从复制的原理:主从复制包括了连接建立阶段、数据同步阶段、命令传播阶段;其中数据同步阶段,有全量复制和部分复制两种数据同步方式;命令传播阶段,主从节点之间有PING和REPLCONF ACK命令互相进行心跳检测。
4、应用中的问题:包括读写分离的问题(数据不一致问题、数据过期问题、故障切换问题等)、复制超时问题、复制中断问题等,然后总结了主从复制相关的配置,其中repl-timeout、client-output-buffer-limit slave等对解决Redis主从复制中出现的问题可能会有帮助。
主从复制虽然解决或缓解了数据冗余、故障恢复、读负载均衡等问题,但其缺陷仍很明显:故障恢复无法自动化;写操作无法负载均衡;存储能力受到单机的限制;这些问题的解决,需要通过其他模式价格如哨兵和集群进行解决。后续会慢慢介绍。
更多关于redis的知识分享,请前往博客主页。编写过程中,难免出现差错,敬请指出