Redis使用主从模式和集群模式来尽量保证缓存服务的高可用

1、前言

  1. Redis是单线程的,可以通过在单机开多个Redis实例,避免CPU多核的浪费,但是单机仍然存在瓶颈。

  2. Redis集群是Redis的分布式解决方案,当一个服务挂了可以快速的切换到另外一个服务,当遇到单机内存、并发等瓶颈时,常使用此方案。

  3. 集群的部署方式也就是Redis cluster,采用主从同步读写分离,类似Mysql的主从同步,Redis cluster支撑 N 个 Redis master node,每个master node都可以挂载多个 slave node,这样整个 Redis就可以横向扩容了。

  4. 如果要支撑更大数据量的缓存,那就横向扩容更多的 master 节点,每个 master 节点就能存放更多的数据了。

  5. 集群中的节点分为主节点和从节点:只有主节点负责读写请求和集群信息的维护;从节点只进行主节点数据和状态信息的复制。

2、Redis主从同步

2.1 、主从同步架构

  • 单机的 redis,能够承载的 QPS 大概就在上万到几万不等。对于缓存来说,一般都是用来支撑读高并发的。

  • 因此架构做成**主从(master-slave)架构,一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。**

  • 所有的读请求全部走从节点。这样可以轻松实现水平扩容,支撑读高并发。

【面试系列】Redis集群如何保证高可用?_ip协议Redis主从同步架构

2.2、数据同步

  1. 启动一台slave 的时候,他会发送psync命令给master ,如果是这个slave第一次连接到master,会触发全量复制
  2. master就会启动一个线程,生成RDB快照,并且把新的写请求都缓存在内存中
  3. RDB文件生成后,master会将这个RDB发送给slave,
  4. slave拿到之后写进本地的磁盘,然后加载进内存,然后master会把内存里面缓存的那些新命名都发给slave

增量复制

  • 如果全量复制过程中,master-slave 网络连接断掉,那么 slave 重新连接 master 时,会触发增量复制。
  • master 直接从自己的 backlog 中获取部分丢失的数据,发送给 slave node,默认 backlog就是 1MB。
  • master 就是根据 slave 发送的 psync 中的offset 来从 backlog 中获取数据的。
【面试系列】Redis集群如何保证高可用?_客户端_02RDB持久化

注意:slave node 如果跟 master node 有网络故障,断开了连接,会自动重连,连接之后 master node 仅会复制给 slave 部分缺少的数据。

3、哨兵集群

3.1、功能

sentinel,中文名是哨兵。哨兵是 redis 集群机构中非常重要的一个组件,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作,主要有以下功能:

  • 集群监控:负责监控 redis master 和 slave 进程是否正常工作。
  • 消息通知:如果某个 redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
  • 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
  • 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址

3.2、经典哨兵集群

  1. 哨兵必须用三个实例去保证自己的健壮性,哨兵+主从的部署架构并不能保证数据不丢失,但是可以保证集群的高可用。
  2. 经典的哨兵集群如图所示,M1所在的机器挂了,哨兵还有两个,可以选举一个出来执行故障转移。
  3. 但是如果只有两个实例存在,master宕机了, s1和s2两个哨兵只要有一个认为你宕机了就切换了,并且会选举出一个哨兵去执行故障
  4. 如果M1宕机了,S1没挂那其实是OK的,但是整个机器都挂了呢?哨兵就只剩下实例S2,没有哨兵去允许故障转移了,虽然另外一个机器上还有R1,但是故障转移是不会执行的,无法保证集群高可用。
【面试系列】Redis集群如何保证高可用?_数据_03经典哨兵集群

3.3、slave->master 选举算法

  1. 如果一个master被认为宕机了,而且大多数的哨兵都允许主备切换,那么某个哨兵就会执行主备切换操作,此时首先要选举一个 slave来,会考虑 slave 的一些信息:
  • 跟 master 断开连接的时长
  • slave 优先级
  • 复制 offset
  • run id
  1. 如果一个 slave 跟 master 断开连接的时间已经超过了 down-after-milliseconds 的 10 倍,外加 master 宕机的时长,那么 slave 就被认为不适合选举为 master。

  2. 接下来会对 slave 进行排序:

  • 按照 slave 优先级进行排序,slave priority 越低,优先级就越高。
  • 如果 slave priority 相同,那么看 replica offset,哪个 slave 复制了越多的数据,offset 越靠后,优先级就越高。
  • 如果上面两个条件都相同,那么选择一个 run id 比较小的那个 slave。

4、集群分区

4.1、数据分区

分布式数据库把整个数据按分区规则映射到多个节点,即把数据划分到多个节点上,每个节点负责整体数据的一个子集。比如我们库有600条用户数据,有3个redis节点,将600条分成3份,分别存入到3个redis节点。

【面试系列】Redis集群如何保证高可用?_客户端_04

4.2、分区规则:

  1. 常见的分区规则哈希分区和顺序分区,redis 集群使用了哈希分区,顺序分区暂用不到,不做具体说明;

  2. 哈希分区的基本思路是:对数据的特征值(如key)进行哈希,然后根据哈希值决定数据落在哪个节点。常见的哈希分区包括:哈希取余分区、一致性哈希分区、带虚拟节点的一致性哈希分区等。

  3. 衡量数据分区方法好坏的标准有很多,其中比较重要的两个因素是**(1)数据分布是否均匀(2)增加或删减节点对数据分布的影响。**

  4. 由于哈希的随机性,哈希分区基本可以保证数据分布均匀;因此在比较哈希分区方案时,重点要看增减节点对数据分布的影响。

1)、节点取余

  • 哈希取余分区思路非常简单:计算key的hash值,然后对节点数量进行取余,从而决定数据映射到哪个节点上。该方案最大的问题是,当新增或删减节点时,节点数量发生变化,系统中所有的数据都需要重新计算映射关系,引发大规模数据迁移。

2)、一致性哈希

  • 一致性哈希算法将整个哈希值空间组织成一个虚拟的圆环,如下图所示,范围为0-2^32-1
  • 对于每个数据,根据key计算hash值,确定数据在环上的位置,然后从此位置沿环顺时针行走,找到的第一台服务器就是其应该映射到的服务器。
【面试系列】Redis集群如何保证高可用?_redis_05一致性哈希
  • 与哈希取余分区相比,一致性哈希分区将增减节点的影响限制在相邻节点。以上图为例,如果在node1和node2之间增加node5,则只有node2中的一部分数据会迁移到node5;如果去掉node2,则原node2中的数据只会迁移到node4中,只有node4会受影响。

  • 一致性哈希分区的主要问题在于,**当节点数量较少时,增加或删减节点,对单个节点的影响可能很大,造成数据的严重不平衡。**还是以上图为例,如果去掉node2,node4中的数据由总数据的1/4左右变为1/2左右,与其他节点相比负载过高。

3)、虚拟槽分区

  • 该方案在一致性哈希分区的基础上,引入了虚拟节点的概念。Redis集群使用的便是该方案,其中的虚拟节点称为槽(slot)。

  • 槽是介于数据和实际节点之间的虚拟概念;每个实际节点包含一定数量的槽,每个槽包含哈希值在一定范围内的数据。引入槽以后,数据的映射关系由数据hash->实际节点,变成了数据hash->槽->实际节点。

  • 在使用了槽的一致性哈希分区中,槽是数据管理和迁移的基本单位。槽解耦了数据和实际节点之间的关系,增加或删除节点对系统的影响很小

  • 仍以上图为例,系统中有4个实际节点,假设为其分配16个槽(0-15);槽0-3位于node1,4-7位于node2,以此类推。如果此时删除node2,只需要将槽4-7重新分配即可,例如槽4-5分配给node1,槽6分配给node3,槽7分配给node4;可以看出删除node2后,数据在其他节点的分布仍然较为均衡。

  • 槽的数量一般远小于2^32,远大于实际节点的数量;在Redis集群中,槽的数量为16384,

  • 所有的键根据哈希函数Hash()=(CRC16[key]&16383)映射到0-16383槽内,共16384个槽位,每个节点维护部分槽及槽所映射的键值数据

下面这张图很好的总结了Redis集群将数据映射到实际节点的过程:

【面试系列】Redis集群如何保证高可用?_客户端_06数据映射到实际节点

(1)Redis对数据的特征值(一般是key)计算哈希值,使用CRC16算法

(2)根据哈希值,计算数据属于哪个槽。

(3)根据槽与节点的映射关系,计算数据属于哪个节点。

redis用虚拟槽分区好处:解耦数据与节点关系,节点自身维护槽映射关系,分布式存储

5、节点通信机制

集群要作为一个整体工作,离不开节点之间的通信

5.1、两个端口

在哨兵系统中,节点分为数据节点和哨兵节点:前者存储数据,后者实现额外的控制功能。在集群中,没有数据节点与非数据节点之分:所有的节点都存储数据,也都参与集群状态的维护。为此,集群中的每个节点,都提供了两个TCP端口:

  • 普通端口:即我们在前面指定的端口(7000等)。普通端口主要用于为客户端提供服务(与单机节点类似);但在节点间数据迁移时也会使用。

  • 集群端口:端口号是普通端口+10000(10000是固定值,无法改变),如7000节点的集群端口为17000。集群端口只用于节点之间的通信,如搭建集群、增减节点、故障转移等操作时节点间的通信;不要使用客户端连接集群接口。为了保证集群可以正常工作,在配置防火墙时,要同时开启普通端口和集群端口。

5.2、Gossip协议

节点间通信,按照通信协议可以分为几种类型:单对单、广播、Gossip协议等。重点是广播和Gossip的对比。

  • 广播是指向集群内所有节点发送消息;优点是集群的收敛速度快(集群收敛是指集群内所有节点获得的集群信息是一致的),缺点是每条消息都要发送给所有节点,CPU、带宽等消耗较大。

  • **Gossip协议的特点是:在节点数量有限的网络中,每个节点都“随机”的与部分节点通信(并不是真正的随机,而是根据特定的规则选择通信的节点),经过一番杂乱无章的通信,每个节点的状态很快会达到一致。**Gossip协议的优点有负载(比广播)低、去中心化、容错性高(因为通信有冗余)等;缺点主要是集群的收敛速度慢。

5.3、消息类型

  1. 集群中的节点采用固定频率(每秒10次)的定时任务进行通信相关的工作:判断是否需要发送消息及消息类型、确定接收节点、发送消息等。如果集群状态发生了变化,如增减节点、槽状态变更,通过节点间的通信,所有节点会很快得知整个集群的状态,使集群收敛。

  2. 节点间发送的消息主要分为5种:meet消息、ping消息、pong消息、fail消息、publish消息。不同的消息类型,通信协议、发送的频率和时机、接收节点的选择等是不同的。

  • MEET消息:在节点握手阶段,当节点收到客户端的CLUSTER MEET命令时,会向新加入的节点发送MEET消息,请求新节点加入到当前集群;新节点收到MEET消息后会回复一个PONG消息。

  • PING消息:集群里每个节点每秒钟会选择部分节点发送PING消息,接收者收到消息后会回复一个PONG消息。PING消息的内容是自身节点和部分其他节点的状态信息;作用是彼此交换信息,以及检测节点是否在线。** **PING消息使用Gossip协议发送,**接收节点的选择兼顾了收敛速度和带宽成本,具体规则如下:(1)随机找5个节点,在其中选择最久没有通信的1个节点(2)扫描节点列表,选择最近一次收到PONG消息时间大于cluster_node_timeout/2的所有节点,防止这些节点长时间未更新。

  • PONG消息:PONG消息封装了自身状态数据。可以分为两种:第一种是在接到MEET/PING消息后回复的PONG消息;第二种是指节点向集群广播PONG消息,这样其他节点可以获知该节点的最新信息,例如故障恢复后新的主节点会广播PONG消息。

  • FAIL消息:当一个主节点判断另一个主节点进入FAIL状态时,会向集群广播这一FAIL消息;接收节点会将这一FAIL消息保存起来,便于后续的判断。

  • PUBLISH消息:节点收到PUBLISH命令后,会先执行该命令,然后向集群广播这一消息,接收节点也会执行该PUBLISH命令。

6、其他

6.1、架构细节

  1. 所有的 redis 节点彼此互联 (PING-PONG 机制),内部使用二进制协议优化传输速度和带宽。
  2. 节点的 fail 是通过集群中超过半数的节点检测失效或者某个节点主从全挂时才生效。
  3. 客户端与 redis 节点直连,不需要中间 proxy 层。客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
  4. redis-cluster 把所有的物理节点映射到 [0-16383]slot 上。

6.2、集群容错

  1. 为了当部分节点失效时,cluster 仍能保持可用,Redis 集群采用每个节点拥有 1(主服务自身)到 N 个副本的主从模型。类似于 master/slave。
  2. 但是 redis cluster 却不是强一致性的,因为 cluster 内部 master 和 slave 之间是通过异步复制做数据同步的,复制过程中可能 master 挂了,这就导致部分数据没有完全同步至 slave 上,不过这种可能性还是很小的。
  3. 集群选举过程是集群中所有 master 参与,如果半数以上 master 节点与当前 master 节点通信超时,则集群认为当前 master 节点挂掉
  4. 当集群不可用时, 所有对集群的操作做都将失败。以下是会导致集群不可用的其中两种情况:
    1. 集群任意 master 挂掉,并且当前 master 没有 slave,集群不可用
    2. 集群超过半数以上 master 挂掉,无论是否有 slave,集群不可用

6.3、集群扩展

以往的一致性哈希方案,如果我们移除或者新增节点时,虽然说不会导致全局 key 的 rehash,但是也会影响到部分 key 的失效。Redis Cluster 在可用性和可扩展性上比较重视,如果集群新增一个节点,在给该节点分配槽时,这些槽所属的源节点和该节点会进行一次 key 的迁移,并且迁移过程中不阻塞集群服务。如果移除一个节点,同理,我们需要将待移除的节点的 key 迁移到另一个节点上。

那集群是如何做到 key 迁移不阻塞集群服务的呢?

key 迁移过程中,涉及到 CLUSTER SETSLOT slot8 MIGRATING node 命令和 CLUSTER SETSLOT slot8 IMPORTING node 命令,前者用于将给定节点 node 中的槽 slot8 迁移出节点,而后者用于将给定槽 slot8 导入到节点 node :

  1. 如果一个槽被设置为 MIGRATING 状态时,原本持有该槽的节点会继续接受关于这个槽的命令请求,但只有当键存在于该节点时,节点才会处理这个请求。如果命令所使用的键不存在于该节点,那么节点将向客户端返回一个 ASK 转向(redirection)错误,告知客户端,要将命令请求发送到槽的迁移目标节点

  2. 如果一个槽被设置为 IMPORTING 状态时,节点仅在接收到 ASKING 命令之后,才会接受关于这个槽的命令请求。如果客户端向节点发送该槽的数据请求,命令为非 ASKING 时,那么节点会使用 MOVED 转向错误将命令请求转向至真正负责处理这个槽的节点。

关注我

我是一名后端开发工程师,个人公众号:任冬学编程