Redis 的集群容错与故障转移

1. Redis 服务模式

Redis 的服务模式主要有三种,分别是单机模式、主从模式、集群模式。在不同的模式下当机器发生故障时,Redis 应对故障的方法也是不一样的。其中单机模式下主要是通过备份与恢复解决故障。而主从模式下则是通过哨兵在从节点中重新选举主节点替换故障节点的工作来实现的,集群模式下也是通过类似于主从模式的方式重新选举新的主节点来实现的。区别则是在集群模式下不需要哨兵来主持选举了。

2. 单机模式下的备份与恢复

Redis 支持两种备份方式,分别是 RDB 备份和 AOF 备份。

2.1 RDB 备份

RDB 持久化支持手动执行,也可以通过配置定期执行。RDB 备份是将某个时间点的 Redis 数据保存到一个 RDB 的文件中,这个文件保存了当前时间点 Redis 数据库中的数据。当 Redis 服务启动时,可以指定载入 RDB 文件,恢复 Redis。

有两个命令可以生成 RDB 文件,分别是 SAVE 和 BGSAVE。其中 SAVE 命令将会阻塞 Redis 服务器,即执行 SAVE 命令期间,Redis 服务将拒绝所有的命令请求。只有在执行结束后,才能正常使用。

而 BGSAVE 则是通过子进程执行的,所以不会阻塞 Redis 服务。而且我们可以手动是的设置自动执行 BGSAVE,可以设置每个多少时间,或者执行多少次写操作就执行一次 BGSAVE。

2.2 AOF 备份

AOF 与 RDB 备份方式的区别是,RDB 文件保存的是 Redis 服务器里具体的数据,而 AOF 备份则保存的是 Redis 的命令。当 Redis 的 AOF 功能是打开的时候,每次 Redis 执行完一个写命令后,就会将命令写到 aof_buf 缓冲区的末尾(类似于 Java 的 writer 方法)。每次当 Redis 执行时间事件结束后,都会根据配置决定将 aof_buf 缓冲区的内容刷新到磁盘中去(类似于 Java 的 flush 方法)。

2.3 数据恢复

当 Redis 服务器启动时可以指定加载 RDB 文件 或者是 AOF 文件。Redis 服务器会优先载入 AOF 文件,如果 AOF 功能没有启动的话,才会自动载入 RDB 文件。

3. 主从模式下的故障转移

Redis 的 SLAVEOF 命令可以让一个 Redis 服务器去复制另外一个 Redis 服务器。这种模式称为主从模式,或者叫主从复制。被复制的服务器称为主服务器。复制主服务器的服务器称为从服务器。主从服务器将保存相同的数据。在主服务器宕机的情况下,可以将从服务器变更为主服务器继续执行命令,而且数据不会丢失。

image-20210524194117194

3.1 主从复制

主从复制有两种情况,一种是第一次开始复制,被称为同步(sync),另外一个是命令传播(command propagate)。

同步是指当执行 slaveof 命令时,从服务器需要同步主服务器的数据,从服务器需要通过 sync 命令来实现。从服务器收到 slaveof 命令后,会向主服务器发送 sync 命令,主服务器收到 sync 命令后,会执行 BGSAVE 命令。在生成文件期间收到的写命令都会保存到缓冲区。之后会将生成后的 RDB 文件发送给从服务器,从服务器加载 RDB 文件,更新数据库的数据。然后主服务器会将缓冲区的命令发送给从服务器。从服务器同步缓冲区的数据。

命令传播是指在执行完同步过程后,主从服务器的状态是一致的,但是如果主服务器再次收到写命令后,主从服务器的状态又会出现不一致,这个时候主服务器就会把写命令发送给从服务器,用来保证主从服务器的数据一致。

3.2 部分重同步

有两种情况下需要执行同步操作,一种是初次收到 slaveof 命令需要执行的同步操作,另外一种情况是在主从服务器断连后重新连接需要执行的同步操作。在旧版的 Redis 服务器中,断连后会执行同步操作,但是会执行全量的同步。即重新生成新的 RDB 文件。这样就会同步大量不需要同步的数据,更好的一种做法是同步断连期间的数据。所以在新版(v2.8+)的 Redis 数据库中支持了部分同步的功能

新版的 psync 命令支持全量重同步部分重同步两种方式,其中全量重同步是用在初次复制的情况下的,而部分重同步则是在出现断连后重新复制的情况下,当从服务器重新连接主服务器后,主服务器会将断连期间执行的写命令重新发送给从服务器。

部分重同步的实现主要靠三个部分:

  • 主服务器的复制偏移量和从服务器的复制偏移量
  • 主服务器的复制积压缓冲区
  • 服务器的运行ID

依靠这三个部分,Redis 实现了部分重同步的功能。

3.2.1 复制偏移量

需要执行复制的主从服务器会分别维护一个复制偏移量,主服务器每次向从服务器传播 N 个字节时就会在复制偏移量上加 N。而从服务每次收到N个字节后也会将自己的复制偏移量加上 N 。如果主从服务器的状态一致,那么复制偏移量总是相等的。如果出现断线,那么复制偏移量就会出现不相等的情况。

image-20210525195028251

3.2.2 复制积压缓冲区

复制积压缓冲区是主服务器维护的一个先进先出的队列。当主服务器执行命令传播时,不仅仅是将数据发送给从服务,而且也会将数据发送到复制积压缓冲区,而且复制积压缓冲区会为每个字节保留对应的复制偏移量,

image-20210526152257709

当从服务器重连之后,从服务器将自己的复制偏移量发送给主服务器,如果从服务器发送过来的复制偏移量在复制缓冲区内,那么主服务器就会执行部分重同步,如果不在复制积压缓冲区,则会执行全量重同步

3.2.3 服务器ID

每个从服务器都会记录主服务器的服务器ID,当断线重连后,从服务器会将自己保存的服务器ID发送给主服务器,如果主服务器发现与当前的服务器ID不一致,则会直接执行全量重同步,如果一致则会使用复制积压缓冲区尝试执行部分重同步,尝试失败则会执行全量重同步

3.2 哨兵

哨兵(Sentinel)是 Redis 高可用的解决方案:由一个或多个哨兵实例组成的哨兵系统可以监控任意多个 Redis 实例。并且在监控到主服务器出现宕机、下线等情况可以直接将当前主服务器的某个从服务器自动的升级为主服务器,由新的主服务器继续处理请求。

3.3 leader 选举

当监控到主服务器发生下线情况后,监控这个主服务的多个哨兵实例会选举出一个 leader 哨兵,由这个 leader 实例负责进行后续的故障转移流程。

每个哨兵实例都会保存一个被称为纪元(epoch)的属性,每进行一次选举,不论是否成功,都会自增一次。这个纪元参数实际上就是一个计数器,并不是什么特别的属性。在一个配置纪元里,每个哨兵都有一次机会将某个哨兵设置为局部领头 leader,一旦选定,在当前纪元里不可更改。

每个监控到主服务器发生客观下线的哨兵,都会要求其他哨兵将自己设置为局部领头 leader。如果其他服务器还没有设置过局部领头 leader,那么就会同意这次请求。否则就会拒绝。

以 三个哨兵节点为例。A、B、C 三个哨兵节点同时监控一个主服务器,B 节点发现主服务器下线,首先会将自己的current_epoch +1 ,然后将局部领头 leader 设置为自己,再向 A、C 两个节点发送选举命令SENTINEL is-master-down-by-addr,并携带 current_epoch,和自己的 runid(即服务器ID),希望A,C 节点可以选举自己为局部领头 leader 。

A、C节点收到请求后,首先判断请求携带的 epoch是否大于自己当前的 epoch ,如果大于自己的 epoch,说明当前节点还没有选择局部领头 leader。此时就会将自己的 epoch 更新为请求中的 epoch,并将自己的 leader_runid 设置成 B,然后返回 leader_runid = B 的结果。说明 A、C 节点选择了 B 节点作为局部领头 leader。

如果请求中的 epoch 不大于自己的 epoch,比如此时有一个 D 节点也发现主服务器下线,并且发送命令SENTINEL is-master-down-by-addr给到 A、C。因为 A、C 当前的 epoch 已经是 1 ,则会拒绝当前请求(返回 leader_runid=B的结果)。相当于告诉 D 节点,已经选举 B 节点为局部领头 leader。

命令SENTINEL is-master-down-by-addr收到的响应不仅仅会携带 leader_runid,还会携带 leader_epoch。B 节点收到响应后,首先会判断 leader_epoch 是否跟当前的 epoch一致,如果一致则判断 leader_runid 是否跟当前的 runid 一致,如果一致则说明目标哨兵已经将当前节点设置为局部领头 leader。完整过程见下图:

image-20210611181918412

如果某个哨兵被半数以上的哨兵选举为局部领头leader,那么这个哨兵将成为领头 leader,即上图中的 B 哨兵。如果在一段时间内没有一个哨兵超过半数,则会再次发起重新选举。

后续的转移过程将由领头leader执行。

3.4 转移过程

转移过程主要有三步:

  1. 在从服务中挑选出一个合适的从服务器,并将它设置为主服务器。
  2. 让所有的从服务器复制新的主服务器
  3. 将已下线的主服务器设置为新的主服务器的从服务器

3.4.1 新服务器的挑选

哨兵并不是在从服务器中随机挑选一个从服务器,而是经过一系列的过滤,然后再根据优先级,选出最高的从服务器。

首先,哨兵获取到已下线的主服务器的所有从服务器的列表,然后依次过滤

  1. 过滤掉所有下线,或者断线的的从服务器,保证列表里的服务器都是在线的。
  2. 过滤掉最近 5 秒内没有回复过领头哨兵 INFO 命令的服务器,保证列表里的服务器通信都是正常的。
  3. 过滤掉与下线的主服务器连接断开超过 down-after-milliseconds*10毫秒的服务器,保证列表里的服务器断开时间不长,数据较新。

然后根据从服务器的优先级进行排序,如果有相同的优先级,则根据复制偏移量进行排序,复制偏移量大的在前,如果复制偏移量相同,则根据 runid 进行排序,runid 小的在前。

所以最后选出的新服务器就是,优先级最高,复制偏移量最大,runid 最小的从服务。

3.4.2 让从服务器复制新的主服务器

首先会向上面选择出来的从服务器发送 SLAVEOF no one命令,停止复制动作,然后向其他的从服务器发送 SLAVEOF ip port命令,让所有的从服务器复制新的主服务器。

3.4.3 将下线的主服务器设置为从服务器

因为旧的主服务器已经下线了,所以这里的设置是保存在哨兵的实例里的,当监听到就的主服务器从新上线后,哨兵就会发送 SLAVEOF 命令,让它成为新的主服务器的从服务器。

4. 集群模式下的故障转移

一个 Redis 集群是由多个 Redis 节点构成的,节点间可以相互通信。而每个节点又可以是多个 redis 服务器构成的主从模式。比如,我们可以将 九个节点,三三的构成主从模式(一主两从),然后将三个主节点构成集群(三主六从)。

image-20210716163216688

详细的过程可以参考 Redis 集群内的数据同步。下面主要介绍下,在集群模式下是如何进行故障转移的。

4.1 故障检测

集群中的每个节点都会保存一份集群里所有节点的状态表。类似于下面这种。

ID 角色 状态
1000 主节点 在线
1001 从节点 在线
2000 主节点 在线
2001 从节点 在线
3000 主节点 在线
3001 从节点 在线

集群中的每个节点都会定期的向其他节点发送 PING 消息,以此来检测对方是否下线。如果再规定的时间内没有收到 PONG 消息的响应,就会将该节点标记为 疑似下线(PFAIL)。比如,1000 向 2000 发送 PING 消息,如果在一定时间内没有收到 PONG 消息的响应,就会修改自己维护的状态表,将 2000 标记为 疑似下线(PFAIL)

集群中的各个节点会通过消息,来 交换 自己维护的节点状态表。比如某个节点是在线、下线还是疑似下线。注意是 交换,而不是 同步。如果某个节点收到了其他节点发来的疑似下线状态,会创建一份下线报告保存下来。

比如,1000 收到了 2000 发来的消息,2000 标记了 3000 是 疑似下线状态,那么。1000 就会创建一份下线报告保存到自己的状态表里。这时 1000 的状态表结构如下:

ID 角色 状态 下线报告
1000 主节点 在线
1001 从节点 在线
2000 主节点 在线
2001 从节点 在线
3000 主节点 在线 [2000上报3000下线]
3001 从节点 在线

如果集群里半数以上的主节点,都将某个主节点标记为疑似下线,那么这个主节点将被标记为下线(FAIL)状态,将主节点标记为下线状态的节点将会向集群广播一条消息,所有收到消息的节点,都会将节点标记为下线状态。

比如上面的例子,如果 1000 也将 3000 标记为疑似下线状态,并且收到了 2000 的下线报告,那么 1000 就会将 3000 标记为下线,并广播 3000 下线的消息,其他节点收到消息后,也会将 3000 标记为下线。

4.2 leader 选举

当从节点发现自己复制的主节点进入下线状态后,就会触发进入选举过程,redis 集群将会在下线的主节点的从节点中选出一个节点作为新的主节点。

选举过程与上面的哨兵选举过程非常类似。都是基于 Raft 算法做的。

首先,集群有一个配置纪元 的计数器。初始值为0,每进行一次故障转移就会自增+1。集群中只有主节点有投票的权利,且每个主节点在一次故障转移期间只有一次投票的机会。

当集群中的从节点发现自己复制的主节点下线后,就会广播一条消息,要求所有收到这条消息且具有投票权利的节点向自己投票。

image-20210716191749393

如果主节点还没有投票,则会返回 ACK 消息,表示支持该从节点成为主节点。如果某个从节点收到的票数超过主节点个数的一半,那么这个从节点将成为新的主节点。

image-20210716191922349

4.3 瓜分问题

如果集群中除下线节点外剩余的节点刚好能够被从节点瓜分,在极端情况下就会出现所有从节点的票数都不会超过一半,就会造成本次选举失败,然后配置纪元+1,进行重新选举。比如上图中的例子,如果两个从节点同时发起投票,那么就有可能每个从节点都收到一票,导致本次选举失败。

redis集群为例尽量避免出现这种情况做了一下优化,在发现主节点下线后,从节点并不会立即发送消息,而是延迟一定的时间再发送选举消息,这个时间是随机的。计算公式如下:

DELAY = 500 milliseconds 
        + random delay between 0 and 500 milliseconds 
        + SLAVE_RANK * 1000 milliseconds

500ms 加上一个随机时间,SLAVE_RANK 是复制偏移量的差值,即与主节点的数据差距越小的从节点,等待的时间就会越小,越有可能成为新的主节点。

4.2 转移过程

新选举出的主节点会首先会执行SLAVEOF no one,停止对旧主节点的复制动作。然后会将旧主节点的槽位指派给自己。最后向集群发送广播消息,让集群中的其他节点知道新的主节点已经接管所有的槽位,旧主节点已经完成下线动作。

这样就完成了整个的故障转移过程,新的主节点将开始接受与自己负责的槽位有关的数据。

5 结束

Redis 的三种工作方式对于服务的可用性是不断提高的,最基本的单机模式采用的是RDB和AOF 的备份方式。这种模式下一旦服务宕机,Redis 服务将立即不可用,不过可以手动的通过备份文件恢复。需要人工操作。

而主从模式下,加入了哨兵机制进行监控,当主节点发生宕机后可以进行故障转移,可以 Redis 服务依然可以使用。缺点是依然只有一个节点处理命令,无法应对高并发的大量请求。

集群模式是可用性最高的一种工作模式,而且集群模式下可以在集群内部进行故障转移动作,不需要引入哨兵。是目前常用的一种模式。

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
本文链接:https://zdran.com/20210519.html