当前位置: 首页 > article >正文

Redis--高可用(主从复制、哨兵模式、分片集群)

高可用(主从复制、哨兵模式、分片集群)

  • 高可用性
    • Redis如何实现高可用架构?
    • 主从复制
      • 原理
      • 1. 全量同步
      • 2. 命令传播
      • 3. 增量同步
    • Redis Sentinel(哨兵模式)
      • 为什么要有哨兵模式?
      • 哨兵机制是如何工作的?
        • 故障检测
        • 由哪个哨兵进行主从故障转移?
        • 主从故障转移的过程是怎样的?
      • 集群脑裂导致数据丢失怎么办?
        • 什么是脑裂?
        • 解决方案
    • Redis Cluster(分片集群)
      • 为什么需要Redis Cluster?
      • Redis Cluster 是如何分⽚的?
      • 为什么 Redis Cluster 的哈希槽是 16384 个?
      • Redis Cluster中的节点是怎么进行通信的?
        • Gossip 协议
        • 节点间的数据交换

高可用性

Redis如何实现高可用架构?

Redis 官方提供了三种分布式部署模式:

  • 主从复制:通过 slaveof 命令可以让任意的节点成为对应的节点的从节点,主节点通过主从复制机制向从主节点同步数据。其中,主节点提供读写功能,而从节点只提供读功能。当主节点出现故障时,需要手动进行主从切换完成故障转移。
  • Sentinel 哨兵模式:哨兵模式是主从的增强版本,它在主从模式的基础上添加了哨兵节点,哨兵节点通过心跳监控节点健康状态,并在主节点出现故障后,自动通过投票机制选举出新的主节点并完成故障转移。不过哨兵本身也需要做保证高可用,因此一般也会同时部署多个哨兵节点组成哨兵集群。
  • Cluster 集群模式:Redis Cluster 一般也叫分片集群,它是 Redis 提供的一个去中心化的集群部署方案。它将全部数据划分为 16394 个哈希槽,由集群中的每个节点负责其中的一部分槽位,使用时 Key 将通过哈希取模并最终指派到某个特定的槽位。在集群中,不同的 Redis 节点间通过 Gossip 协议与其他节点保持通信,每个主节点都将可以具备多个从节点,从节点不提供服务,当主节点出现故障时将会自动投票选举出新的主节点,并且完成故障转移。

此外,在 Redis Cluster 之前,也有一些基于中间代理路由或客户端路由的 Redis 的解决方案,比如 Jedis 自带的 ShardedJedis,或者推特的 Twemproxy。

主从复制

当 Redis 的访问量以及数据量随着随着业务规模一起扩大,单机部署的一些问题就逐渐体现出来,比如:

  • 服务器一旦宕机,所有 Redis 服务都不可用。
  • 读写请求全打到单个 Redis 实例上,会遇到性能瓶颈。

为了解决这些问题,Redis 中提供了主从复制(replication)的机制。简单的来说,当我们有多个 Redis 实例时,它们会被划分为两类节点

  • Master 主节点:负责处理客户端的读写请求,以及数据的修改操作。
  • Slave 从节点:通过复制主节点的数据,提供备份和读取服务。

img

也就是说,所有的数据修改只在主服务器上进行,然后将最新的数据同步给从服务器,这样就使得主从服务器的数据是一致的。

原理

当从节点初次连接到主节点,或者掉线重连后进度落后较多时会进行一次全量数据同步。此时,主节点会生成 RDB 快照并传输给从节点,在此期间,主节点接受到的增量命令将会先写入 replication_buffer 缓冲区,等到从节点加载完 RDB 快照的数据后,再将缓冲区的命令传输给从节点,以次完成初次同步。

当从节点掉线重连后,如果进度落后的不多,将会进行增量同步。主节点内部维护了一个环形的固定大小的 repl_backlog_buffer 缓冲区,它用于记录最近传播的命令。其中,主节点和从节点会分别在该缓冲区维护一个 offset ,用于表示自己的写进度和读进度。当从节点掉线重连后,将会检查主节点和从节点 offset 之差是否小于缓冲区大小,如果确实小于,说明从节点同步进度落后不多,则主节点将该缓冲区中的两 offset 之间的增量命令发送给从节点,完成增量同步。

当主从节点完成初次同步后,将会建立长连接进行命令传播。简单的来说,就是每当主节点执行一条命令,它就会写入 replication_buffer 缓冲区,随后再将缓冲区的命令通过节点间的长连接发送给对应的从节点。

1. 全量同步

一般来说,当两个节点第一次建立主从关系的时候,一定会触发一次全量同步。整个过程大致分为四步:

  1. 从节点向主节点发送 psync 请求获取 runID 和 offset;
  2. 主节点返回自己的 runID 与该从节点的 offset;
  3. 主节点生成 RDB 文件,并传输给从节点,从节点加载文件后获取全量数据。
  4. 主节点确认从节点加载完 RDB 文件后,将这期间缓存的增量命令发送给从节点,从节点加载完毕后结束第一次同步。

图片

1.获取 runID 与 offset

当我们对一个 Redis 实例使用 replicaof 让指定从节点与主节点构成主从关系时,从节点将会根据你指定的 host 和端口号请求对应的服务实例,并向主节点发送 psync {runID} {offset} 请求。

其中,runID 是主节点启动时生成的标识 ID,而 offset 则表示当前从节点从主节点复制数据的偏移量,也就是复制进度。最开始的时候,由于是第一次同步,因此从节点并不知道主节点的 runID,因此 runID 为 ?,由于也还没有复制过数据,因此 offset 为 -1。

当主节点响应请求时,将会返回主节点自己的 runID 和当前从节点的复制进度 offset。并且因为是首次同步,因此主节点会返回 FULLRESYNC 响应,表示从节点需要进行全量同步

2.加载 RDB 数据

在前文 Redis 的持久化部分,我们提到过,Redis 恢复数据或者进行主从同步的时候是通过 RDB 文件完成的,实际上指的就是全量同步。

在从节点接受了响应以后,主节点将会执行 bgsave 去生成一个 RDB 文件——这个流程与正常生成 RDB 文件一致——并发送给从节点,从节点接受后,由于是全量同步,因此会先清空自己的已有数据,然后再加载 RDB 文件中的数据

此时,从节点已经同步过来了主节点的大部分数据,不过由于 RDB 实际上只是一个快照,因此在生成 RDB 文件期间主节点的增量数据实际上还没有被从节点获取

  1. 获取增量数据

在主节点生成 RDB 文件期间,由于 RDB 文件是通过子进程异步生成的,因此在这个过程中主节点仍然还在正常的处理请求,这部分的增量命令将会写入 replication buffer 缓冲区

当从节点加载完 RDB 中的数据后,将会向主节点发送确认消息,此时主节点会再将缓冲区中的命令发送给从节点,从节点执行完这部分增量命令后,数据即与主节点基本一致,则全量同步完成。

不过,由于主从的更新已有延迟,因此无论如何数据是很难保证完全一致的,不过这就是后面命令的正常传播和增量同步的事情了。

2. 命令传播

当主从之间根据 psync 请求完成第一次全量或增量同步后,就会保持长连接,此后,将会通过长连接进行命令传播。

在这个过程中,主节点将会在每次执行完一个命令后,分别写两个缓冲区:

  • replication_buffer:主从复制缓冲区,主节点每拥有一个从节点,就会有一个对应的缓冲区,要传播给从节点的命令会先写入该缓冲区,随后在通过长连接发送给从节点。
  • repl_backlog_buffer:最近传播命令缓冲区,一个主节点只有一个,用于记录主节点最近传播出去的命令,主节点和全部从节点都会分别在上面维护一个 offset,用于表示自己已经写入或读取的命令进度。

在这里插入图片描述

3. 增量同步

在这里插入图片描述

repl_backlog_buffer 是主节点中一个比较特殊的缓冲区,和每个从节点都有一个 replication_buffer 不同,一个主节点只会有一个 repl_backlog_buffer

repl_backlog_buffer 是一个固定大小的“环形”区域,当主节点写入数据时,它会使用 master_offset 记录自己当前已经写到哪个字节,对应的,从节点也会有一个 slave_offset表示从节点已经读到的哪个字节。当主节点写的数据超过缓冲区大小后,它将会覆盖最早写过的内容。

在这里插入图片描述

因此,当从节点要与主节点进行同步时,仅需要在 psync 请求时给出自己的 slave_offset 即可,主节点将计算其与 master_offset 的差值:

  • 如果差值大于 repl_backlog_buffer 的大小,说明两者数据已经差了很多,那么需要重新进行全量同步。
  • 如果查找小于 repl_backlog_buffer 的大小,说明数据差还在容许范围内,则主节点将返回 CONTINUE 响应,让从节点准备进行增量同步,并把 repl_backlog_buffer 中的差值部分写入 replication_buffer 并发送给从节点,让从节点把同步进度追上来。

简单的来说,如果从节点最后一次读取的命令可以在上面找到,那么说明从节点的数据没有落后太多,因此可以增量同步,否则就需要进行全量同步。

当主从链接断开后,从节点会重新发送 psync 请求向主节点要求同步数据,在 2.8 之前总是会使用全量同步,而在 2.8 及以后的版本,将会使用增量同步。

综上,我们不难意识到,如果你的网络不太稳定,那么最好把 repl_backlog_buffer 调大一些,这样可以尽可能的避免从节点掉线重连后需要频繁的进行全量同步。

Redis Sentinel(哨兵模式)

为什么要有哨兵模式?

Redis 默认提供的主从模式不具备故障转移的能力,当主节点宕机后,需要用户手动的进行主从切换,在这段时间内整个集群实际上处于不可用的状态。为了解决这个问题,Redis 在 2.8 及以上版本提供了哨兵模式。

哨兵机制是如何工作的?

简单的来说,哨兵模式与普通的主从复制相比,新增了一类哨兵节点,它是一个特殊的 Redis 进程,哨兵节点本身不对外提供任何读写服务,它的作用是:

  • 监控与通知:与所有节点保持心跳,监控它们的监控状况。
  • 自动故障转移:当主节点故障后,自动选举新的主节点并完成主从切换,实现自动故障转移。
  • 通知告警:用户可以通过哨兵节点的 API 订阅消息,从而在节点状态异常时获得通知,又或者在完成主从切换后得知新的主节点地址。
故障检测

当哨兵节点启动以后,每隔 1 秒,它将会向集群中的主从节点与其他的哨兵节点发送 ping 请求,以确认对方的健康状况。如果在指定的超时时间 down-after-millisenconds 内没有收到响应,那么它就会认为没有响应的那个节点已经掉线,将其标记为 sdown。这种这种下线状态称为“主观下线”。

哨兵监控主从节点

如果下线的是从节点或者其他哨兵节点,那么直到其重新上线为止,哨兵都会认为其已经不可用。如果下线的是主节点,那么哨兵为了防止因为网络波动导致的误判,会向其他的哨兵节点发送 sentinel is-master-down-by-addr 请求,确认对方是否也认定该节点下线。当整个哨兵集群中有足够数量的哨兵(该值通过配置文件中的 quorum 进行配置)节点确认主节点主观下线,那么主节点就会被认定为“客观下线”,并标记为 odown

img

由哪个哨兵进行主从故障转移?

在一切开始前,哨兵集群会从所有的哨兵节点中挑选一个领头哨兵,领头哨兵将用来代表其他节点完成重新选主和主从切换的任务

Redis 基于 Raft 分布式共识算法来实现领头哨兵的选举,这个过程大致如下:

  1. 确认主节点客观下线的哨兵将成为候选者,它先投自己一票,然后邀请其他哨兵向自己投票;
  2. 其他哨兵如果尚未投票,则会将把赞同票投给邀请它投票的候选哨兵,否则就不投票;
  3. 所有的候选哨兵都统计自己的得票数,当得票数满足下述两条件时,该节点当选为领头哨兵:
    • 当得票数超过哨兵集群中节点数量的一半,即 n/2+1
    • 得票数且超过了认定节点客观下线所要求的哨兵数量(即配置文件中的 quorum 配置);
  4. 如果在规定时间内或投票结束后都没有选出领头哨兵,则再进行一轮投票,直到选出领头哨兵为止。

由于选举机制需要保证有一个节点可以拿到超半数票,因此为了保证哨兵集群的高可用,一个最小的哨兵集群至少需要有三个节点,这样当任意一个节点下线后,哨兵集群仍然可以正常选举出领头哨兵去做故障转移。

此外,我们可以注意到,由于故障检测只受 quorum 影响,而不受哨兵数量的影响。因此,当 quorum 被设置了一个过小的值 —— 比如为低于 n/2+1——的时候,可能会出现虽然哨兵确认节点客观下线了,但是却由于在线的哨兵数量不足以选举出领头哨兵,最终无法进行故障转移尴尬局面。

比如,我们有一个由五个节点组成的哨兵集群,当我们把 quorum 设置为 2 时,如果挂掉了三个哨兵节点,此时哨兵集群可以确认主节点已经客观下线,但是由于哨兵节点数量不足,无法选出领头哨兵,所以即使发现了问题也无法完成故障转移。

主从故障转移的过程是怎样的?

在哨兵集群中通过投票的方式,选举出了哨兵 leader 后,就可以进行主从故障转移的过程了,如下图:

img

主从故障转移操作包含以下四个步骤:

  • 第一步:在已下线主节点(旧主节点)属下的所有「从节点」里面,挑选出一个从节点,并将其转换为主节点。
  • 第二步:让已下线主节点属下的所有「从节点」修改复制目标,修改为复制「新主节点」;
  • 第三步:将新主节点的 IP 地址和信息,通过「发布者/订阅者机制」通知给客户端;
  • 第四步:继续监视旧主节点,当这个旧主节点重新上线时,将它设置为新主节点的从节点;

选举出领头哨兵后,它将从被自己监控的所有从节点中,先排除下述节点:

  • 排除所有已经掉线的从节点;
  • 排除五秒内没有响应哨兵的 INFO 命令的从节点(当出现主节点客观下线后,哨兵对节点的 INFO 命令已经变为一秒请求一次)。
  • 排除与主节点断连超过 down-after-milliseconds * 10 毫秒的从节点(down-after-milliseconds即为主节点的超时时间)。

然后剩下的节点都有资格参与选举,此时哨兵再根据以下条件选择出优先级最高的节点作为新的主节点:

  1. 优先级配置:先尝试根据 slave-priority 配置,选出一个优先级最高的节点;
  2. 数据同步进度:如果所有节点优先级配置均相同,则从中挑选出一个复制进度最高的(即主从复制中提到的从节点的 offset);
  3. 启动时间:如果所有的节点复制进度均相同,则挑选一个 runID 最小的节点(即最新的从节点)。

当选出一个新的主节点后,领头哨兵将会发送 slave no one 命令将该节点真正的升级为主节点,然后向其他的从节点发送 slaveof 命令让它们成为这个新主节点的从节点,至此,故障转移的就完成了。

需要注意的是,在这个过程中,由于主从复制延迟,当主从切换后,旧的主节点尚未同步从节点的数据可能就会丢失。

集群脑裂导致数据丢失怎么办?

什么是脑裂?

在正常情况下,当发生主从切换时,客户端将会从哨兵收到主节点切换的通知,然后与旧的主节点断开连接,并连接到新的主节点。不过,如果有的客户端与哨兵也断开了连接,那么它将无法意识到主节点已经切换,并且还是连接到旧的主节点。此时,整个 Redis 集群实际上同时有两个主节点在工作,这就是脑裂问题。

解决方案

我们可以通过调整两个参数防止这种情况出现:

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

分别给它们设置一定的阈值,假设为 N 和 T。

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

如此一来,当主节点掉线时,我们就可以让客户端能够及时发现问题,然后重新向哨兵确认当前的主节点。在最大限度的避免的数据不一致问题后,当旧的主节点节点重新上线,就可以放心的让它去当从节点,而不必担心数据丢失的问题。

不过,有得必有失,为了避免主节点“罢工”,min-replicas-to-write 调的越高,从节点就需要部署的越多,min-replicas-max-lag 调的越小,那主从节点间的网络就要越稳定。因此,实际使用时,还是需要结合自己的业务需求来衡量要采用什么样的配置。

Redis Cluster(分片集群)

为什么需要Redis Cluster?

高并发场景下,使⽤ Redis 主要会遇到的两个问题:

  1. 缓存的数据量太⼤ :实际缓存的数据量可以达到⼏⼗ G,甚至是成百G;
  2. 并发量要求太大:虽然 Redis 号称单机可以⽀持 10w 并发,

主从复制和 Redis Sentinel 这两种⽅案本质都是通过增加主库(master)的副本(slave)数量的⽅式来提⾼ Redis 服务的整体可⽤性和读吞吐量,都不⽀持横向扩展来缓解写压⼒以及解决缓存数据量过⼤的问题。

Redis 切⽚集群 就是部署多台 Redis 主节点(master),这些节点之间平等,并没有主从之说,同时对外提供读写服务。缓存的数据库相对均匀地分布在这些 Redis 实例上,客户端的请求通过路由规则转发到⽬标 master 上。

总结⼀下 Redis Cluster 的主要优势:

  • 可以横向扩展缓解写压⼒和存储压⼒,⽀持动态扩容和缩容;
  • 具备主从复制、故障转移(内置了 Sentinel 机制,⽆需单独部署 Sentinel 集群)等开箱即⽤的功能。

Redis Cluster 是如何分⽚的?

Redis Cluster 并没有使⽤⼀致性哈希,采⽤的是 哈希槽分区 ,每⼀个键值对都属于⼀个 hash slot(哈希槽) 。

Redis Cluster 通常有 16384 个哈希槽 ,要计算给定 key 应该分布到哪个哈希槽中,我们只需要先对每个 key 计算 CRC-16(XMODEM) 校验码,然后再对这个校验码对 16384(哈希槽的总数) 取模,得到的值即是 key 对应的哈希槽。

在这里插入图片描述

为什么 Redis Cluster 的哈希槽是 16384 个?

CRC16 算法产⽣的校验码有 16 位,理论上可以产⽣ 65536(2^16,0 ~ 65535)个值。为什么 Redis Cluster 的哈希槽偏偏选择的是 16384(2^14)个呢?

Redis Cluster 的哈希槽的数量选择 16384 ⽽不是 65536 的主要原因:

  • 哈希槽太⼤会导致⼼跳包太⼤,消耗太多带宽;
  • 哈希槽总数越少,对存储哈希槽信息的 bitmap 压缩效果越好;
  • Redis Cluster 的主节点通常不会扩展太多,16384 个哈希槽已经⾜够⽤了。

Redis Cluster中的节点是怎么进行通信的?

由于 Redis 的集群是去中心化的,这意味着集群中实际上没有一个类似注册中心一样的角色,所以每个节点都需要通过 Gossip 协议与其他节点保持通信,这个通信端口通常是默认的服务端口加 10000,比如默认的服务端口是 6379,那么集群通信端口就是 16379。

Gossip 协议

尽管我们常说节点之间会“保持通信”,但实际上,Redis 集群并不要求每个节点都必须一直与其他所有节点同时保持连接,这要归功于 Redis 使用的 Gossip 协议,Gossip 可以译为流言或者八卦,这很好的反映了这个协议的特点。

举个例子,假如现在有一个新节点 D 要加入:

  1. C 邀请 D 加入集群,现在 C 知道了 D 的存在;
  2. B 与 C 进行 ping & pong,通过 C 得知 D 加入了集群,现在 B 知道了 D 的存在;
  3. A 与 B 进行 ping & pong,通过 B 得知 D 加入了集群,现在 A 也知道了 D 的存在。

可见,当有一个新的事件发生后,它会逐步的在集群中传播,即使中间有节点挂掉,只要消息的传播路径没有完全被切断,那么其他节点也会最终会被通知到,比如新节点的加入,或者节点下线……等。

当然,实际上 Redis 也不是完全依赖这种方式传播消息,对于一些时效性要求比较强的消息 —— 比如节点下线或者主从切换 —— 则会直接通过广播的方式进行通知

节点间的数据交换

组群后,每个 Redis 实例都会在本地维护一个集群实例列表,然后定期从中挑选节点发送 ping 消息,而另一个节点收到了之后会回以 pong 响应。两个节点通过这个步骤来确认彼此健康状态,并传递其其他节点的下线状态,以及交换彼此持有的槽位信息等数据。

需要注意的是,ping 的目标并不是完全随机的,它遵循两个规则:

  • 默认情况下,Redis 实例每隔一秒都会从已知的集群节点中挑选出 5 个实例,然后再从中挑选出一个最久没有 ping 过的节点发送消息。
  • 每隔一段时间,Redis 将会检查其他节点对当前节点请求的响应情况,如果发现有节点最近一次响应距今已接近超时时间 cluster-node-timeout,那么它会立刻向该节点发起 ping 请求,若再无响应则会标记为主观下线。

http://www.kler.cn/a/464476.html

相关文章:

  • Python 开发框架搭建简单博客系统:代码实践与应用
  • 【新教程】华为昇腾NPU的pytorch环境搭建
  • pygame飞机大战
  • 安装PyQt5-tools卡在Preparing metadata (pyproject.toml)解决办法
  • 2025年第五届控制理论与应用国际会议 | Ei Scopus双检索
  • 大模型系列——旋转位置编码和长度外推
  • commit 错分支的一些补救操作
  • uni-app 多平台分享实现指南
  • 【Unity3D】ECS入门学习(十)NativeContainer、EntityCommandBuffer、CompleteDependency
  • el-table树形懒加载展开改为点击行展开
  • SAP财务凭证的更改、冲销的方式
  • python: generate model and DAL using Oracle
  • 【从零开始入门unity游戏开发之——C#篇43】C#补充知识——值类型和引用类型汇总补充、变量的生命周期与性能优化、值类型和引用类型组合使用
  • 虚拟路由冗余协议VRRP(Virtual Router Redundancy Protocol)
  • Springboot使用RabbitMQ实现关闭超时订单的一个简单示例
  • 【面试】深入理解 JavaScript 中的 Object.freeze()
  • k8s部署juicefs
  • SpringBoot教程(十四) SpringBoot之集成Redis
  • Vue 全局事件总线:Vue 2 vs Vue 3 实现
  • 一条SQL语句是如何执行的
  • BOC调制信号matlab性能仿真分析,对比功率谱,自相关性以及抗干扰性
  • python学opencv|读取图像(二十三)使用cv2.putText()绘制文字
  • 嵌入式驱动开发详解8(阻塞/非阻塞/异步通信)
  • Dokcer部署双主Mysql
  • XDOJ 771 求二叉树高度
  • C++ 面向对象编程:多继承、多态