跳至主要內容

Redis-高可用篇-①主从复制

holic-x...大约 21 分钟RedisRedis

Redis-高可用篇-①主从复制

学习核心

  • 理解主从复制的目的和实现

学习资料

主从复制

1.主从复制模式

主从复制的引入是为了避免传统单机部署模式下服务器出现单点故障问题,这个模式可以保证多台服务器的数据一致性,且主从服务器之间采用的是【读写分离】的方式。主服务器可以进行读写操作,当发生写操作时自动将写操作同步给从服务器,而从服务器一般是只读,并接受主服务器同步过来写操作命令,然后执行这条命令

image-20240719171159005

​ 主从复制有三种模式:全量复制、基于长连接的命令传播、增量复制

  • 全量复制:主从复制第一次同步的时候采用的是全量复制

    • 此时主服务器在生成RDB文件和传输RDB文件会比较耗时,因此可以通过构建多级的主从关系,用于分摊主服务器的压力
  • 基于长连接的命令传播:主从复制第一次同步后会维护一个长连接,后续主服务器执行新的写操作命令时会通过这个连接将新的写命令传给从服务器,进而确保主从服务器的数据一致性

  • 增量复制:增量复制是针对网络延迟或断开的场景引入的,它与repl-backlog-size配置的大小有关

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

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

      • 如果repl-backlog-size配置的过小,主从服务器网络恢复时,可能发生「从服务器」想读的数据已经被覆盖了,那么这时就会导致主服务器采用全量复制的方式
      • 一般结合实际场景调大repl-backlog-size配置,进而降低主从服务器断开后重连发生全量同步的概率

2.主从复制如何实现?

第一次同步

​ 使用replicaof(Redis5.0之前使用slaveof)命令形成主服务器和从服务器的关系。例如此处有服务器A、服务器B

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

​ 此时服务器B就会变成服务器A的从服务器,然后与主服务器进行第一次同步。主从服务器间的第一次同步过程可分为3个阶段:

  • 阶段1:建立链接、协商同步;
  • 阶段2:主服务器同步数据给从服务器;
  • 阶段3:主服务器发送新写操作命令给从服务器;

image-20240719172749427

第一阶段:建立链接、协商同步(准备阶段)

  • 【1.1】从服务器执行replicaof命令,建立主从关系

  • 【1.2】执行了 replicaof 命令后,从服务器就会给主服务器发送 psync 命令,表示要进行数据同步。psync 命令包含两个参数主服务器的 runID复制进度 offset

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

    • offset:表示复制的进度,第一次同步时,其值为 -1

  • 【1.3】主服务器收到 psync 命令后,会用 FULLRESYNC 作为响应命令返回给对方。附带参数主服务器的 runID主服务器目前的复制进度 offset

  • 【1.4】从服务器收到响应后,会记录这两个值。FULLRESYNC 响应命令的意图是采用全量复制的方式,也就是主服务器会把所有的数据都同步给从服务器。

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

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

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

​ 此处需注意,主服务器生成 RDB 这个过程是不会阻塞主线程的,因为 bgsave 命令是产生了一个子进程来做生成 RDB 文件的工作(异步工作),这个过程中 Redis 依然可以正常处理命令。但是,这期间的写操作命令并没有记录到刚刚生成的 RDB 文件中,这时主从服务器间的数据就不一致了。那么为了保证主从服务器的数据一致性,主服务器在下面这三个时间间隙中将收到的写操作命令,写入到 replication buffer 缓冲区里

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

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

​ 完成阶段2操作后(即从服务器接收RDB文件并清空旧数据以加载新的RDB文件)会回复一个确认消息给主服务器。随后主服务器将 replication buffer 缓冲区里所记录的写操作命令发送给从服务器,从服务器接收命令并执行,进而确保主从服务器的数据致

命令传播

基于长连接的命令传播:主从服务器在完成第一次同步后,双方之间就会维护一个 TCP 连接。后续主服务器可以通过这个连接继续将写操作命令传播给从服务器,然后从服务器执行该命令,使得与主服务器的数据库状态相同。且这个连接是长连接的,目的是避免频繁的 TCP 连接和断开带来的性能开销。

image-20240719174132925

​ 基于长连接的命令传播 可以拆解关键字进行理解:

  • 为什么是长连接?:避免频繁的 TCP 连接和断开带来的性能开销
  • 命令传播?:主从服务器在完成第一次同步后,双方之间就会维护一个 TCP 连接,后续主从服务器可以继续用这个连接进行交互(主服务器传输写操作命令,从服务器接收并执行)

分摊主服务器压力

主服务器的压力来源

​ 主从服务器在第一次数据同步的过程中,主服务器会做两件耗时的操作:生成 RDB 文件和传输 RDB 文件。主服务器是可以有多个从服务器的,如果从服务器数量非常多,而且都与主服务器进行全量同步的话,就会带来两个问题:

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

如何分摊主服务器压力?

​ 类比现实场景案例:就好像刚创业的公司,由于人不多,所以员工都归老板一个人管,但是随着公司的发展、人员的扩充,老板慢慢就无法承担全部员工的管理工作。要解决这个问题,老板就需要设立经理职位,由经理管理多名普通员工,然后老板只需要管理经理就好

​ 类似地,Redis 的从服务器也可以有自己的从服务器,可以将某个从服务器设置为“经理角色的从服务器”,主服务器生成 RDB 和传输 RDB 的压力可以分摊到充当经理角色的从服务器

image-20240719175617063

​ 类似地,还是通过replicaof指令构建主从关系:replicaof <目标服务器的IP> [端口号],如果目标服务器本身为从服务器,则其相应就会变成“经理角色”,它既可以接收主服务器同步的数据,还可以将数据同步给旗下的从服务器,从而减轻主服务器的负担

image-20240719180445242

增量复制

​ 主从服务器在完成第一次同步后,就会基于长连接进行命令传播。如果某个时刻出现网络延迟或中断的情况,则会有存在异常问题。如果主从服务器间的网络连接断开了,那么就无法进行命令传播了,进而导致此时从服务器的数据没办法和主服务器保持一致,导致客户端可能从「从服务器」读到旧的数据

​ 如果某个时刻断开的网络又恢复正常了,要怎么继续保证主从服务器的数据一致性呢?在 Redis 2.8 之前,如果主从服务器在命令同步时出现了网络断开又恢复的情况,从服务器就会和主服务器重新进行一次全量复制,很明显这样的开销太大,必须进行优化。从 Redis 2.8 开始,网络断开又恢复后,从主从服务器会采用增量复制的方式继续同步,也就是只会把网络断开期间主服务器接收到的写操作命令,同步给从服务器。

image-20240719182415024

增量复制过程分析:

  • 从服务器在恢复网络后,会发送 psync 命令给主服务器,此时的 psync 命令里的 offset 参数不是 -1;
  • 主服务器收到该命令后,然后用 CONTINUE 响应命令告诉从服务器接下来采用增量复制的方式同步数据;
  • 主服务将主从服务器断线期间所执行的写命令发送给从服务器,由从服务器接收命令并执行;

主服务器怎么知道要将哪些增量数据发送给从服务器呢?

  • repl_backlog_buffer,是一个「环形」缓冲区,用于主从服务器断连后,从中找到差异的数据;
  • replication offset,标记上面那个缓冲区的同步进度,主从服务器都有各自的偏移量,主服务器使用 master_repl_offset 来记录自己「」到的位置,从服务器使用 slave_repl_offset 来记录自己「」到的位置。

repl_backlog_buffer 缓冲区的写入:在主服务器进行命令传播时,不仅会将写命令发送给从服务器,还会将写命令写入到 repl_backlog_buffer 缓冲区里,因此这个缓冲区里会保存着最近传播的写命令。

​ 网络断开后,当从服务器重新连上主服务器时,从服务器会通过 psync 命令将自己的复制偏移量 slave_repl_offset 发送给主服务器,主服务器根据自己的 master_repl_offset 和 slave_repl_offset 之间的差距,然后来决定对从服务器执行哪种同步操作:

  • 如果判断出从服务器要读取的数据还在 repl_backlog_buffer 缓冲区里,那么主服务器将采用增量同步的方式;
  • 相反,如果判断出从服务器要读取的数据已经不存在 repl_backlog_buffer 缓冲区里,那么主服务器将采用全量同步的方式;

​ 当主服务器在 repl_backlog_buffer 中找到主从服务器差异(增量)的数据后,就会将增量的数据写入到 replication buffer 缓冲区,用于缓存将要传播给从服务器的命令。

image-20240719184045097

​ repl_backlog_buffer 缓行缓冲区的默认大小是 1M,并且由于它是一个环形缓冲区,所以当缓冲区写满后,主服务器继续写入的话,就会覆盖之前的数据。因此,当主服务器的写入速度远超于从服务器的读取速度,缓冲区的数据一下就会被覆盖。那么在网络恢复时,如果从服务器想读的数据已经被覆盖了,主服务器就会采用全量同步,这个方式比增量同步的性能损耗要大很多。因此,为了避免在网络恢复时,主服务器频繁地使用全量同步的方式,应该调整下 repl_backlog_buffer 缓冲区大小,尽可能的大一些,减少出现从服务器要读取的数据被覆盖的概率,从而使得主服务器采用增量同步的方式。

repl_backlog_buffer 缓冲区的 repl_backlog_buffer 最小的大小可以根据公式估算:second * write_size_per_sceond

  • second 为从服务器断线后重新连接上主服务器所需的平均 时间(以秒计算)。
  • write_size_per_second 则是主服务器平均每秒产生的写命令数据量大小。

​ 举个例子,如果主服务器平均每秒产生 1 MB 的写命令,而从服务器断线之后平均要 5 秒才能重新连接主服务器。那么 repl_backlog_buffer 大小就不能低于 5 MB,否则新写地命令就会覆盖旧数据了。为了应对一些突发的情况,可以将 repl_backlog_buffer 的大小设置为此基础上的 2 倍,也就是 10 MB。

#  repl_backlog_buffer 大小修改(修改配置文件里参数项的值)
repl-backlog-size 1mb

3.主从复制扩展问题

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

​ Redis 判断节点是否正常工作,基本都是通过互相的 ping-pong 心态检测机制,如果有一半以上的节点去 ping 一个节点的时候没有 pong 回应,集群就会认为这个节点挂掉了,会断开与这个节点的连接。Redis 主从节点发送的心态间隔是不一样的,而且作用也有一点区别:

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

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

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

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

replication buffer 、repl backlog buffer 区别如下:

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

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

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

​ 主从数据不一致,就是指客户端从从节点中读取到的值和主节点中的最新值并不一致。之所以会出现主从数据不一致的现象,是因为主从节点间的命令复制是异步进行的,所以无法实现强一致性保证(主从数据时时刻刻保持一致)。

​ 在主从节点命令传播阶段,主节点收到新的写命令后,会发送给从节点。但是,主节点并不会等到从节点实际执行完命令后,再把结果返回给客户端,而是主节点自己在本地执行完命令后,就会向客户端返回结果了。如果从节点还没有执行主节点同步过来的命令(例如出现网络延迟或断开),主从节点间的数据就不一致了。

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

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

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

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

主从切换如何减少数据丢失?

​ 主从切换过程中,产生数据丢失的情况有两种:异步复制同步丢失集群产生脑裂数据丢失,实际上不可能保证数据完全不丢失,只能做到使得尽量少的数据丢失。

(1)异步复制同步丢失

​ 对于 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 。

(2)集群产生脑裂数据丢失

​ 先来理解集群的脑裂现象,这就好比一个人有两个大脑,那么到底受谁控制呢在 Redis 主从架构中,部署方式一般是「一主多从」,主节点提供写操作,从节点提供读操作。如果主节点的网络突然发生了问题,它与所有的从节点都失联了,但是此时的主节点和客户端的网络是正常的,这个客户端并不知道 Redis 内部已经出现了问题,还在照样的向这个失联的主节点写数据(过程A),此时这些数据被主节点缓存到了缓冲区里,因为主从节点之间的网络问题,这些数据都是无法同步给从节点的。

​ 这时,哨兵也发现主节点失联了,它就认为主节点挂了(但实际上主节点正常运行,只是网络出问题了),于是哨兵就会在从节点中选举出一个 leeder 作为主节点,这时集群就有两个主节点了 —— 脑裂出现了

​ 如果网络突然好了,哨兵因为之前已经选举出一个新主节点了,它就会把旧主节点降级为从节点(A),然后从节点(A)会向新主节点请求数据同步,因为第一次同步是全量同步的方式,此时的从节点(A)会清空掉自己本地的数据,然后再做全量同步。所以,之前客户端在过程 A 写入的数据就会丢失了,也就是集群产生脑裂数据丢失的问题

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

减少脑裂的数据丢失的方案

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

在 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 内进行数据复制,原主节点也无法接收客户端请求了。这样一来,主从切换完成后,也只有新主节点能接收请求,不会发生脑裂,也就不会发生数据丢失的问题了。

主从如何做到故障自动切换?

​ 主节点挂了 ,从节点是无法自动升级为主节点的,这个过程需要人工处理,在此期间 Redis 无法对外提供写操作。此时,需引入Redis 哨兵机制,哨兵在发现主节点出现故障时,由哨兵自动完成故障发现和故障转移,并通知给应用方,从而实现高可用性。

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3