Redis主从复制

主要参考的以下两篇

主从复制是怎么实现的? | 小林coding

彻底搞懂Redis主从复制原理及实战 - cooffeeli - 博客园 (cnblogs.com)

第一次同步

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

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

  • 第一阶段是建立链接、协商同步;
  • 第二阶段是主服务器同步数据给从服务器;
  • 第三阶段是主服务器发送新写操作命令给从服务器。

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

执行了 replicaof 命令后,从服务器就会给主服务器发送 psync 命令,表示要进行数据同步。

psync 命令包含两个参数,分别是主服务器的 runID 和复制偏移量offset

  • runID,每个 Redis 服务器在启动时都会自动生产一个随机的 ID 来唯一标识自己。当从服务器和主服务器第一次同步时,因为不知道主服务器的 run ID,所以将其设置为 "?"。
  • offset,复制偏移量

主服务器收到 psync 命令后,会用 FULLRESYNC 作为响应命令返回给对方。

并且这个响应命令会带上两个参数:主服务器的 runID 和主服务器目前的复制偏移量 offset。从服务器收到响应后,会记录这两个值。

FULLRESYNC 响应命令的意图是采用全量复制的方式,也就是主服务器会把所有的数据都同步给从服务器。

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

接着,主服务器会执行 bgsave 命令来生成 RDB 文件,然后把文件发送给从服务器。

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

需要注意,主服务器生成 RDB 这个过程是不会阻塞主线程的,因为 bgsave 命令是产生了一个子进程来做生成 RDB 文件的工作,是异步工作的,这样 Redis 依然可以正常处理命令。

但是,这期间的写操作命令并没有记录到刚刚生成的 RDB 文件中,这时主从服务器间的数据就不一致了。那么为了保证主从服务器的数据一致性,主服务器在下面这三个时间间隙中将收到的写操作命令,写入到 replication buffer 缓冲区里。

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

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

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

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

命令传播

 主从服务器在完成第一次同步后,双方之间就会维护一个 TCP 连接,这是一个长连接。后续主服务器可以通过这个连接继续将写操作命令传播给从服务器,然后从服务器执行该命令,使得与主服务器的数据库状态相同。

分摊主服务器的压力

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

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

通过这种方式,主服务器生成 RDB 和传输 RDB 的压力可以分摊到充当经理角色的从服务器

增量复制

在 Redis 2.8 之前,如果主从服务器在命令同步时出现了网络断开又恢复的情况,从服务器就会和主服务器重新进行一次全量复制,很明显这样的开销太大了,必须要改进一波。

从 Redis 2.8 开始,网络断开又恢复后,从主从服务器会采用增量复制的方式继续同步,也就是只会把网络断开期间主服务器接收到的写操作命令,同步给从服务器。

增量复制的实现,依赖于三个重要的概念:

  1. 复制偏移量offest(这个是可以无限递增的吗?如果可以回到0是不是会出现主offest小于从offest?)
  2. 复制积压缓冲区repl_backlog_buffer,一个环形缓冲区。一个主节点只分配一个 repl backlog buffer
  3. 服务器运行ID(runid)

1. 复制偏移量:

执行复制的双方,主从节点,分别会维护一个复制偏移量offset第一次同步时,其值为 -1。主节点每次向从节点同步了N字节数据后,将修改自己的复制偏移量offset+N;从节点每次从主节点同步了N字节数据后,将修改自己的复制偏移量offset+N

offset用于判断主从节点的数据库状态是否一致:如果二者offset相同,则一致;如果offset不同,则不一致,此时可以根据两个offset找出从节点缺少的那部分数据。

例如,如果主节点的offset是1000,而从节点的offset是500,那么部分复制就需要将offset为501-1000的数据传递给从节点。

2. 复制积压缓冲区:

主节点内部维护了一个固定长度的、先进先出(FIFO)队列 作为复制积压缓冲区,其默认大小为1MB
主节点进行命令传播时,不仅会将写命令同步到从节点,还会将写命令写入复制积压缓冲区

由于复制积压缓冲区定长且是先进先出,所以它保存的是主节点最近执行的写命令;时间较早的写命令会被挤出缓冲区。因此,当主从节点offset的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制。

从节点将offset发送给主节点后,主节点根据offset和缓冲区大小决定能否执行增量复制:

  • 如果offset偏移量之后的数据,仍然都在复制积压缓冲区里,则执行增量复制
  • 如果offset偏移量之后的数据已不在复制积压缓冲区中(数据已被挤出),则执行全量复制

为了提高网络中断时部分复制执行的概率,可以根据需要增大复制积压缓冲区的大小(通过配置repl-backlog-size);

可以根据服务器断线后重新连接上主服务器所需的平均 时间(以秒计算) * 主服务器平均每秒产生的写命令数据量大小估算。

例如如果网络中断的平均时间是60s,而主节点平均每秒产生的写命令(特定协议格式)所占的字节数为100KB,则复制积压缓冲区的平均需求为6MB,保险起见,可以设置为12MB,来保证绝大多数断线情况都可以使用增量复制。

3. 服务器运行ID(runid):

每个Redis节点,都有其运行ID,运行ID由节点在启动时自动生成,主节点会将自己的运行ID发送给从节点,从节点会将主节点的运行ID存起来。
从节点Redis断开重连的时候,就是根据运行ID来判断同步的进度:

  • 如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会继续尝试使用增量复制(到底能不能增量复制还要看offset和复制积压缓冲区的情况);
  • 如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的Redis节点并不是当前的主节点,只能进行全量复制。

 当主服务器在 repl_backlog_buffer 中找到主从服务器差异的数据后,就会将增量的数据写入到 replication buffer 缓冲区。

如何判断Redis某个节点是否正常工作?

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

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

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

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

在主从复制场景下,为了主从节点的数据一致性,从节点不会主动删除数据,而是由主节点控制从节点中过期数据的删除。

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

Redis 3.2中,从节点在读取数据时,增加了对数据是否过期的判断:如果该数据已过期,则不返回给客户端;

主从复制中两个 Buffer(replication buffer 、repl backlog buffer)有什么区别?

  • 出现的阶段不一样:
    • repl backlog buffer 是在增量复制阶段出现,一个主节点只分配一个 repl backlog buffer
    • replication buffer 是在全量复制阶段和增量复制阶段都会出现,主节点会给每个新连接的从节点,分配一个 replication buffer
  • 这两个 Buffer 都有大小限制的,当缓冲区满了之后,发生的事情不一样:
    • 当 repl backlog buffer 满了,因为是环形结构,会直接覆盖起始位置数据;
    • 当 replication buffer 满了,会导致连接断开,删除缓存,从节点重新连接,重新开始全量复制

如何解决主从数据不一致?

为什么会出现主从数据不一致?

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

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

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

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

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

在命令传播阶段以外的其他情况下,从节点的数据不一致可能更加严重,例如在数据同步阶段,或从节点失去与主节点的连接时等。从节点的slave-serve-stale-data参数便与此有关:它控制这种情况下从节点的表现;如果为yes(默认值),则从节点仍能够响应客户端的命令,如果为no,则从节点只能响应info、slaveof等少数命令。该参数的设置与应用对数据一致性的要求有关;如果对数据一致性要求很高,则应设置为no。

如何解决数据丢失?

主从架构中,产生数据丢失的情况有两种:

  • 异步复制同步丢失
  • 集群产生脑裂数据丢失

异步复制同步丢失

对于 Redis 主节点与从节点之间的数据复制,是异步复制的,当客户端发送写请求给主节点的时候,客户端会返回 ok,接着主节点将写请求异步同步给各个从节点,但是如果此时主节点还没来得及同步给从节点时发生了断电,那么主节点内存中的数据会丢失。

减少异步复制的数据丢失的方案

Redis 配置里有一个参数 min-slaves-max-lag,表示一旦所有的从节点数据复制和同步的延迟都超过了 min-slaves-max-lag 定义的值,那么主节点就会拒绝接收任何请求

假设将 min-slaves-max-lag 配置为 10s 后,根据目前 master->slave 的复制速度,如果数据同步完成所需要时间超过10s,就会认为 master 未来宕机后损失的数据会很多,master 就拒绝写入新请求。这样就能将 master 和 slave 数据差控制在10s内,即使 master 宕机也只是这未复制的 10s 数据。

对于客户端,当客户端发现 master 不可写后,我们可以采取降级措施,将数据暂时写入本地缓存和磁盘中,在一段时间(等 master 恢复正常)后重新写入 master 来保证数据不丢失,也可以将数据写入 kafka 消息队列,等 master 恢复正常,再隔一段时间去消费 kafka 中的数据,让将数据重新写入 master 。

集群产生脑裂数据丢失

脑裂现象“”由于网络问题,集群节点之间失去联系。主从数据不同步;重新平衡选举,产生两个主服务。等网络恢复,旧主节点会降级为从节点,再与新主节点进行同步复制的时候,由于会从节点会清空自己的缓冲区,所以导致之前客户端写入的数据丢失了。

减少脑裂的数据丢的方案

当主节点发现「从节点下线的数量太多」,或者「网络延迟太大」的时候,那么主节点会禁止写操作,直接把错误返回给客户端。

在 Redis 的配置文件中有两个参数我们可以设置:

  • min-slaves-to-write x,主节点必须要有至少 x 个从节点连接,如果小于这个数,主节点会禁止写数据。
  • min-slaves-max-lag x,主从数据复制和同步的延迟不能超过 x 秒,如果主从同步的延迟超过 x 秒,主节点会禁止写数据。

我们可以把 min-slaves-to-write 和 min-slaves-max-lag 这两个配置项搭配起来使用,分别给它们设置一定的阈值,假设为 N 和 T。

这两个配置项组合后的要求是,主节点连接的从节点中至少有 N 个从节点,「并且」主节点进行数据复制时的 ACK 消息延迟不能超过 T 秒,否则,主节点就不会再接收客户端的写请求了。

即使原主节点是假故障,它在假故障期间也无法响应哨兵心跳,也不能和从节点进行同步,自然也就无法和从节点进行 ACK 确认了。这样一来,min-slaves-to-write 和 min-slaves-max-lag 的组合要求就无法得到满足,原主节点就会被限制接收客户端写请求,客户端也就不能在原主节点中写入新数据了

等到新主节点上线时,就只有新主节点能接收和处理客户端请求,此时,新写的数据会被直接写到新主节点中。而原主节点会被哨兵降为从节点,即使它的数据被清空了,也不会有新数据丢失。

假设我们将 min-slaves-to-write 设置为 1,把 min-slaves-max-lag 设置为 12s,把哨兵的 down-after-milliseconds 设置为 10s,主节点因为某些原因卡住了 15s,导致哨兵判断主节点客观下线,开始进行主从切换。同时,因为原主节点卡住了 15s,没有一个从节点能和原主节点在 12s 内进行数据复制,原主节点也无法接收客户端请求了。这样一来,主从切换完成后,也只有新主节点能接收请求,不会发生脑裂,也就不会发生数据丢失的问题了。

复制超时

主从节点复制超时是导致复制中断的最重要的原因之一

在复制连接建立过程中及之后,主从节点都有机制判断连接是否超时,其意义在于:

  1. 如果主节点判断连接超时,其会释放相应从节点的连接,从而释放各种资源,否则无效的从节点仍会占用主节点的各种资源(输出缓冲区、带宽、连接等);此外连接超时的判断可以让主节点更准确的知道当前有效从节点的个数,有助于保证数据安全(配合前面讲到的min-slaves-to-write等参数)。

  2. 如果从节点判断连接超时,则可以及时重新建立连接,避免与主节点数据长期的不一致。

主从复制超时判断的核心,在于repl-timeout参数,该参数规定了超时时间的阈值(默认60s),对于主节点和从节点同时有效;主从节点触发超时的条件分别如下:

  1. 主节点:每秒1次调用复制定时函数replicationCron(),在其中判断当前时间距离上次收到各个从节点REPLCONF ACK的时间,是否超过了repl-timeout值,如果超过了则释放相应从节点的连接。

  2. 从节点:从节点对超时的判断同样是在复制定时函数中判断,基本逻辑是:

    • 如果当前处于连接建立阶段,且距离上次收到主节点的信息的时间已超过repl-timeout,则释放与主节点的连接;
    • 如果当前处于数据同步阶段,且收到主节点的RDB文件的时间超时,则停止数据同步,释放连接;
    • 如果当前处于命令传播阶段,且距离上次收到主节点的PING命令或数据的时间已超过repl-timeout值,则释放与主节点的连接。

与复制阶段连接超时有关的一些实际问题:

  1. 数据同步阶段:在主从节点进行全量复制bgsave时,主节点需要首先fork子进程将当前数据保存到RDB文件中,然后再将RDB文件通过网络传输到从节点。如果RDB文件过大,主节点在fork子进程+保存RDB文件时耗时过多,可能会导致从节点长时间收不到数据而触发超时;此时从节点会重连主节点,然后再次全量复制,再次超时,再次重连……这是个悲伤的循环。为了避免这种情况的发生,除了注意Redis单机数据量不要过大,另一方面就是适当增大repl-timeout值,具体的大小可以根据bgsave耗时来调整。

  2. 命令传播阶段:如前所述,在该阶段主节点会向从节点发送PING命令,频率由repl-ping-slave-period控制;该参数应明显小于repl-timeout值(后者至少是前者的几倍)。否则,如果两个参数相等或接近,网络抖动导致个别PING命令丢失,此时恰巧主节点也没有向从节点发送数据,则从节点很容易判断超时。

  3. 慢查询导致的阻塞:如果主节点或从节点执行了一些慢查询(如keys *或者对大数据的hgetall等),导致服务器阻塞;阻塞期间无法响应复制连接中对方节点的请求,可能导致复制超时。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值