Redis的主从复制篇来啦,本篇咱们来详细了解一下Redis的复制功能。

前言

在分布式系统中为了解决单点问题,通常会把数据复制多个副本部署到其他机器,满足故障恢复和负载均衡等需求。Redis也是如此,它为我们提供了复制功能,实现了相同数据的多个Redis副本。

Redis复制概述

Redis复制是主机数据更新后根据配置和策略,自动会以异步复制的方式,同步数据到备机的机制;复制过程中并不会阻塞复制涉及的Redis节点,他们都能继续处理其他命令的请求。其实复制本质上沿用了RDB持久化的流程,在主节点收到命令后执行bgsave生成RDB文件,然后将RDB文件送给另一个Redis节点使用。

参与复制的Redis实例划分为主节点(master)和从节点(slave)。默认情况下,Redis都是主节点。每个从节点只能有一个主节点,而主节点可以同时具有多个从节点。复制的数据流是单向的,只能由主节点复制到从节点。

由于复制涉及到了主从节点的概念,所以也可以称为主从复制,但是,主从复制的概念和主从集群还是有些差别的,我们下一篇集群中就会讲到主从集群。

Redis复制的用途

(1)读写分离:主节点提供写服务,从节点提供读服务;提高Redis服务器的并发量。
(2)容灾恢复:当主节点发生故障无法服务,可以由从节点继续提供服务,实现故障的快速恢复。
(3)数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
(4)高可用的基石:主从复制还是哨兵和主从集群能够实施的基础,因此说主从复制是Redis高可用的基础。

Redis主从复制操作

开启主从复制,一般有以下三种方式:

(1)配置文件中配置:slaveof masterip masterport在该配置文件中加入这个,指定成为谁的从机。
(2)Redis服务启动时配置:redis-server redis.conf --slaveof masterip masterport;在服务启动的时候指定成为谁的从机。
(3)Redis服务启动后通过客户端配置:slaveof masterip masterport:输入该命令后,本机成为从机,本机原本的数据也会不存在,跟主机的数据保持一致。

关闭主从复制:

slaveof no one:在从机上输入该命令,使本机成为主数据库,也就结束了与其他数据库的同步。

Redis主从复制实现过程

Redis主从复制过程大致分为三个步骤:

(1)连接建立阶段:主从节点之间建立连接,为数据同步做好准备。
(2)数据同步阶段:是主从复制最核心的阶段,根据主从节点当前状态的不同,可以分为全量复制和部分复制。
(3)命令传播阶段:在这个阶段主节点将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性。

在本次例子中我使用了第三种方式创建主从复制,也就是连接客户端后输入slaveof masterip masterport命令去复制,输入完这个命令后,Redis的日志如下,里面记录了在这之后具体发生了什么事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
27962:S 23 Apr 12:09:11.774 * Before turning into a slave, using my master parameters to synthesize a cached master: I may be able to synchronize with the new master with just a partial transfer.
#保存主节点的信息
27962:S 23 Apr 12:09:11.776 * SLAVE OF 127.0.0.1:26379 enabled (user request from 'id=4 addr=127.0.0.1:26461 fd=7 name= age=43 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=slaveof')
#建立socket连接
27962:S 23 Apr 12:09:12.596 * Connecting to MASTER 127.0.0.1:26379
27962:S 23 Apr 12:09:12.598 * MASTER <-> SLAVE sync started
#通过ping命令检测socket连接是否可用
27962:S 23 Apr 12:09:12.598 * Non blocking connect for SYNC fired the event.
27962:S 23 Apr 12:09:12.598 * Master replied to PING, replication can continue...
27962:S 23 Apr 12:09:12.599 * Trying a partial resynchronization (request 21fa80655747f5e0448ae8449ebf7c61664304cb:1).
27962:S 23 Apr 12:09:12.601 * Full resync from master: cc4cce78d7d21fe6d93ce00825db1274c8b256f1:4606
27962:S 23 Apr 12:09:12.601 * Discarding previously cached master state.
#从机接收到来自主机的197比特的数据
27962:S 23 Apr 12:09:12.731 * MASTER <-> SLAVE sync: receiving 197 bytes from master
#从节点清除旧数据
27962:S 23 Apr 12:09:12.731 * MASTER <-> SLAVE sync: Flushing old data
#从节点把RDB中的数据载入内存
27962:S 23 Apr 12:09:12.732 * MASTER <-> SLAVE sync: Loading DB in memory
27962:S 23 Apr 12:09:12.732 * MASTER <-> SLAVE sync: Finished with success

连接建立阶段

根据上面的日志可以看到,连接建立也分为几个步骤:
(1)保存主节点信息:

1
27962:S 23 Apr 12:09:11.776 * SLAVE OF 127.0.0.1:26379 enabled (user request from 'id=4 addr=127.0.0.1:26461 fd=7 name= age=43 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=slaveof')

(2)建立socket连接:

1
2
27962:S 23 Apr 12:09:12.596 * Connecting to MASTER 127.0.0.1:26379
27962:S 23 Apr 12:09:12.598 * MASTER <-> SLAVE sync started

(3)通过ping检测连接是否可用:

1
2
27962:S 23 Apr 12:09:12.598 * Non blocking connect for SYNC fired the event.
27962:S 23 Apr 12:09:12.598 * Master replied to PING, replication can continue...

如果还配置了身份验证的话,从节点还需要向主节点进行身份验证。

数据同步阶段

主从节点之间的连接建立以后,便可以开始进行数据同步,该阶段可以理解为从节点数据的初始化。具体执行的方式是:从节点向主节点发送psync命令(Redis2.8以前是sync命令),开始同步。

数据同步阶段是主从复制最核心的阶段,根据主从节点当前状态的不同,可以分为全量复制部分复制,下面会详细了解这两种复制方式以及psync命令的执行过程;在本例中,是初次复制的场景,属于全量复制。

需要注意的是,在数据同步阶段之前,从节点是主节点的客户端,主节点不是从节点的客户端;而到了这一阶段及以后,主从节点互为客户端。原因在于:在此之前,主节点只需要响应从节点的请求即可,不需要主动发请求,而在数据同步阶段和后面的命令传播阶段,主节点需要主动向从节点发送请求(如推送缓冲区中的写命令),才能完成复制。

实际上psync命令运行就需要下面这几个组件的支持:

(1)复制偏移量:

参与复制的主从节点都会维护自身复制偏移量。主节点(master)在处理完写入命令后,会把命令的字节长度做累加记录,统计信息在info relication中的master_repl_offset指标中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
127.0.0.1:26379> info Replication
# Replication
role:master
connected_slaves:1
# 从节点的偏移量
slave0:ip=127.0.0.1,port=16379,state=online,offset=7826,lag=1
master_replid:06229f3457d2630cbf179ae725c6148489245a95
master_replid2:0000000000000000000000000000000000000000
#主节点的偏移量
master_repl_offset:7826
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:7826

从节点(slave)每秒钟上报自身的复制偏移量给主节点,因此主节点也会保存从节点的复制偏移量,指标如下:

1
slave0:ip=127.0.0.1,port=16379,state=online,offset=7826,lag=1

从节点在接收到主节点发送的命令后,也会累加记录自身的偏移量。统计信息在从节点的info relication中的slave_repl_offset指标中:

1
2
3
4
127.0.0.1:16379> info Replication
# Replication
role:slave
slave_repl_offset:8120

(2)复制积压缓冲区

复制积压缓冲区是保存在主节点上的一个固定长度的队列,默认大小为 1MB,当主节点有连接的从节点(slave)时被创建,这时主节点(master)响应写命令时,不但会把命令发送给从节点,还会写入复制积压缓冲区。

由于缓冲区本质上是先进先出的定长队列,所以能实现保存最近已复制数据的功能,用于部分复制和复制命令丢失的数据补救。复制缓冲区相关统计信息保存在主节点的info replication中:

1
2
3
4
5
6
7
8
#1 代表开启复制缓冲区,当主节点有连接的从节点(slave)时自动被创建
repl_backlog_active:1
#缓冲区最大长度
repl_backlog_size:1048576
#起始偏移量
repl_backlog_first_byte_offset:1
#已保存数据的有效长度
repl_backlog_histlen:8344

(3)主节点运行ID

每个Redis节点启动后都会动态分配一个40位的十六进制字符串作为运行ID。运行ID的主要作用是用来唯一识别Redis节点,比如从节点保存主节点的运行ID识别自己正在复制的是哪个主节点。如果只使用ip+port的方式识别主节点,那么主节点重启变更了整体数据集(如替换RDB/AOF文件),从节点再基于偏移量复制数据将是不安全的,因此当运行ID变化后从节点将做全量复制。可以运行info server命令查看当前节点的运行ID:

1
2
3
4
127.0.0.1:16379> info Server
# Server
#主节点运行ID
run_id:b8eac4a5b10bdf8703ede30f7e2c8a9ef1e84a72

psync命令的执行过程

从节点会使用psync命令完成全量复制和部分复制功能,命令格式:
psync {runId} {offset},参数含义如下:
runId:从节点所复制主节点的运行id。
offset:当前从节点已复制的数据偏移量。

**psync命令执行流程: **

(1)从节点(slave)发送psync命令给主节点,参数runId是当前从节点保存的主节点运行ID,如果没有则默认值为,参数offset是当前从节点保存的复制偏移量,如果是第一次参与复制则默认值为-1。
(2)主节点(master)根据psync参数和自身数据情况决定响应结果:

  • 如果回复+FULLRESYNC {runId} {offset},那么从节点将触发全量复制流程。
  • 如果回复+CONTINUE,从节点将触发部分复制流程。
  • 如果回复+ERR,说明主节点版本低于Redis 2.8,无法识别psync命令,从节点将发送旧版的sync命令触发全量复制流程。

全量复制

全量复制一般用于初次复制场景,Redis早期支持的复制功能只有全量复制,它会把主节点全部数据一次性发送给从节点,当数据量较大时,会对主从节点和网络造成很大的开销。

在Redis >= 2.8版本后,触发全量复制的的命令是psync,这里也主要介绍psync全量复制的流程:

(1)从节点发送psync命令进行数据同步,由于是第一次进行复制,从节点没有复制偏移量和主节点的运行ID,所以发送psync -1。
(2)主节点根据psync -1解析出当前为全量复制,回复FULL RESYNC响应。
(3)从节点接收主节点的响应数据保存运行ID和偏移量offset,执行到当前步骤时从节点打印如下日志:

1
2
27962:S 23 Apr 12:09:12.599 * Trying a partial resynchronization (request 21fa80655747f5e0448ae8449ebf7c61664304cb:1).
27962:S 23 Apr 12:09:12.601 * Full resync from master: cc4cce78d7d21fe6d93ce00825db1274c8b256f1:4606

(4)主节点回复FULL RESYNC响应后开始执行bgsave保存RDB文件到本地,并使用一个缓冲区(称为复制缓冲区)记录从现在开始执行的所有写命令。
(5)主节点的bgsave完成后,发送RDB文件给从节点,从节点把接收的RDB文件保存在本地并直接作为从节点的数据文件,接收完RDB后从节点打印相关日志,可以在日志中查看主节点发送的数据量:

1
27962:S 23 Apr 12:09:12.731 * MASTER <-> SLAVE sync: receiving 197 bytes from master

(6)对于从节点开始接收RDB快照到接收完成期间,主节点仍然响应读和写命令,因此主节点会把这期间写命令数据保存在复制客户端缓冲区内,当从节点加载完RDB文件后,主节点再把缓冲区内的数据发送给从节点,这样就保证主从之间数据一致性。
(7)从节点接收完主节点传送来的全部数据后会先清空自身旧数据,然后开始加载RDB文件,对于较大的RDB文件,这一步操作依然比较耗时。
(9)从节点成功加载完RDB后,如果当前节点开启了AOF持久化功能, 它会立刻做bgrewriteaof操作,为了保证全量复制后AOF持久化文件立刻可用。

了解完全量复制的所有流程,大家会发现全量复制是一个非常耗时费力的操作。它的时间开销主要包括:

(1)主节点bgsave进行RDB持久化的时间,该过程是非常消耗CPU、内存(页表复制)、硬盘IO的。
(2)RDB文件网络传输时间,对主从节点的带宽都会带来很大的消耗。
(3)从节点清空数据、加载RDB的时间。
(4)可能的AOF重写时间。

例如我们数据量在6G左右的主节点,从节点发起全量复制的总耗时在2分钟左右。因此当数据量达到一定规模之后,由于全量复制过程中将 进行多次持久化相关操作和网络数据传输,这期间会大量消耗主从节点所在服务器的CPU、内存和网络资源。

所以除了第一次复制时采用全量复制在所难免之外,对于其他场景应该规避全量复制的发生。正因为全量复制的成本问题,Redis实现了部分复制功能。

部分复制

部分复制主要是Redis针对全量复制的过高开销做出的一种优化措施,用于处理在主从复制中因网络闪断等原因造成的数据丢失场景,使用psync {runId} {offset}命令实现。

当从节点(slave)正在复制主节点 (master)时,如果出现网络闪断或者命令丢失等异常情况时,从节点会向 主节点要求补发丢失的命令数据,如果主节点的复制积压缓冲区内存在这部分数据则直接发送给从节点,这样就可以保持主从节点复制的一致性。因为补发的数据远远小于全量数据,可以有效避免全量复制的过高开销。Redis 2.8版本开始提供部分复制,因此当使用复制功能时,尽量采用2.8以上版本的Redis。

部分复制的流程:

(1)当主从节点之间网络出现中断时,如果超过配置文件中配置的repl-timeout时间,主节点会认为从节点故障并中断复制连接。
(2)主从连接中断期间主节点依然响应命令,但因复制连接中断命令无法发送给从节点,不过主节点内部存在的复制积压缓冲区,依然可以保存最近一段时间的写命令数据,默认最大缓存大小为1MB。
(3)当主从节点网络恢复后,从节点会再次连上主节点。

(4)当主从连接恢复后,由于从节点之前保存了自身已复制的偏移量和主节点的运行ID。因此会把它们当作psync参数(上面已经讲过psync命令参数)发送给主节点,要求进行部分复制操作。
(5)主节点接到psync命令后首先核对参数runId是否与自身一致,如果一致,说明之前复制的是当前主节点;之后根据参数offset在自身复制积压缓冲区查找,如果偏移量之后的数据存在缓冲区中,则对从节点发送+CONTINUE响应,表示可以进行部分复制。
(6)主节点根据偏移量把复制积压缓冲区里的数据发送给从节点,保证主从复制进入正常状态。

在实际流程中,部分复制传递的数据远远小于全量数据,所以开销很小。

命令传播阶段

在命令传播阶段,除了发送写命令,主从节点之间维护着长连接并彼此发送心跳命令;
主节点向从节点发送ping命令,从节点回复主节点replconf ack {offset} 。心跳机制对于主从复制的超时判断、数据安全等有作用。

主从心跳判断机制:

(1)主从节点彼此都有心跳检测机制,各自模拟成对方的客户端进行通信,通过client list命令查看复制相关客户端信息,主节点的连接状态为 flags=M,从节点连接状态为flags=S。
(2)主节点默认每隔10秒对从节点发送ping命令,判断从节点的存活性和连接状态。可通过参数repl-ping-slave-period控制发送频率,单位是秒。
(3)从节点在主线程中每隔1秒发送replconf ack {offset}命令,给主节点上报自身当前的复制偏移量。

replconf命令还有其他作用,主要作用如下:

(1)实时监测主从节点网络状态。
(2)上报自身复制偏移量,检查复制数据是否丢失,如果从节点数据丢失,再从主节点的复制缓冲区中拉取丢失数据。
(3)实现保证从节点的数量和延迟性功能,通过min-slaves-to-write、minslaves-max-lag参数配置定义。

主节点可以根据replconf命令判断从节点超时时间,体现在info replication统计中的从机信息的lag字段中,lag表示与从节点最后一次通信延迟的秒数,正常延迟应该在0和1之间。如果超过repl-timeout配置的值(默认60秒),则判定从节点下线并断开复制客户端连接。即使主节点判定从节点下线后,如果从节点重新恢复,心跳检测会继续进行。

Redis主从复制缺点

  • Redis不具备自动容错和恢复功能,一旦主节点出现故障,需要手动将一个从节点晋升为主节点,同时需要修改应用方的主节点地址,还需要命令其他从节点去复制新的主节点,整个过程都需要人工干预
  • 主节点的写能力受到单机的限制。
  • 主节点的存储能力受到单机的限制。

Redis主从复制总结

接下来的是一些关于 Redis 复制的非常重要的事实(摘自官网):

  • Redis 使用异步复制,slave 和 master 之间异步地确认处理的数据量。

  • 一个 master 可以拥有多个 slave。

  • slave 可以接受其他 slave 的连接。除了多个 slave 可以连接到同一个 master 之外, slave 之间也可以像层叠状的结构(cascading-like structure)连接到其他 slave 。自 Redis 4.0 起,所有的 sub-slave 将会从 master 收到完全一样的复制流。

  • Redis 复制在 master 侧是非阻塞的。这意味着 master 在一个或多个 slave 进行初次同步或者是部分重同步时,可以继续处理查询请求。

  • 复制在 slave 侧大部分也是非阻塞的。当 slave 进行初次同步时,它可以使用旧数据集处理查询请求,假设你在 redis.conf 中配置了让 Redis 这样做的话。否则,你可以配置如果复制流断开, Redis slave 会返回一个 error 给客户端。但是,在初次同步之后,旧数据集必须被删除,同时加载新的数据集。 slave 在这个短暂的时间窗口内(如果数据集很大,会持续较长时间),会阻塞到来的连接请求。自 Redis 4.0 开始,可以配置 Redis 使删除旧数据集的操作在另一个不同的线程中进行,但是,加载新数据集的操作依然需要在主线程中进行并且会阻塞 slave 。

  • 复制既可以被用在可伸缩性,以便只读查询可以有多个 slave 进行(例如 O(N) 复杂度的慢操作可以被下放到 slave ),或者仅用于数据安全。

  • 可以使用复制来避免 master 将全部数据集写入磁盘造成的开销:一种典型的技术是配置你的 master Redis.conf 以避免对磁盘进行持久化,然后连接一个 slave ,其配置为不定期保存或是启用 AOF。但是,这个设置必须小心处理,因为重新启动的 master 程序将从一个空数据集开始:如果一个 slave 试图与它同步,那么这个 slave 也会被清空。