1、主从复制机制
通过info replication获取的主从信息:
master_link_status:up
:表示从节点与主节点的连接状态,可以是up或down。master_last_io_seconds_ago:4
: 表示从节点最后一次与主节点通信的时间,单位是秒。master_sync_in_progress:0
: 表示从节点是否正在与主节点进行同步,可以是0或1。slave_read_repl_offset:46567
: 从节点读取的复制偏移量,即从节点已经接收了主节点发送的多少字节的命令。slave_repl_offset:46567
: 从节点复制的偏移量,即从节点实际执行了主节点发送的多少字节的命令,若与slave_read_repl_offset相差太大,则说明从机有很多数据还未处理。slave_priority:100
:表示从节点的优先级,用于在主节点故障时选择新的主节点,数值越小优先级越高。slave_read_only:1
:表示从节点是否只读,可以是0或1。replica_announced:1
:表示从节点是否向主节点报告自己的IP地址和端口号,可以是0或1。connected_slaves:0
: 当前节点连接的从节点数量。master_failover_state
: 当前是否有故障转移发生,no-failover或其他状态。master_replid
: 表示主节点的复制ID,用于标识一个复制流程。master_replid2
:表示主节点的第二个复制ID,用于在故障转移后进行部分重同步。master_repl_offset:46567
: 主节点复制的偏移量,即主节点已经发送了多少字节的命令给从节点。second_repl_offset:40478
: 从机在切换主机前原主机的复制偏移量master_repl_offset,用于故障切换时支持增量同步。repl_backlog_active:1
: 从机是否启用了复制积压缓冲区,可以是0或1。repl_backlog_size:1048576
: 配置复制积压缓冲区的大小默认1048576字节,即1M。repl_backlog_first_byte_offset:39162
: 从机复制积压缓冲区中第一个字节对应的偏移量。repl_backlog_histlen:7406
: 从机的复制积压缓冲区中实际存储的字节数。
主从复制系统使用三种主要机制工作:
- 当Master、Slave连接良好时,Master通过向Slave发送命令流来保持Slave更新,以复制由于在Master对数据集产生的影响:客户端写入、Key过期或逐出、任何其他更改主数据集的操作。
- 当Master、Slave由于网络问题或由于主从心跳检测超时等原因导致连接中断时,Slave将重新连接并尝试继续增量同步,尝试获取在断开连接期间丢失的命令流部分。
- 当偏移量之后的数据已挤出挤压缓冲区或初次连接,则副本将要求全量同步。Master将创建其数据的快照并发送到Slave,然后在数据集更改时继续发送命令流。
1.1 全量复制
Slave首次连接或执行过SLAVEOF no one
则会向主服务器发送PSYNC ? -1
(在2.8版本之前是SYNC
),主动请求主服务器进行完整重同步。master
收到后,会立即响应FULLRESYNC runid offset
,从机保存Master的runid
并将offset
作为初始偏移量,之后Master会fork
后台子线程执行bgsave
命令并使用复制缓存区client-output-buffer(replication buffer)
记录此后执行的写命令,bgsave
执行完成后将生成的RDB
文件发送给slave
节点,然后master
节点再将复制缓冲区内容以redis协议格式全部发送给slave
节点。slave
节点先删除旧数据,再将收到的RDB
文件载入自己的内存,再加载所有收到缓冲区的内容,从而这样一次完整的数据同步。
若从机开启了AOF,则在全量复制阶段会触发重写机制
若从节点开启了AOF
,则触发bgrewriteaof
,保证AOF文件更新至最新状态。如果主节点接收到多个并发的全量同步请求,则只会执行单个后台保存以服务于所有从机。
通过全量复制的过程可以看出,全量复制是非常重型的操作:
- 主节点通过bgsave命令fork子进程进行RDB持久化,该过程是非常消耗CPU、内存(页表复制)、硬盘IO的;
- 主节点通过网络将RDB文件发送给从节点,对主从节点的带宽都会带来很大的消耗
- 从节点清空老数据、载入新RDB文件的过程是阻塞的,无法响应客户端的命令;且若执行bgrewriteaof,还将有额外的消耗。
1.2 增量复制
如果从服务器已经复制过某个主服务器,那么从服务器在开始复制时将向主服务器发送PSYNC <rep_id> <offset>
命令:其中rep_id
是上次获取的Master的master_replid
,而offset
则是从服务器当前的复制偏移量+1。Master会通过这两个参数来判断应该对Slave执行哪种同步操作:Slave
上报的master_replid
与Master
的master_replid1
或replid2其中一个是否相等,判断主从是否发生改变;offset+1
来判断偏移量之后的数据是否还存在于积压缓冲区中。
当主机版本过低,收到
psync
如何处理?
主服务器的版本低于Redis 2.8
,识别不了PSYNC
命令并返回-ERR
回复,随后从服务器将向主服务器发送SYNC
命令,并与主服务器执行全量同步。
1.2.1 复制偏移量
主库和从库分别各自维护一个复制偏移量(info replication
查看),在Master
中master_repl_offset
代表服务器的当前复制偏移量(总字节数),也代表Master通过命令流向从节点传递的字节数;slave_repl_offset
代表从库完成同步的字节数。Slave
会定期向Master
报告已经处理了多少偏移量的数据,这样master就可以知道哪些Master
已经同步了哪些命令。
如果Master
和Slave
之间的连接出现网络抖动,Slave
会尝试增量同步,获取断开期间错过的命令流部分。否则,会请求进行全量同步,然后继续发送命令流同步。
1.2.2 积压缓冲区
Master
中维护着一个复制积压缓冲区replication buffer,一个固定长度的FIFO队列,保存最近redis执行的命令和对应字节的复制偏移量offset
,大小由配置参数repl-backlog-size
指定(默认1MB),所有Slave共享此缓冲区,备份最近通过命令流同步给从库的数据(在主从命令传播阶段,Master不仅将写命令发送给Slave,还会备份一份到复制积压缓冲区)。当在命令传播阶段,主从断联恢复后,主库就会在repl_backlog_buffer
中找到offset
对应位置,并把之后的写命令写replication buffer
同步给从库。
〖推荐大小〗:比如若网络中断的平均时间是60s,而主节点平均每秒产生的写命令字节数为100KB,则复制积压缓冲区的平均需求为6MB,保险起见可以设置为12MB,来保证绝大多数断线情况都可以使用部分复制。
client-output-buffer-limit slave
复制缓冲区和repl-backlog-size
积压缓冲区:
复制缓冲区只在全量复制时使用,而复制积压缓冲区只在增量复制时使用。复制缓冲区是为了保证RDB文件传输期间的数据一致性,而复制积压缓冲区是为了保证网络断连期间的数据一致性。复制缓冲区是客户端输出缓冲区的一种,主节点会为每一个从节点分别分配复制缓冲区;而复制积压缓冲区则是一个主节点只有一个,无论它有多少个从节点。
1.2.3 Replication ID
通过info replication
、info server
可以查看主从机的信息和runid
。每个redis实例在启动时候,都会随机生成一个长度为40唯一随机字符串来标识当前redis节点复制idrepl_id
。当主从复制在初次复制时,Slave接收来自Master的FULLRESYNC
并将repl_id
保存下来;当断线重连时,从节点会将这个repl_id
发送给主节点,主节会判断能否进行部分复制。
注意: redis4.0之后,
replication ID
取代了runid
,作为redis服务器的复制标识,连接到master的slave会在握手后继承其复制IDreplication ID
而不再需要提供运行IDrunid
。
1.2.4 源码分析
Master
的replication ID
与Slave
通过PSYNC
请求中的是否一致,如果复制 ID 更改,则此主服务器具有不同的复制历史,并且无法继续增量同步,必须全量同步。Master
包含两个有效的复制IDmaster_replid
和master_replid2
,但是对于master_replid2
仅在特定偏移量second_replid_offset
之前有效。
//replication.c:syncCommand->masterTryPartialResynchronization()
int masterTryPartialResynchronization(client *c, long long psync_offset) {
long long psync_len;
char *master_replid = c->argv[1]->ptr;
char buf[128];
int buflen;
//满足这个条件,将进行全量同步
if (strcasecmp(master_replid, server.replid) &&//当前server的ID不是master的ID
//并且,主机旧的master不是从机的master 或 是同一个master但从机要求的偏移比自己接收的数据还大
(strcasecmp(master_replid, server.replid2) || psync_offset > server.second_replid_offset)) {
//携带的复制id为?,则意味着从机想强制进行全量同步
/* Replid "?" is used by slaves that want to force a full resync. */
if (master_replid[0] != '?') {
if (strcasecmp(master_replid, server.replid) &&
strcasecmp(master_replid, server.replid2))
{
serverLog(LL_NOTICE,"Partial resynchronization not accepted: "
"Replication ID mismatch (Replica asked for '%s', my "
"replication IDs are '%s' and '%s')",
master_replid, server.replid, server.replid2);
} else {
serverLog(LL_NOTICE,"Partial resynchronization not accepted: "
"Requested offset for second ID was %lld, but I can reply "
"up to %lld", psync_offset, server.second_replid_offset);
}
} else {
serverLog(LL_NOTICE,"Full resync requested by replica %s",replicationGetSlaveName(c));
}
goto need_full_resync;
}
/* We still have the data our slave is asking for? */
if (!server.repl_backlog || //主机是否开启了积压缓冲区
psync_offset < server.repl_backlog->offset || //请求的offset小于了积压缓冲区起始的offset
//或请求的offset大于积压缓冲区最新的offset,即超过已有数据范围将开启全量复制
psync_offset > (server.repl_backlog->offset + server.repl_backlog->histlen)) {
serverLog(LL_NOTICE,"Unable to partial resync with replica %s for lack of backlog" +
"(Replica request was: %lld).", replicationGetSlaveName(c), psync_offset);
if (psync_offset > server.master_repl_offset) {
serverLog(LL_WARNING,"Warning: replica %s tried to PSYNC with an offset that is " +
"greater than the master replication offset.", replicationGetSlaveName(c));
}
goto need_full_resync;
}
//至此开始启动增量复制响应
/* If we reached this point, we are able to perform a partial resync:
* 1) Set client state to make it a slave.
* 2) Inform the client we can continue with +CONTINUE
* 3) Send the backlog data (from the offset to the end) to the slave. */
c->flags |= CLIENT_SLAVE;//设置客户端状态使其成为slave
c->replstate = SLAVE_STATE_ONLINE;//通知Slave:+CONTINUE
c->repl_ack_time = server.unixtime;
c->repl_start_cmd_stream_on_ack = 0;
listAddNodeTail(server.slaves,c);
/* We can't use the connection buffers since they are used to accumulate
* new commands at this stage. But we are sure the socket send buffer is
* empty so this write will never fail actually. */
if (c->slave_capa & SLAVE_CAPA_PSYNC2) {
buflen = snprintf(buf,sizeof(buf),"+CONTINUE %s\r\n", server.replid);
} else {
buflen = snprintf(buf,sizeof(buf),"+CONTINUE\r\n");
}
if (connWrite(c->conn,buf,buflen) != buflen) {
freeClientAsync(c);
return C_OK;
}
psync_len = addReplyReplicationBacklog(c,psync_offset);
serverLog(LL_NOTICE,"Partial resynchronization request from %s accepted. " +
"Sending %lld bytes of backlog starting from offset %lld.",
replicationGetSlaveName(c),psync_len, psync_offset);
/* Note that we don't need to set the selected DB at server.slaveseldb
* to -1 to force the master to emit SELECT, since the slave already
* has this state from the previous connection with the master. */
refreshGoodSlavesCount();
/* Fire the replica change modules event. */
moduleFireServerEvent(REDISMODULE_EVENT_REPLICA_CHANGE,
REDISMODULE_SUBEVENT_REPLICA_CHANGE_ONLINE,
NULL);
return C_OK; /* The caller can return, no full resync needed. */
need_full_resync:
/* We need a full resync for some reason... Note that we can't
* reply to PSYNC right now if a full SYNC is needed. The reply
* must include the master offset at the time the RDB file we transfer
* is generated, so we need to delay the reply to that moment. */
//如果需要完全同步,不能立即回复PSYNC请求,还必须包含RDB文件生成时的master偏移量
return C_ERR;
}
1.3 命令传播阶段
数据同步阶段完成后,主从节点进入命令传播阶段;在这个阶段主节点将自己执行的写命令通过复制流发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性。在命令传播阶段,除了发送写命令,主从节点还维持着心跳机制:PING
和REPLCONF ACK。
延迟与不一致: 命令传播是异步的过程,即主节点发送写命令后并不会等待从节点的回复;因此实际上主从节点之间很难保持实时的一致性,延迟在所难免。数据不一致的程度,与主从节点之间的网络状况、主节点写命令的执行频率、以及主节点中的
repl-disable-tcp-nodelay
配置等有关。
repl-disable-tcp-nodelay no
:作用于命令传播阶段,控制Master是否禁止与Slave的TCP_NODELAY
。默认no,TCP将主节点的数据立即发送给从节点,带宽增加但延迟变小。。为yes时,会使用Nagle
算法对TCP包进行合并从而减少带宽,将发送频率降低,会使得Slave
数据延迟增加和一致性变差。具体发送频率与Linux内核的配置有关,默认配置为40ms。一般来说,只有当应用对Redis数据不一致的容忍度较高,且主从节点之间网络状况不好时,才会设置为yes;多数情况使用默认值no。
2、配置及使用
2.1 Master配置
-
开启
Master
防火墙端口:firewall-cmd --add-port=6379/tcp --permanent
随后重启防火墙`firewall-cmd --reload`` -
``repl-diskless-sync no
:作用于全量复制阶段,控制主节点是否使用
diskless`复制(无盘复制)。所谓diskless复制,是指在全量复制时,主节点不再先把数据写入RDB文件,而是直接写入slave的socket中,整个过程中不涉及硬盘;diskless复制在磁盘IO很慢而网速很快时更有优势。 -
repl-diskless-sync-delay 5
:作用于全量复制阶段且diskless
配置为yes时,当主节点使用diskless复制时,该配置决定主节点向从节点发送之前停顿的时间,单位秒,默认5s。之所以设置停顿时间,是基于以下两个考虑:①向slave的socket的传输一旦开始,新连接的slave只能等待当前数据传输结束,才能开始新的数据传输; ②多个从节点有较大的概率在短时间内建立主从复制。 -
client-output-buffer-limit slave 256MB 64MB 60
:在全量复制阶段,限制主节点为每个从节点分配的复制缓冲区的大小:若复制缓冲区buffer
大于256MB,或者60s内大于64M,则主节点会断开与该从节点的连接。全量复制期间主节点将bgsave
生成RDB
文件、主节点发往从节点RDB
和从节点载入RDB
期间执行的写命令存储在复制缓冲区中。如果主节点数据量大(Slave
还没装载完rdb
)或网络延迟高,可能导致缓冲区超限,引起全量复制和连接中断的循环。 -
repl-backlog-size 1mb
:设置复制积压缓冲区的大小,一个环形缓冲区,主节点只有一个,所有从节点共享。复制积压缓冲区用来保存主节点最近的写命令,当主从连接断开后,重新建立连接,从节点可以通过复制积压缓冲区进行部分复制,而不需要进行全量同步。当复制积压缓冲区不足时,缓冲区发生覆写,导致重连的从机进行全量复制。 -
repl-backlog-ttl 3600
:当主节点没有从节点时,复制积压缓冲区保留的时间,这样当断开的从节点重新连进来时,可以进行部分复制;默认3600s。如果设置为0,则永远不会释放复制积压缓冲区。 -
repl-ping-slave-period 10
与REPLCONF ACK <offset>
:当数据同步完成以后,在此主从维护着心跳检查来确认对方是否在线,每隔一段时间(默认10秒,repl-ping-slave-period
)主节点向从节点发送PING命令判断从节点是否在线;而从节点每秒1次向主节点发送REPLCONF ACK <offset>
,其中offset指从节点保存的复制偏移量,汇报自己复制偏移量并判断判断主节点是否在线,主节点会对比复制偏移量并向从节点发起增量同步,最终实现与主库数据相同。 -
repl-timeout
:设置超时时间默认60秒,通过上两个参数判断。有两个作用:在全量复制过程中,若从节点在repl-timeout
内未接收到主节点的数据,那么从节点会关闭连接并重新尝试复制;在主从节点正常通信时,若从节点在repl-timeout
内未向主节点发送任何数据,那么主节点会认为从节点已经下线,并在INFO replication
中显示该从节点的lag=-1
。 -
repl-disable-tcp-nodelay no:是否禁用
TCP_NODELAY
,控制主节点在命令传播阶段是否使用Nagle
算法,以减少网络包的数量和带宽消耗,但是可能增加数据在从节点出现的延迟。 -
min-slaves-to-write 3
与min-slaves-max-lag 10
:保证主节点在不安全的情况下不会执行写命令。规定了主节点在执行写命令时,最小从节点数目,及对应的最大延迟(Master接收REPLCONF ACK时间间隔),提高数据的可靠性和数据的一致性。
2.2 Slave配置
replicaof 192.168.1.1 6379
:配置Master地址和端口,其中192.168.1.1 6379
为主机IP地址(或主机名)和端口。也可调用REPLICAOF/SLAVEOF ip port
指定Master,slaveof no one
表示恢复自己主机身份。masterauth<password>
:设置要向主机进行身份验证的从机,若主机配置了requirepass
,那需要在从机中配置主机的密码来实现连接:运行中在命令行要使用redis cli
并键入:`config set masterauth ``slave-read-only yes
:从节点是否只读;默认是只读的。由于从节点开启写操作容易导致主从节点的数据不一致,因此该配置尽量不要修改。slave-serve-stale-data yes
:控制从服务器在失去主服务器连接或复制进行中时是否响应旧数据,默认yes。如果对数据一致性要求很高则设置为no,从服务器会返回一个错误SYNC with master in progress
给除了INFO
和SLAVEOF
之外的所有请求。
3、主从复制问题
3.1 从机宕机恢复
在Redis4.0 之前psync
有不足之处,当从库重启之后会丢失Master的runid
,则从库将进行全量复制。在实际生产中时常需要对从库的维护并进行重启,所以应避免执行全量复制。因此redis 4.0
优化了psync
实现重启的情况下也能实现增量同步:新增两个复制ID
,通过repl_id
取代runid
:
master_replid
: 每个redis启动时,都会为其生成40个字节的随机字符串,和runid
没有直接关联,但生成规则相同。当变为从机后,master_replid
会被Master的master_replid
覆盖。master_replid2
:默认初始化为全0,默认初始化为全0,用于存储上次主实例的master_replid
。
redis通过shutdown save
关闭,会调用rdbSaveInfoAuxFields
函数,把当前repl-id
和repl-offset
保存到RDB文件中,当前的RDB存储的数据内容和复制信息可通过redis-check-rdb
查看。当从机宕机重启后,redis加载RDB
文件并处理其中的辅助字段AUX fields
,把其中repl_id
和repl_offset
加载到实例并赋给master_replid
和master_repl_offset
。
当从库开启了AOF,则会失去部分复制机制
当从库开启了AOF持久化,redis加载顺序发生变化优先加载AOF文件,但是由于aof文件中没有复制信息,所以导致重启后从实例依旧使用全量复制。
3.2 数据过期
在单机版Redis中,存在两种删除策略:惰性删除,服务器不会主动删除数据,只有当客户端查询某个数据时,服务器判断该数据是否过期,如果过期则删除;定期删除,服务器执行定时任务删除过期数据,但是考虑到内存和CPU的折中(删除会释放内存,但是频繁的删除操作对CPU不友好),该删除的频率和执行时间都受到了限制。
在主从复制场景下不能依赖主从同步时间,为了主从节点的数据一致性,从节点不会主动删除数据,而是由主节点通过DEL
控制从节点中过期数据的删除。由于主节点的惰性删除和定期删除策略,都不能保证主节点及时对过期数据执行删除操作,因此,当客户端通过Redis从节点读取数据时,很容易读取到已经过期的数据。
Redis 3.2
中可以解决数据过期问题,从节点在读取数据时,增加了对数据是否过期的判断:如果该数据已过期,则返回给客户端空值nil,避免了数据的脏读。Redis 3.2
之后还引入了replica-ignore-maxmemory
配置项(默认yes),从节点不会根据maxmemory
设置进行内存淘汰,而是等待主节点发送DEL
命令来删除过期的键,保证主从节点的数据一致性;若为no,则从节点会进行内存淘汰,可能导致主从节点的数据不一致。
3.3 主机宕机处理
Redis4.0 之后,主机宕机重启之后,仍能与从机进行增量复制而非全量复制(前提是开启了RDB)。但实际上在主节点宕机的情况下,会利用哨兵自动进行故障转移处理,将其中的一个从节点升级为主节点,其他从节点从新的主节点进行复制。通过新增的两个复制ID
和复制偏移量OFFSET
来确保故障转移后增量复制的正常进行。
为什么需要两个复制 ID 和 OFFSET 呢?
当故障切换后,slave
被提升为Master
,会将前任主节点复制ID作为其次要复制IDmaster_replid2
,并记录ID切换时的偏移量master_repl_offset+1
并保存到second_repl_offset
。这样,当其他slave
将与新master
同步时,将尝试使用旧主节点复制ID执行部分重新同步。但是当从机通过psync
请求提供的master_repl_offset
大于second_repl_offset
,则从机将丢弃所有数据并做全量同步来确保数据的一致。
4、特点
- 异步复制:使用异步复制,每次接收到写命令之后,先在内部写入数据,然后异步发送给slave服务器。且从Redis 2.8开始,从服务器通过
config ack offset
周期性的应答处理复制流中的数据量情况。 - 负载均衡:在主从复制的基础上,配合读写分离,主写从读,分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
- 数据冗余,读写分离:使用从机取代Master进行数据备份持久化,实现数据热备份及持久化数据冗余处理,免除Master将数据写入磁盘的消耗。**注意:**若主机没有配置持久化,应该关闭其主机自动重启,因为可能直接跳过
Redis Sentinel
过程,致使副本数据也被销毁。 - 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;
- 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
- 主从复制不阻塞slave服务器。当master服务器进行初始同步时,slave服务器返回的是以前旧版本的数据,可配置slave-serve-stale-data为
no
,则Slave
在同步过程收到的查询请求将返回错误(在删除旧数据集和加载新的数据集,在这个短暂时间内,从服务器仍会阻塞连接进来的请求)