Redis之主从复制
复制功能是Redis高可用的基础,Redis提供了主从复制的功能,实现了相同数据的多个Redis副本,从而解决分布式环境下的单点问题以及故障恢复和负载均衡等需求。
创建复制
创建主从复制有以下几种方式,一种是在redis服务启动之前在配置文件中配置好直接启动,第二种是启动redis服务时的启动命令,第三种是在启动以后使用命令行的方式创建
配置文件方式
编辑redis.conf文件,修改replicaof
参数的值为主节点的ip地址和端口号,主节点有密码的还需要设置masterauth
参数的值为主节点密码
replicaof 192.168.216.128 6379
masterauth 123456
修改以上参数值以后使用指定配置文件启动redis即可,redis启动以后可以在日志中看到如下的提示信息
启动命令时配置
使用redis-server
命令启动redis时,添加replicaof
参数即可,如下所示:
$ redis-server --port 6379 --replicaof 192.168.216.128 6379
命令行配置
在redis-cli
客户端中执行slaveof
命令:
$ redis-cli> slaveof 192.168.216.128 6379
使用以上任意一种方式都可以创建主从复制,可以根据具体情况择优使用,需要注意的一点就是以上的命令或者修改配置文件都是以从节点为执行节点的,也就是要在从节点上执行。
主从创建成功以后,可以通过info Replication
查看主从情况,比如我有一个一主两从的结构,查询结果如下:
主节点:
从节点:
主从拓扑结构
Redis的主从拓扑结构按照复杂度可以分为一主一从、一主多从以及树状主从结构,一主一从以及一主多从很容易理解,重点要看一下树状主从结构,拓扑结构图如下所示:
从上图可以看出来,树状主从是将一部分从节点作为另外一部分的从节点的主节点从而形成树形结构,那为什么要使用树状主从,直接使用一主多从不是更简单吗?这个主要是为了避免影响主节点的性能,这个等后面我们说到复制原理的时候再具体说。
断开复制
断开复制在不停止服务的情况下,同样适用slaveof
命令,将主从断开,将从节点升级为主节点,在从节点中执行如下命令:
$ redis-cli> slaveof no one
执行以上命令后,可以看到对应的redis日志中出现以下内容:
意思就是说已失去主节点的连接,master模式已启动,此时在主节点执行info Replication
命令就可以发现从节点已经不存在了。
slaveof host port是创建主从,slaveof no one是断开主从,两者结合可以实现切换主节点的功能,但是有一点需要注意,在切换主节点后,当前节点的历史数据就会被清空,然后再从新的主节点全量复制新的数据
数据复制流程
执行slaveof命令或者使用指定配置文件启动从节点以后,从主节点到从节点的复制流程就开始了,复制流程图如下所示
- 从节点执行slaveof 主节点host 主节点port命令后,在redis会打印如下所示的日志信息:
* REPLICAOF 192.168.216.129:6379 enabled (user request from 'id=4 addr=127.0.0.1:58476 fd=9 name= age=142 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=49 qbuf-free=32719 argv-mem=26 obl=0 oll=0 omem=0 tot-mem=61490 events=r cmd=slaveof use
r=default')
slaveof是异步命令,执行完该命令后从节点保存主节点的host和port信息,但是并未真正开始复制;
- 创建socket连接,从节点内部定时每秒执行一次复制定时函数
replicationCron
,当发现存在可以连接主节点时就会根据主节点的信息创建socket连接,如果节点无法连接就会无限重试或者直到执行slaveof no one命令,此时redis会打印如下所示日志:
* Connecting to MASTER 192.168.216.129:6379
* MASTER <-> REPLICA sync started
此时主节点会给从节点的socket连接创建客户端状态并将其当作主节点上的一个客户端,使用client list
命令是可以明确看到这个客户端的,如下所示:
3. 从节点发送ping命令,等待主节点回复pong,用以检测socket是否可用以及主节点是否可接受处理命令,如果从节点接收响应超时或者接受到pong以外的响应,从节点就会断开复制链接,等待下次定时任务时再发起重连,日志如下:
* Master replied to PING, replication can continue...
-
masterauth验证,如果主节点设置了密码,那么此时需要验证密码才可以进行下一步操作,密码验证失败的话会断开连接,等待下一次重连;
-
数据同步,数据同步其实就是从节点的初始化的过程,数据同步包含全量同步以及部分同步,同步的过程后面具体分析,另外要注意一点的是在数据同步阶段主节点需要主动向从节点发送请求,因此此时主从节点互为客户端,数据同步对应的日志如下所示:
* Trying a partial resynchronization (request 6c4167e7160b6bb316de536f24a93b1f260b2f10:1).
* Full resync from master: ac0f31e4a32d99d77c2007009c312868023713a9:840612
* Discarding previously cached master state.
* MASTER <-> REPLICA sync: receiving 269 bytes from master to disk
* MASTER <-> REPLICA sync: Flushing old data
* MASTER <-> REPLICA sync: Loading DB in memory
* Loading RDB produced by version 6.0.9
* RDB age 0 seconds
* RDB memory usage when created 1.85 Mb
* MASTER <-> REPLICA sync: Finished with success
- 持续性复制,经过前面五个步骤,正常情况下主从已经创建成功,之后主节点会源源不断的将写命令发送到从节点,从而保证主从一致性;
全量复制
全量复制是一个比较重型的操作,需要将主节点上的全部数据复制到从节点上,用于初次复制或者无法进行部分复制的情况,在实际使用中应该尽量避免全量复制。
《Redis开发和运维》以及查询的资料中看到的都是初次执行slaveof或者之前执行过slaveof no one的节点在第一次数据同步时会发送psync-1,主节点根据psync-1解析出是全量复制,从而开启全量复制,但是在6.0.9中测试时发送的是复制id:1
全量复制的流程(以打印的日志来分析):
- 从节点向主节点发送psync请求,对应日志如下:
* Trying a partial resynchronization (request 56530e3cbd0c37b80143315ef8ef535172eab781:1).
- 主节点接收到从节点的psync请求,判断是否是全量同步,如果是就响应从节点Full resync,主节点对应日志如下:
# 部分复制没有被接受,复制id不匹配
* Partial resynchronization not accepted: Replication ID mismatch (Replica asked for '56530e3cbd0c37b80143315ef8ef535172eab781', my replication IDs are 'c6a9a35ad63a0ce35b6574641a3d01bc3f586f7f' and '0000000000000000000000000000000000000000'
)
* Replication backlog created, my new replication IDs are 'adbec8fcdbcafeb97c46e1b8cd209b6121b353d8' and '0000000000000000000000000000000000000000'
此时从节点接收到主节点发出的全量复制响应,包含新的复制id以及offset值:
* Full resync from master: adbec8fcdbcafeb97c46e1b8cd209b6121b353d8:0
- 主节点执行bgsave生成rdb文件,关于生成rdb文件的细节请参考拙作redis之持久化,主节点日志如下:
* Starting BGSAVE for SYNC with target: disk
* Background saving started by pid 21
* DB saved on disk
* RDB: 6 MB of memory used by copy-on-write
* Background saving terminated with succeed
- 主节点把生成的rdb文件发送给从节点,从节点把接收到的rdb文件直接作为自己的数据文件,对应从节点日志如下:
* MASTER <-> REPLICA sync: receiving 175 bytes from master to disk
注意:此处是为了演示,所以生成的rdb文件较小,但是正式环境的数据量较大特别是超过6GB时数据传输就会比较依赖宽带了,如果数据传输的时间超过配置的
repl-timeout
默认的60s就会导致从节点放弃接收的rdb文件并清理已下载的rdb文件,导致全量复制失败,因此在实际使用过程中要根据实际情况适当调大该参数的值,防止超时导致的复制失败。
- 从节点在接收rdb文件时,主节点仍然在继续响应其他客户端的命令,此时主节点会将这个过程中的写命令对应的数据写入到复制缓冲区中,当从节点加载完rdb文件后再把复制缓冲区中的数据发给从节点,从而达到主从一致性的目的。
注意:
复制缓冲区默认配置是client-output-buffer-limit replica 256mb 64mb 60,也就是60s内缓冲区持续消耗64m或者直接超过256m时,主节点将直接关闭复制客户端连接,导致全量同步失败,因此在高流量写入的情况下全量复制比较容易导致缓冲区溢出,从而导致复制失败,这个也要根据实际情况要调整参数大小。
- 从节点接收完成rdb文件后,清空自身原有数据,日志如下
* MASTER <-> REPLICA sync: Flushing old data
- 开始加载rdb文件中的数据,对于较大的rdb文件,加载也比较耗时,对应日志如下
* MASTER <-> REPLICA sync: Loading DB in memory
* RDB age 0 seconds
* RDB memory usage when created 1.85 Mb
* MASTER <-> REPLICA sync: Finished with success
- rdb文件加载完以后从节点的状态就变成了主节点执行bgsave时的状态了,此时再把复制缓冲区的写命令发送给从节点,从节点执行完成后状态就和主节点一致了。
- 上述步骤执行完以后,如果从节点开启了aof,那么就会立刻执行bgrewriteaof操作,这是为了保证全量复制以后aof持久化文件立刻可用;
经过以上9个步骤就完成了全量复制,可以发现全量复制在主节点要执行bgsave,还要把生成的rdb文件网络传输,到了从节点还要加载rdb文件,甚至可能要进行aof文件重写,这些都是比较耗费资源的操作,因此还是要尽可能地减少全量复制。
前面拓扑结构中提到不建议使用一主多从的拓扑结构,其实就是基于这个考虑,多个从节点同时对一个主节点进行全量复制,虽然redis能够使多个从节点复用一个rdb文件,但是rdb文件向多个从节点发送以及在写并发较高时,主节点需要向多个从节点发送消息从而浪费大量的网络带宽,同时也加重了主节点的负载从而影响主节点的稳定性。
部分复制
部分复制是为了解决全量复制开销过大的一种优化措施,当从节点复制主节点数据过程中,如果出现网络中断或者命令丢失等异常情况,此时从节点可以向主节点要求补发丢失的命令数据,如果主节点的复制缓冲区中刚好存在这一部分数据,那就直接发送给从节点以此来保证主从一致性,在了解部分复制之前我们需要先知道三个概念:
runid
runid就是主节点的运行id,是redis启动时随机分配的一个40位的十六进制字符串,运行id用来唯一识别redis节点,从节点保存主节点的runid从而知道自己需要从哪个节点来复制数据。
之所以使用runid而不是使用host+port的方式是因为一旦aof或者rdb文件发生改变并重启了redis服务,那么从节点再基于偏移量(offset)去复制数据是不安全的。
那么已经构建的主从架构,如果要主节点出现故障需要重启怎么办呢?可以使用slaveof no one
先将从节点升级为主节点,待真正的主节点重启完成后再使用slaveof重新创建主从,当然这是一种很low的做法,高级的一点做法可以使用哨兵或者集群等高可用方案。
offset
offset是复制偏移量,表示主节点向从节点传递的字节数,主节点每次向从节点传递N个字节数据时,主节点的复制偏移量增加N,从节点从主节点接收N个字节数据时,从节点的复制偏移量增加N。
复制偏移量可以用来判断主从节点的一致性,如果两者复制偏移量相同,那么就是主从一致,如果主节点偏移量大于从节点偏移量,且远远大于,那么此时可能出现了网络延迟或者命令阻塞,主节点的偏移量比从节点偏移量大的部分就存在于复制缓冲区中,当从节点请求部分复制时就从复制缓冲区中获取到对应偏移量的数据传递给从节点。
复制缓冲区
顾名思义,复制缓冲区就是一个缓冲区,它是保存在主节点上一个固定长度、先进先出的队列,默认大小时1M,当主节点存在从节点时,不管是几个从节点,主节点都会将写命令发送给从节点的同时缓存到复制缓冲区中,当缓冲区占满时就会将最先进入缓冲区的数据挤出缓冲区。
当主从节点断开重连时,从节点带着offset请求复制,主节点判断从节点的offset是否存在于缓冲区中,如果存在,那么就进行部分复制,将缓冲区中的数据直接返回给从节点,如果从节点传递的offset已经超过缓冲区中的offset的值,那么就需要开启全量复制。基于此,要根据具体的业务需求调整复制缓冲区的大小,尽可能地使用部分复制。
部分复制实例
我在两台机器上分别安装了一个redis,其中一个作为另外一个的从节点,从节点:
主节点:
为了验证部分复制,现在将两台机器之间的网络断开(简单粗暴的办法,拔网线),经过一会儿之后查看主从节点的日志,可以看到各自都出现了lost,如下所示:
# 主节点日志
# Connection with slave 10.18.30.178:6379 lost.
# 从节点日志
1:S # MASTER timeout: no data nor PING received...
1:S # Connection with master lost.
1:S * Caching the disconnected master state.
1:S * Connecting to MASTER 10.18.30.34:6379
1:S * MASTER <-> REPLICA sync started
1:S # Error condition on socket for SYNC: No route to host
1:S * Connecting to MASTER 10.18.30.34:6379
在主从节点各自丢失对方的连接时,在主节点上执行写操作(随便写入一些数据),如下所示:
经过一段时间以后,重新连通主从节点之间的网络,此时,在从节点的日志中可以看到如下所示的内容:
1:S * Connecting to MASTER 10.18.30.34:6379 # 连接上了主节点
1:S * MASTER <-> REPLICA sync started # 主从节点开始同步
1:S * Non blocking connect for SYNC fired the event.
1:S * Master replied to PING, replication can continue... # 主节点回复了ping,主从可以继续
1:S * Trying a partial resynchronization (request eb32e34c84e7123e7ddb6ae1fab5e348bc58af31:1234). # 开始部分复制,offset是1234
1:S * Successful partial resynchronization with master. # 部分复制成功
从上面的日志可以看出来,从节点连接上主节点之后开始尝试部分复制,并且最后部分复制成功。我们再看一下此时主节点的日志:
* Slave 10.18.30.178:6379 asks for synchronization # 从节点请求同步
* Partial resynchronization request from 10.18.30.178:6379 accepted. Sending 444 bytes of backlog starting from offset 1234. # 接受从节点的部分复制请求,从1234的offset位置发送444bytes的数据
也就是主节点此时判断这次的同步请求符合部分复制,那么就从对应的offset位置发送数据给从节点,如果此时offset不在复制缓冲区的范围内,那么开启的就是全量复制,而不是部分复制了。
另外,如果部分复制完成后aof文件达到了auto-aof-rewrite-min-size
以及auto-aof-rewrite-percentage
的要求,那么此时就会触发aof的重写。
总结一下重写的过程,就是如下几个步骤:
- 主从节点网络中断,超过
repl-timeout
时间后,主节点认为从节点故障,打印lost日志; - 主从断开期间,主节点继续响应请求,会将写命令缓存到大小为1M的复制缓冲区;
- 主从网络恢复后,从节点会再次连接上主节点,打印主从可以继续的日志;
- 主从恢复后,从节点使用psync请求,带着保存好的主节点运行id以及自身已经复制的偏移量去请求进行复制;
- 主节点接收到从节点的psync请求后,首先判断runID是否一直,不一致的话表示之前复制的不是当前主节点,需要重新开始全量复制,一致的话再查找请求offset是否存在于复制缓冲区中,不存在的话同样要开启全量复制;
- runID和offset都符合部分复制的要求后,主节点会把复制缓冲区中相应的数据发送给从节点,保证主从进入正常复制;