参考

小林coding:https://xiaolincoding.com/redis/cluster/master_slave_replication.html#%E7%AC%AC%E4%B8%80%E6%AC%A1%E5%90%8C%E6%AD%A5

码哥字节: https://xie.infoq.cn/article/85b537627355b21b02760f306

 https://blog.csdn.net/succing/article/details/121230604

 

数据都是存储在一台服务器上,如果出事就完犊子了,比如:

  • 如果服务器发生了宕机,由于 数据恢复 是需要点时间,那么 这个期间是无法服务新的请求的
  • 如果这台服务器的 硬盘出现了故障,可能 数据就都丢失了

要避免这种单点故障,最好的办法是将数据备份到其他服务器上,让这些服务器也可以对外提供服务,这样即使有一台服务器出现了故障,其他服务器依然可以继续提供服务。

Redis 提供了 主从复制 模式,来避免上述的问题。

这个模式可以保证多台服务器的数据一致性,且主从服务器之间采用的是「读写分离」的方式。

主服务器可以进行读写操作,当发生写操作时自动将写操作同步给从服务器,而从服务器一般是只读,并接受主服务器同步过来写操作命令,然后执行这条命令。

可以使用 replicaof(... 的副本之意。Redis 5.0 之前使用 slaveof)命令形成主服务器和从服务器的关系。

比如,现在有服务器 A 和 服务器 B,我们在服务器 B 上执行下面这条命令:

# 服务器 B 执行这条命令
replicaof <服务器 A 的 IP 地址> <服务器 A 的 Redis 端口号>

 服务器B 就成了 服务器A 的从节点

 

分担主服务器的压力(“主-从-从”模式

主从服务器在第一次数据全量同步的过程中,主服务器会做两件耗时的操作:生成 RDB 文件 和 传输 RDB 文件

主服务器是可以有多个从服务器的,如果从服务器数量非常多,而且都与主服务器进行全量同步的话,就会带来两个问题:

  • 由于是通过 bgsave 命令来生成 RDB 文件的,那么主服务器就会忙于使用 fork() 创建子进程,如果主服务器的内存数据非大,在 执行 fork() 函数时是会阻塞主线程 的,从而使得 Redis 无法正常处理请求;
  • 传输 RDB 文件会用主服务器的网络带宽,会对主服务器响应命令请求产生影响。

Redis 也是一样的,从服务器可以有自己的从服务器,我们可以把拥有从服务器的从服务器当作经理角色,它不仅可以接收主服务器的同步数据,自己也可以同时作为主服务器的形式将数据同步给从服务器,组织形式如下图:

在「从服务器」上执行下面这条命令,使其作为目标服务器的从服务器:

replicaof <目标服务器的IP> 6379

此时如果目标服务器本身也是「从服务器」那么该目标服务器就会成为「经理」的角色,不仅可以接受主服务器同步的数据,也会把数据同步给自己旗下的从服务器,从而减轻主服务器的负担。

 

全量复制(第一次同步)

 

主从服务器间的第一次同步的过程可分为三个阶段:

 第一阶段:建立链接、协商同步

主节点启动成功后,我们启动一个从节点。从节点启动成功后,会向主节点发送一个要数据同步的命令,格式如下:

runid是主库redis实例的一个唯一标识id,是redis实例创建的时候自动生成的。offset是指从节点的复制偏移量。

psync $master_runid $slave_offset

从库第一次向主节点发送请求时,并不知道主库的runid,也没有偏移量,因为需要全量同步。这时的命令如下:

psync ? -1

主库收到这个请求后,回复一个下面命令,把自己的 runid 和 offset 发送给从库:

FULLRESYNC 响应命令的意图是采用全量复制的方式(因为前面的 psync offset 是 -1 ,所以需要全量同步)

FULLRESYNC $master_runid $master_offset

 第二阶段:主服务器同步数据给从服务器

主服务器会执行 bgsave 命令(子进程)来生成 RDB 文件,然后把文件发送给从服务器。

从服务器收到 RDB 文件后,会先清空当前的数据,然后载入 RDB 文件

但是,这期间的写操作命令并没有记录到刚刚生成的 RDB 文件中,主服务器需要在下面这三个时间间隙中将收到的写操作命令,写入到 replication buffer 缓冲区里(每个 redis client 一个 replication buffer,从库也是一个 client,类似地,AOF 重写时期间写入的是 aof_rewrite_buf):

    • 主服务器生成 RDB 文件期间;
    • 主服务器发送 RDB 文件给从服务器期间;
    • 「从服务器」加载 RDB 文件期间;

第三阶段:主服务器发送新写操作命令给从服务器

在主服务器生成的 RDB 文件发送完,从服务器收到 RDB 文件后,丢弃所有旧数据,将 RDB 数据载入到内存。完成 RDB 的载入后,会回复一个确认消息给主服务器。

接着,主服务器将 replication buffer 缓冲区里所记录的写操作命令 发送给从服务器,从服务器执行来自主服务器 replication buffer 缓冲区里发来的命令,这时主从服务器的数据就一致了。

至此,主从服务器的第一次同步的工作就完成了。

replication buffer 太小会引发的问题(满了会重新全量同步):

replication buffer 由 client-output-buffer-limit slave 设置,当这个值太小,缓冲区满了之后会导致主从复制连接断开

  1. 当 master-slave 复制连接断开,master 会释放连接相关的数据。replication buffer 中的数据也就丢失了,此时主从之间重新开始复制过程。
  2. 还有个更严重的问题,主从复制连接断开,导致主从上出现重新执行 bgsave 和 rdb 重传操作无限循环。

当主节点数据量较大,或者主从节点之间网络延迟较大时,可能导致该缓冲区的大小超过了限制,此时主节点会断开与从节点之间的连接;

这种情况可能引起全量复制 -> replication buffer 溢出导致连接中断 -> 重连 -> 全量复制 -> replication buffer 缓冲区溢出导致连接中断……的循环。

具体详情:[top redis headaches for devops – replication buffer] 因而推荐把 replication buffer 的 hard/soft limit 设置成 512M。

 

 

replication buffer 和 repl_backlog_buffer 

replication buffer

replication 顾名思义,就是复制的意思;buffer是缓冲区的意思,两者合在一起 replication_buffer 就是复制缓冲区;

redis 会每一个 redis client (从库也是一个 client)创建一个独立的 replication buffer,通过该 buffer 传输全量同步期间的数据。

主节点的写操作是异步复制到从节点的,所以要这个缓冲区暂时存放主节点的写命令。

使用场景(主要是全量同步阶段, bgsave 期间增量;其他还有后面命令传播时对从节点异步复制的一个缓冲区)

当且仅当 slave 与 master 首次或者出于某种原因,需要 全量 rdb 传输数据主从同步时

  1. 全量同步的 bgsave 期间增量:主从节点第一次建立连接,全量同步
  2. 全量同步的 bgsave 期间增量:一个从库如果和主库断连时间过长,造成它在主库 repl_backlog_buffer 的 slave_repl_offset 位置上的数据已经被覆盖掉了,此时从库和主库间将进行全量复制。

会把 bgsave 时候(下面三个阶段)产生的新数据暂时存放在 replication buffer 中。 最后全量传给slave。

    • 主服务器生成 RDB 文件期间;
    • 主服务器发送 RDB 文件给从服务器期间;
    • 「从服务器」加载 RDB 文件期间;

repl_backlog_buffer 

backlog英文释义,是积压的意思;三者合在一起 replication_backlog_buffer,就是复制积压缓冲区。

一个环形缓冲区。多个从库共用一个环形缓冲区,大小是固定的,可存储的命令有限,超出部分将会被删除。

使用场景(命令传播期阶段,常规增量、断连增量)

  1. 常规增量:当主从全量同步后,master 和 slave 之间维持 TCP长连接,从节点心跳发送自己的 slave_repl_offset。master 与自己的 master_repl_offset 对比,处理它们之间丢失的命令。
  2. 断连增量:在网络断连阶段,主库可能会收到新的写操作命令,所以 master_repl_offset会大于 slave_repl_offset。master 只需要把自己的 master_repl_offset 与从节点的 slave_repl_offset之间的命令同步给从库即可。

关于 Replication ID, offset

Redis 使用一对 Replicaion ID, offset 来唯一识别 Master 节点数据集的版本

Replication ID

独有固定的 Replication ID,在其成为master角色的那一刻,由源码可知在第一个从节点加入时,Redis初始化了复制ID(随机生成的字符串)。

offset

Master 节点每当有新的写数据进来时,每进来1个字节 offset 就会自增+1,此处记做 master_repl_offset

maser通过全量rdb把数据同步给slave后,后面的通信便步入增量通信阶段。每次slave接收到的增量数据后,都会自己标记一个offset,此处记做 slave_repl_offset每一个 redis client 都会有一个自己的 slave_repl_offset。

master_repl_offset 和 slave_repl_offset ,中间相差的这一部分,即为本次要增量传输的。

 

 

(平时)增量复制(基于长连接)

主从服务器在完成第一次同步后,双方之间就会维护一个 TCP 长连接的,目的是避免频繁的 TCP 连接和断开带来的性能开销

后续主服务器可以通过这个连接继续将写操作命令传播给从服务器,然后从服务器执行该命令,使得与主服务器的数据库状态相同。

在命令传播阶段,除了发送写命令,主从节点还维持着 心跳机制PING 和 REPLCONF ACK。

主->从:PING

每隔指定的时间,主节点会向从节点发送 PING 命令,这个 PING 命令的作用,主要是为了让从节点进行超时判断。

从->主:REPLCONF ACK

在命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令:

REPLCONF ACK <replication_offset>

其中 replication_offset 是从服务器当前的复制偏移量。发送 REPLCONF ACK 命令对于主从服务器有三个作用:

  1. 检测主从服务器的网络连接状态。

  2. 辅助实现 min-slaves 选项。

  3. 检测命令丢失, 从节点发送了自身的 slave_replication_offset,主节点会用自己的 master_replication_offset 对比,如果从节点数据缺失,主节点会从 repl_backlog_buffer 缓冲区中找到并推送缺失的数据。注意,offset 和 repl_backlog_buffer 缓冲区,不仅可以用于部分复制,也可以用于处理命令丢失等情形;区别在于前者是在断线重连后进行的,而后者是在主从节点没有断线的情况下进行的。

 

(网络断开期间)增量复制

如果主从服务器间的网络连接断开了,那么就无法进行命令传播了,这时从服务器的数据就没办法和主服务器保持一致了,客户端就可能从「从服务器」读到旧的数据

如果此时断开的网络,又恢复正常了,要怎么继续保证主从服务器的数据一致性呢?

  • Redis 2.8 之前,如果主从服务器在命令同步时出现了网络断开又恢复的情况,从服务器就会和主服务器重新进行一次全量复制,很明显这样的开销太大了,必须要改进一波。
  • 所以,从 Redis 2.8 开始,网络断开又恢复后,从主从服务器会采用 增量复制 的方式继续同步,也就是只会把 网络断开期间主服务器接收到的写操作命令,同步给从服务器。

正常情况下,这两个偏移量基本相等。在网络断连阶段,主库可能会收到新的写操作命令,所以 master_repl_offset会大于 slave_repl_offset

当主从断开重连后,slave 会先发送 psync 命令给 master,同时将自己的 runIDslave_repl_offset 发送给 master。

master 只需要把 master_repl_offsetslave_repl_offset之间的命令同步给从库即可。

增量复制执行流程如下图:

repl_backlog_buffer 太小会引发的问题

(满了会被新数据覆盖,如果 slave_repl_offset 被覆盖则这个从节点重新全量同步

repl_backlog_buffer 太小的话,从库在主库 repl_backlog_buffer slave_repl_offset 位置上的数据会很快被新数据覆盖掉,一旦被覆盖就会执行全量复制

可以调整 repl_backlog_size 这个参数用于控制缓冲区大小。计算公式:

repl_backlog_buffer = second * write_size_per_second
  1. second:从服务器断开重连主服务器所需的平均时间;
  2. write_size_per_second:master 平均每秒产生的命令数据量大小(写命令和数据大小总和);

例如,如果主服务器平均每秒产生 1 MB 的写数据,而从服务器断线之后平均要 5 秒才能重新连接上主服务器,那么复制积压缓冲区的大小就不能低于 5 MB。

为了安全起见,可以将复制积压缓冲区的大小设为2 * second * write_size_per_second,这样可以保证绝大部分断线情况都能用部分重同步来处理。

 

如何确定执行全量同步还是部分同步?

在 Redis 2.8 及以后,从节点可以发送 psync 命令请求同步数据,此时根据主从节点当前状态的不同,同步方式可能是全量复制或部分复制。本文以 Redis 2.8 及之后的版本为例。

关键就是 psync的执行:

另外,一个从库如果和主库断连时间过长,造成它在主库 repl_backlog_buffer 的 slave_repl_offset 位置上的数据已经被覆盖掉了,此时从库和主库间将进行全量复制。

 

 

灵魂问答

1、怎么判断 Redis 某个节点是否正常工作?

Redis 判断节点是否正常工作,基本都是通过互相的 ping-pong 心态检测机制,如果有一半以上的节点去 ping 一个节点的时候没有 pong 回应,集群就会认为这个节点挂掉了,会断开与这个节点的连接。

Redis 主从节点发送的心态间隔是不一样的,而且作用也有一点区别:

  • Redis 主节点默认每隔 10 秒对从节点发送 ping 命令,判断从节点的存活性和连接状态,可通过参数repl-ping-slave-period控制发送频率。
  • Redis 从节点每隔 1 秒发送 replconf ack{offset} 命令,给主节点上报自身当前的复制偏移量,目的是为了:
    • 实时监测主从节点网络状态;
    • 上报自身复制偏移量, 检查复制数据是否丢失, 如果从节点数据丢失, 再从主节点的复制缓冲区中拉取丢失数据。

2、主从复制架构中,过期key如何处理?

主节点处理了一个key或者通过淘汰算法淘汰了一个key,这个时间主节点模拟一条del命令发送给从节点,从节点收到该命令后,就进行删除key的操作。

3、Redis 主从同步时是同步复制还是异步复制?

Redis 主节点每次收到写命令之后,先写到内部的缓冲区,然后异步发送给从节点。

主节点并不会等到从节点实际执行完命令后,再把结果返回给客户端,而是主节点自己在本地执行完命令后,就会向客户端返回结果了。如果从节点还没有执行主节点同步过来的命令,主从节点间的数据就不一致了。

4、如何应对主从数据不一致?

之所以会出现主从数据不一致的现象,是因为主从节点间的命令复制是异步进行的,所以无法实现强一致性保证(主从数据时时刻刻保持一致)。

如何如何应对主从数据不一致?

第一种方法,尽量保证主从节点间的网络连接状况良好,避免主从节点在不同的机房。

第二种方法,可以开发一个外部程序来监控主从节点间的复制进度。具体做法:

  • Redis 的 INFO replication 命令可以查看主节点接收写命令的进度信息(master_repl_offset)和从节点复制写命令的进度信息(slave_repl_offset),所以,我们就可以开发一个监控程序,先用 INFO replication 命令查到主、从节点的进度,然后,我们用 master_repl_offset 减去 slave_repl_offset这样就能得到从节点和主节点间的复制进度差值了。
  • 如果某个从节点的进度差值大于我们预设的阈值,我们可以让客户端不再和这个从节点连接进行数据读取,这样就可以减少读到不一致数据的情况。不过,为了避免出现客户端和所有从节点都不能连接的情况,我们需要把复制进度差值的阈值设置得大一些。