主从复制

概述

Redis主从复制是指:主机数据更新后根据配置和策略, 自动将一台 Redis服务器的数据复制到其它的 Redis 服务器,前者所在的 Redis服务器也被称为 “主节点”(Master / Leader),后者则被称为 “从节点”(Slave / Follower)。

image-20230418141218412

读操作:主库、从库都可以接收;

写操作:首先到主库执行,然后,主库将写操作同步给从库。

作用:

  • 实现读写分离,性能扩展
  • 能够在Redis服务器崩掉后快速恢复(因为在其他服务器上进行了数据备份)

复制过程

当我们启动多个 Redis 实例的时候,它们相互之间就可以通过 replicaof(Redis 5.0 之前使用 slaveof)命令形成主库和从库的关系,之后会按照三个阶段完成数据的第一次同步。

  1. 第一阶段:是主从库间建立连接、协商同步的过程,主要是为全量复制做准备。(从库给主库发送 psync 命令,表示要进行数据同步,主库根据这个命令的参数来启动复制。psync 命令包含了主库的 runID复制进度 offset 两个参数。)
  2. 在第二阶段:主库将所有数据同步给从库。从库收到数据后,在本地完成数据加载。这个过程依赖于内存快照生成的 RDB 文件。(从库接收到 RDB 文件后,会先清空当前数据库,然后加载 RDB 文件)
  3. 第三个阶段:主库会把第二阶段执行过程中新收到的写命令,再发送给从库。
image-20230417160818398

问题一:

一次全量复制中,对于主库来说,需要完成两个耗时的操作:生成 RDB 文件和传输 RDB 文件。如果从库数量很多,而且都要和主库进行全量复制的话,就会导致主库忙于 fork 子进程生成 RDB 文件,进行数据全量同步,导致fork操作阻塞主线程

解决:通过“主 - 从 - 从”模式将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上。

问题二:

网络断连或阻塞,如果从库和主库重新进行一次全量复制,开销非常大。

解决:增量复制

当主从库断连后,主库会把断连期间收到的写操作命令,写入 replication buffer,同时也会把这些操作命令也写入 repl_backlog_buffer 这个缓冲区(repl_backlog_buffer 是一个环形缓冲区(所以如果长时间不恢复或者读取速度太慢了,就会被新数据覆盖导致主从库间的数据不一致。),主库会记录自己写到的位置,从库则会记录自己已经读到的位置。)。

主从库的连接恢复之后,从库首先会给主库发送 psync 命令,并把自己当前的 slave_repl_offset 发给主库,主库会判断自己的 master_repl_offset 和 slave_repl_offset 之间的差距。并将中间语句同步给从库。

配置

  1. 配置多个服务器

    1. 创建多个redis的配置文件

    2. 在每个配置文件中配置:

      1
      2
      3
      4
      5
      6
      include redis中的配置文件
      pidfile /var/run/redis_6379.pid #配置pid文件位置
      port 6379 #配置每个主机端口号
      dbfilename dump6379.rdb #配置RDB数据备份位置
      slave-priority 10 # 配置从机的优先级,值越小,优先级越高,用于选举主机时使用
      # slaveof <主机ip> <主机port> 如果配置了这个就会每次启动自动成为所写ip主机的从机
    3. 启动服务器redis-server 配置文件

    4. 让某个服务器成为从机:slaveof <主机ip> <主机port>

    5. 这样主机中写数据,从机都可以看到,但是不允许写

如果从机太多,可以让从机下面在挂从机,并且可以在主机挂了之后,使用slaveof no one成为主机

主从问题

数据不一致

主要原因:

  1. 主从库间的网络可能会有传输延迟,所以从库不能及时地收到主库发送的命令,从库上执行同步命令的时间就会被延后。
  2. 另一方面,即使从库及时收到了主库的命令,但是,也可能会因为正在处理其它复杂度高的命令(例如集合操作命令)而阻塞。此时,从库需要处理完当前的命令,才能执行主库发送的命令操作,这就会造成主从数据不一致。

两种方法:

  1. 在硬件环境配置方面,我们要尽量保证主从库间的网络连接状况良好

  2. 我们还可以开发一个外部程序来监控主从库间的复制进度

    因为 Redis 的 INFO replication 命令可以查看主库接收写命令的进度信息(master_repl_offset)和从库复制写命令的进度信息(slave_repl_offset),所以,我们就可以开发一个监控程序,先用 INFO replication 命令查到主、从库的进度,然后,我们用 master_repl_offset 减去 slave_repl_offset,这样就能得到从库和主库间的复制进度差值了。

    如果某个从库的进度差值大于我们预设的阈值,我们可以让客户端不再和这个从库连接进行数据读取,这样就可以减少读到不一致数据的情况。

  3. 将edis 中的 slave-serve-stale-data(能否处理数据读写命令) 配置项设置为 no,这样当请求来的时候会回复”正在从master同步”,除了 INFO 和 SLAVEOF 命令。

读取过期数据

主要原因:

  1. 定期删除策略:留存有没有被删除完的过期数据。
  2. 惰性删除策略:使用的是EXPIRE 和 PEXPIRE来设置过期时间,等同步到从库时产生延迟。

解决方法:

​ 使用 Redis 3.2 及以上版本;使用 EXPIREAT/PEXPIREAT 命令设置过期时间,避免从库上的数据过期时间滞后。

配置项设置得不合理从而导致服务挂掉

  1. protected-mode 配置项:

    这个配置项的作用是限定哨兵实例能否被其他服务器访问。当这个配置项设置为 yes 时,哨兵实例只能在部署的服务器本地进行访问。当设置为 no 时,其他服务器也可以访问这个哨兵实例。

    问题:如果 protected-mode 被设置为 yes,而其余哨兵实例部署在其它服务器,那么,这些哨兵实例间就无法通信。当主库故障时,哨兵无法判断主库下线,也无法进行主从切换,最终 Redis 服务不可用。

    解决:所以,我们在应用主从集群时,要注意将 protected-mode 配置项设置为 no,并且将 bind 配置项设置为其它哨兵实例的 IP 地址

  2. cluster-node-timeout 配置项

    这个配置项设置了 Redis Cluster 中实例响应心跳消息的超时时间。

    问题:如果执行主从切换的实例超过半数,而主从切换时间又过长的话,就可能有半数以上的实例心跳超时,从而可能导致整个集群挂掉。

    解决:将 cluster-node-timeout 调大些(例如 10 到 20 秒)

脑裂问题

所谓的脑裂,就是指在主从集群中,同时有两个主节点,它们都能接收写请求。而脑裂最直接的影响,就是客户端不知道应该往哪个主节点写入数据,结果就是不同的客户端会往不同的主节点上写入数据。而且,严重的话,脑裂会进一步导致数据丢失。

原因:

  • 在主从集群中发生数据丢失,最常见的原因就是主库的数据还没有同步到从库,结果主库发生了故障,等从库升级为主库后,未同步的数据就丢失了。

  • 发现是原主库假故障导致的脑裂:

    主库是由于某些原因无法处理请求,也没有响应哨兵的心跳,才被哨兵错误地判断为客观下线的。结果,在被判断下线之后,原主库又重新开始处理请求了,而此时,哨兵还没有完成主从切换,客户端仍然可以和原主库通信,客户端发送的写操作就会在原主库上写入数据了。

解决:

Redis 已经提供了两个配置项来限制主库的请求处理,分别是 min-slaves-to-write 和 min-slaves-max-lag。

  • min-slaves-to-write:这个配置项设置了主库能进行数据同步的最少从库数量;

  • min-slaves-max-lag:这个配置项设置了主从库间进行数据复制时,从库给主库发送 ACK 消息的最大延迟(以秒为单位)。

我们可以把 min-slaves-to-write 和 min-slaves-max-lag 这两个配置项搭配起来使用,分别给它们设置一定的阈值,假设为 N 和 T。这两个配置项组合后的要求是,主库连接的从库中至少有 N 个从库,和主库进行数据复制时的 ACK 消息延迟不能超过 T 秒,否则,主库就不会再接收客户端的请求了。

通讯开销问题

为了让集群中的每个实例都知道其它所有实例的状态信息,实例之间会按照一定的规则进行通信。这个规则就是 Gossip 协议

通信开销受到通信消息大小通信频率这两方面的影响

  • 通信消息大小

    包含自身状态信息、集群十分之一实例的状态信息、Slot映射表,大概有12KB。并且PING和PONG消息内容一致。一来一回就是24KB。随着集群规模增加,这些心跳消息越来越多占据部分宽带,降低正常服务的吞吐量。

  • 通信频率

    每秒会从本地实例随机挑选5个实例,并选择一个最久没有通信的实例,发送PING命令同步状态。但是由于是随机挑选的实例,很有可能会有遗漏的实例一直没有进行通信,导致集群状态信息过期。为了解决这种情况,Redis Cluster 的实例会按照每 100ms 一次的频率扫描本地的实例列表,如果发现有实例最近一次接收 PONG 消息的时间,已经大于配置项 cluster-node-timeout 的一半了(cluster-node-timeout/2),就会立刻给该实例发送 PING 消息,更新这个实例上的集群状态信息。

解决方案:

  1. 根本的解决方法就是降低集群规模,因为实例间的通信开销会随着实例规模增加而增大(理论单个实例每秒能支撑 8 万请求操作(8 万 QPS),那么,400~ 500 个实例可支持 1600 万~2000 万 QPS)

  2. 降低通信频率:

    提高cluster-node-timeout配置项(默认是15s),但是也不能调太大了,不然会遇到实例真的发生了故障没有得到及时的检测。

    可以在调整 cluster-node-timeout 值的前后,使用 tcpdump 命令抓取实例发送心跳信息网络包的情况

    例如,执行下面的命令后,我们可以抓取到 192.168.10.3 机器上的实例从 16379 端口发送的心跳网络包,并把网络包的内容保存到 r1.cap 文件中:tcpdump host 192.168.10.3 port 16379 -i 网卡名 -w /tmp/r1.cap

    通过分析网络包的数量和大小,就可以判断调整 cluster-node-timeout 值前后,心跳消息占用的带宽情况了。