主从复制
概念
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称之为从节点(slave);数据的复制是单向的,只能由主节点到从节点。
默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但是一个从节点只能有一个主节点。
作用
读写分离:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担负载,可以大大提高Redis服务器的并发量。
高可用基石:主从复制还是哨兵模式和集群能够实施的基础。
故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
主从拓扑架构
Redis的主从拓扑结构可以支持单层或多层复制关系,根据拓扑复杂性可以分为以下三种:一主一从、一主多从、树状主从架构。
一主一从结构
一主一从结构是最简单的复制拓扑结构,我们前面搭建的就是一主一从的架构,架构如图所示:
一主一从架构用于主节点出现宕机时从节点提供故障转移支持,当应用写命令并发量较高且需要持久化时,可以只在从节点上开启AOF,这样既保证数据安全性同时也避免了持久化对主节点的性能干扰。但是这里有一个坑,需要你注意,就是当主节点关闭持久化功能时,如果主节点脱机要避免自动重启操作。因为主节点之前没有开启持久化功能自动重启后数据集为空,这时从节点如果继续复制主节点会导致从节点数据也被清空的清空,丧失了持久化的意义。安全的做法是在从节点上执行slaveof on one断开与主节点的复制关系,再重启主节点从而避免这一问题。
一主多从架构
一主多从架构又称之为星形拓扑结构,一主多从架构如下图所示:
一主多从架构可以实现读写分离来减轻主服务器的压力,对于读占比较大的场景,可以把读命令发送到从节点来分担主节点压力。同时在日常开发中如果需要执行一些比较耗时的读命令,如:keys,sort等,可以在其中一台从节点上执行,防止慢查询对主节点造成堵塞从而影响线上服务的稳定性。对于写并发量较高的场景,多个从节点会导致主节点写命令的多次发送从而过度消耗网络带宽,同时也加重了主节点的负载影响服务稳定性。
树状主从架构
树状主从架构又称为树状拓扑架构,树状主从架构如下图所示:
树状主从架构使得从节点不但可以复制主从数据,同时可以作为其他从节点的主节点继续向下层复制。解决了一主多从架构中的不足,通过引入复制中间层,可以有效降低主节点负载和需要传送给主节点的数据量。如架构图中,数据写入节点A后会同步到B和C节点,B节点再把数据同步到D和E节点,数据实现了一层一层的向下复制。当主节点需要挂载多个从节点时为了避免对主节点的性能干扰,可以采用树状主从结构降低主节点压力。
Redis主从复制搭建(一主二仆)
搭建主从复制很简单,如果时三台不同服务器搭建集群,只要设置replicaof参数,配置主节点地址即可,因为我这只有一台虚拟机,所以需要修改很多配置防止Redis实例冲突。
# 创建文件夹及赋值配置文件
[root@192 ~]# cd /usr/local/
[root@192 local]# mkdir myredis
[root@192 local]# cd myredis/
[root@192 myredis]# cp /usr/local/redis/redis.conf ./
# 设置后台启动
[root@192 myredis]# vim redis.conf
daemonize yes
# 创建三个实例的配置文件
[root@192 myredis]# vim redis_6379.conf
include /usr/local/myredis/redis.conf
pidfile /var/run/redis_6379.pid
port 6379
dbfilename dump6379.rdb
[root@192 myredis]# cp redis_6379.conf redis_6389.conf
[root@192 myredis]# vim redis_6389.conf
include /usr/local/myredis/redis.conf
pidfile /var/run/redis_6389.pid
port 6389
replicaof 127.0.0.1 6379
dbfilename dump6389.rdb
[root@192 myredis]# cp redis_6389.conf redis_6399.conf
[root@192 myredis]# vim redis_6399.conf
include /usr/local/myredis/redis.conf
pidfile /var/run/redis_6399.pid
port 6399
replicaof 127.0.0.1 6379
dbfilename dump6399.rdb
# 关闭之前的redis
[root@192 myredis]# systemctl stop redis
# 启动
[root@192 myredis]# /usr/local/redis/bin/redis-server redis_6379.conf
[root@192 myredis]# /usr/local/redis/bin/redis-server redis_6389.conf
[root@192 myredis]# /usr/local/redis/bin/redis-server redis_6399.conf
[root@192 myredis]# ps -ef | grep redis
# 查看节点信息
[root@192 myredis]# /usr/local/redis/bin/redis-cli
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6389,state=online,offset=70,lag=0
slave1:ip=127.0.0.1,port=6399,state=online,offset=70,lag=0
master_failover_state:no-failover
master_replid:2ef264274d864fd97c163ec1017caf648581ec0d
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:70
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:70
# 主节点写入数据
127.0.0.1:6379> set kk vv
OK
# 从节点能读取到主节点数据,但是写入数据会报错
[root@192 myredis]# /usr/local/redis/bin/redis-cli -p 6389
127.0.0.1:6389> set kk vv
(error) READONLY You can't write against a read only replica.
127.0.0.1:6389> get kk
"vv"
实现原理
主从复制过程大体可以分为3个阶段:连接建立阶段(即准备阶段)、数据同步阶段、命令传播阶段。
连接建立阶段
该阶段的主要作用时在主从节点之间建立连接,为数据同步做好准备。
1.保存主节点信息
从节点服务器内部维护了两个字段,即masterhost和masterport字段,用于存储主节点的ip和port信息。
需要注意的时,slaveof是异步命令,从节点完成主节点ip和port的保存后,向发送slaveof命令的客户端直接返回ok,实际的复制操作在这之后才开始进行。
2.建立socket连接
从节点每秒1次调用复制定时函数replicaitonCron(),如果发现了有主节点可以连接,便会根据主节点的ip和port,创建socket连接。如果连接成功,则:
- 从节点:为该socket建立一个专门处理复制工作的文件事件处理器。负责后续的复制工作,如接收RDB文件、接收命令传播等。
- 主节点:接收到从节点的socket连接后(即accept之后),为该socket创建对应的客户端状态,并将从节点看做时连接到主节点的一个客户端,后面的步骤会以从节点向主节点发送命令请求的形式来进行。
3.发送ping命令
从节点成为主节点的客户端之后,发送ping命令进行首次请求,目的是:检查socket连接是否可用,以及主节点当前是否能够处理请求。
从节点发送ping命令后,可能出现3种情况:
- 返回pong:说明socket连接正常,且主节点当前可以处理请求,复制过程继续。
- 超时:一定时间后从节点仍未收到主节点的回复,说明socket连接不可用,则从节点断开socket连接,并重连。
- 返回pong以外的结果:如果主节点返回其他结果,如正在处理超时运行的脚本,说明主节点当前无法处理命令,则从节点断开socket连接,并重连。
4.身份验证
如果从节点中设置了masterauth选项,则从节点需要向主节点进行身份验证;没有设置该选项,则不需要验证。从节点进行身份验证是通过向主节点发送auth命令进行的,auth命令的参数即为配置文件中的masterauth的值。
如果主节点设置密码的状态,与从节点masterauth的状态一致(一致是指都存在,且密码相同,或者都不存在),则身份验证通过,复制过程继续;如果不一致,则从节点断开socket连接,并重连。
5.发送从节点端口信息
身份验证之后,从节点会向主节点发送其监听的端口号(前述例子中为6380),主节点将该信息保存到该从节点对应的客户端slave_listening_port字段中;该端口信息除了在主节点中执行info Replication时显示以外,没有其他作用。
数据同步阶段
每一个Redis master 都有一个replication ID:这是一个较大的伪随机字符串,标记了一个给定的数据集。每个mastet也持有一个偏移量,master将自己产生的复制流发送给slave时,发送多少个字节的数据,自身的偏移量就会增加多少,目的是当有新的操作修改自己的数据集时,它可以以此更新slave的状态。复制偏移量即使在没有一个slave连接到master时,也会自增,所以基本上每一对给定的【Replication ID offset】都会标识一个master数据集的确切版本。
主从节点之间的连接建立以后,便可以开始进行数据同步,该阶段可以理解为从节点数据的初始化。具体执行的方式是:从节点向主节点发送psync命令(Redis2.8以前时sync命令),开始同步。
数据同步阶段是主从复制最核心的阶段,根据主从节点当前状态的不同,可以分为全量复制和部分复制。
在Redis2.8以前,从节点向主节点发送sync命令请求同步数据,此时的同步方式是全量复制;在Redis2.8及以后,从节点可以发送psync命令请求同步数据, 此时根据主从节点当前的状态的不同,同步方式可能时全量复制或部分复制。
Redis之所以能够支持全量复制和部分复制,主要是对sync命令的优化,在Reis2,8版本以后使用的时一个全新的psync命令,命令格式为:psync {runId}{offset},这两个参数的意义:
- runId:主节点运行的id
- offset:当前从节点复制的数据偏移量
全量复制
一般用于初次复制场景,不管是新旧版本的redis在从服务器第一次与主服务连接时都将进行一次全量复制,它会把主节点的全部数据一次性发给从节点,当数据量较大时,会对主节点和网络造成很大的开销,redis的早期版本只支持全量复制,这不是一种高效的数据复制方式。
Reids通过psync命令进行全量复制的过程如下:
1、从节点判断无法进行部分复制,向主节点发送全量复制的请求;或从节点发送部分复制的请求,但主节点判断无法进行部分复制;具体判断过程需要在讲述了部分复制原理后再介绍。
2、主节点收到全量复制的命令后,执行bgsave,在后台生成RDB文件,并使用一个缓冲区(称为复制缓冲区)记录从现在开始执行的所有写命令;
3、主节点的basave执行完成后,将RDB文件发送给从节点;从节点首先清除自己的旧数据,然后载入接收的RDB文件,将数据库状态更新至主节点执行的bgsave时的数据库状态;
4、主节点将前述复制缓冲区中的所有写命令发送给从节点,从节点执行这些写命令,将数据库状态更新至主节点的最新状态;
5、如果从节点开启了AOF,则会触发bgrewriteaof的执行,从而保证AOF文件更新至主节点的最新状态;
其中,有几点需要注意:从节点接收了来自主节点的89260个字节的数据;从节点在载入主节点的数据之前要先将老数据清除;从节点在同步完数据后,调用了bgrewriteaof。
通过全量复制的过程可以看出,全量复制是非常重型的操作:
1、主节点通过bgsave命令fork子进程进行RDB持久化,该进程是非常消耗CPU、内存(页表复制)、硬盘IO的;
2、主节点通过网络将RDB文件发送给从节点,对主从节点的带宽都会带来很大的消耗;
3、从节点清空老数据、载入新RDB文件的过程时阻塞的,无法相应客户端的命令;如果从节点执行bgrewriteaof,也会带来额外的消耗;
部分复制
用于处理在主从复制中因网络闪断等原因造成的数据丢失场景,当从节点再次连上主节点后,如果条件允许,主节点会补发丢失数据给从节点。因为补发的数据远远小于全量数据,可以有效避免全量复制的过高开销,部分复制是对老版复制的重大优化,有效避免了不必要的全量复制操作。
部分复制的实现,依赖于三个重要的概念:
1.服务器运行ID
每个Redis节点启动后都会动态分配一个40位的十六进制字符串作为运行ID,运行ID的主要作用时用来唯一识别Redis节点,我们可以使用info server 命令来查看,run_id字段就是服务器运行的ID。
run_id:af89742131sdhi12hbgdxi84t2849dhsf8r234824
2.复制偏移量
参与复制的主从节点都会分别维护自身复制偏移量:主服务器每次向从服务器传播N个字节的数据时,就将自己的偏移量的值加上N,从服务器每次接收到主服务器传播的N个字节的数据时,将自己的偏移量加上N,通过对比主从服务器的复制偏移量,就可以知道主从服务器的数据是否一致,如果主从服务器的偏移量总是相同,那么主从数据一致,相反,如果主从服务器两个的偏移量并不相同,那么说明主从服务器并未处于数据一致的状态,比如在多个从服务器时,在传输的过程某个服务器离线了,如下图所示:
3.复制积压缓冲区
复制积压缓冲区是保存在主节点上的一个固定长度的队列,默认大小为1MB,当主节点有连接的从节点(slave)时被创建,这时主节点(master)相应写命令时,不但会把命令发送给从节点,还会写入复制积压缓冲区,如下图所示:
因此,主服务器的复制积压缓冲区里面会保存着一部分最近传播的写命令,并且复制积压缓冲区会为队列中的每个字节记录相应的复制偏移量。所以当从服务器重新连上主服务器时,从服务器通过psync命令将自己的复制偏移量offset发送给主服务器,主服务器会根据这个复制偏移量来决定对从服务器执行何种数据同步操作:
- 如果从服务器的复制偏移量之后的数据仍然存在于复制积压缓冲区里面,那么主服务器将对从服务器执行部分复制操作
- 付过从服务器的复制偏移量之后的数据不存在于复制积压缓冲区里面,那么主服务器对从服务器执行全量复制操作
psync命令的执行
在了解了复制偏移量、复制积压缓冲区、节点运行ID之后,本节将介绍psync命令的参数和返回值,从而说明psync命令执行过程中,主从节点时如何确定使用全量复制还是部分复制的。
1、首先,从节点根据当前状态,决定如何调用psync命令:
- 如果节点之前未执行过slaveof或最近执行了slaveof on one,则从节点发送命令为psync ? -1,向主节点请求全量复制。
- 如果从节点之前执行了slaveof,则发送命令为psync,其中runid为上次复制的主节点的runid,offset为上次复制截止时从节点保存的复制偏移量。
1、主节点根据收到的psync命令,及当前服务器状态,决定执行全量复制还是部分复制;
- 如果主节点版本低于Redis2.8,则返回-ERR回复,此时从节点重新发送psync命令执行全量复制;
- 如果主节点版本够新,且runid与从节点发送的runid相同,且从节点发送的offset之后的数据在复制积压缓冲区中都存在,则回复+CONTINUE,表示将进行部分复制,从节点等待主节点发送其缺少的数据即可。
- 如果主节点版本够新,但是runid与从节点发送的runid不同,或从节点发送的offset之后的数据不存在复制积压缓冲区中(在队列中被挤出了),则回复+FULLRESYNC,表示要进行全量复制,其中runid表示主节点的runid,offset表示主节点当前的offset,从节点保存这两个值,以备使用。
命令传播阶段
数据同步阶段完成之后,主从节点会进入命令传播阶段;在这个阶段主节点将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性。
在命令传播阶段,除了发送写命令,主从节点还维持着心跳机智:PING和REPLCONF ACK
需要注意的时,命令传播是异步的过程,即主节点发送写命令后并不会等待从节点的回复;因此实际上主从节点之间很难保持实时的一致性,延迟在所难免。数据不一致的程度,与主从节点之间的网络状况、主从点写命令的执行频率、以及主节点中的repl-disable-tcp-nodelay配置等有关。
repl-disable-tcp-nodelay no:该配置作用于命令传播阶段,控制主节点是否禁止与从节点的TCP0_NODELAY;默认no,即不禁止TCP_NODELAY。当设置为yes时,TCP会对包进行合并从而减少带宽,但是发送的频率会降低,从节点数据延迟增加,一致性变差;具体发送频率与Linux内核的配置有关,默认配置为40ms。当设置为no时,TCP会立马将主节点的数据发送给从节点,带宽增加但延迟变小。
一般来说,只有当应用对Redis数据不一致的容忍度较高,且主从节点之间网络状况不好时,才会设置为yes;多数情况使用默认值no。
心跳机制。
在命令传播阶段,除了发送命令,主从节点还维持着心跳机智:PING和REPLCONF ACK。心跳机制对于主从复制的超时判断,数据安全等有作用。
1、主->从:PING;
每隔指定的时间,主节点会向从节点发送PING命令,这个PING命令的作用,主要是为了让从节点进行超时判断。
PING发送的频率由repl-ping-slave-period参数控制,单位是秒,默认值时10s。
关于该PING命令究竟时由主节点发送给从节点,还是相反,有一些争议;因为在Rdis的官方文档中,对该参数的注释中,说明是从节点向主节点发送PING命令,如下图所示:
但是根据该参数的名称(含有ping-slave),以及代码实现,我认为该PING命令是主节点发给从节点的,相关代码如下:
2、从->主:REPLCONF ACK
在命令传播阶段,从节点会向主节点发送REPLCONF ACK命令,频率是每秒1次;命令格式为:REPLCONF ACK{offset},其中offset指从节点保存的复制偏移量。REPLCONF ACK命令的作用包括:
(1)实时监测主从节点网络状态:该命令会被主节点用于复制超时的判断。此外,在主节点中使用info Replication,可以看到其从节点的状态的lag值,代表的是主节点上次收到该REPLCONF ACK命令的时间间隔,在正常情况下,该值应该是0或1,如下图所示:
(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值。