发布于 

Redis:哨兵机制和切片集群

本站字数:106.9k    本文字数:4k    预计阅读时长:13min    访问次数:

前面的文章提到了,Redis 的数据结构和 Redis 的持久化机制,保证了单机情况下 Redis 的可靠性和高性能。但是对于集群的场景下,只有持久化机制,也不能让 Redis 有更高的可靠性。例如,Redis 的主从集群,在主库挂掉以后,整个 Redis 集群便不能处理写操作了,这个时候就需要一个机制来保证主库挂掉也能正常运行 – 哨兵机制。对于需要大量存储缓存的时候,主从集群,需要主库的服务器有更高的性能或者存储,显然这样的需求并不可持续,如何让缓存能够分布存储,这就是 – 切片集群

Redis Sentinel
Redis Sentinel

哨兵机制:主库挂了该怎么办?

住过主库挂了,那么至少需要解决下面三个问题:

  1. 主库真的挂了吗?
  2. 该选择哪个从库作为主库?
  3. 怎么把新主库的相关信息通知给从库和客户端呢?

这就要提到哨兵机制了。在Redis主从集群中,哨兵机制是实现主从库自动切换的关键机制,它有效地解决了主从复制模式下故障转移的这三个问题。

哨兵机制的基本流程

哨兵主要有三个任务:

  • 监控:监控主库的健康状况,判定主库是否掉线了
  • 选主:在主库客观下线以后,选出一个新的主库
  • 通知:在主库选定以后,需要将选举结果通知到各个从库
Redis 哨兵的工作流程
Redis 哨兵的工作流程

其中,通知过程比较简单,通知给各个从库追随新的主库就好了,不涉及到决策逻辑。但是,监控选主这两个任务,逻辑相对复杂。下面从监控主库健康状况选一个新的主库两方面来讨论哨兵的基础机制。

主观下线和客观下线

哨兵进程会使用PING命令检测它自己和主、从库的网络连接情况,用来判断实例的状态。如果哨兵发现主库或从库对PING命令的响应超时了,那么,哨兵就会先把它标记为“主观下线”。

如果监测是从库,那么直接哨兵将其标记为“主观下线”即可。因为从库下线影响一般不太大,集群的对外服务不会间断。

但是,因为集群网络压力过大、网络阻塞问题造成哨兵误判,但是主库本身没有出现故障。可是,一旦启动了主从切换,并且通知从库和新主库进行数据同步,过程本身有很大的开销:哨兵需要花时间选主从库需要和新的主库进行同步。但是,主库本身没有掉线,但是哨兵误判,造成了大量的资源浪费。

为了减少误判,我们需要一种机制去协商主库的在线状态。哨兵机制也是类似的,它通常会采用多实例组成的集群模式进行部署,这也被称为哨兵集群。引入多个哨兵实例一起来判断,就可以避免单个哨兵因为自身网络状况不好,而误判主库下线的情况。同时,多个哨兵的网络同时不稳定的概率较小,由它们起做决策,误判率也能降低。

哨兵集群判断客观下线
哨兵集群判断客观下线

如何选定一个新的主库?

一般来说,我把哨兵选择新主库的过程称为“筛选+打分”。简单来说,我们在多个从库中,先按照一定的筛选条件,把不符合条件的从库去掉。然后,我们再按照一定的规则,给剩下的从库逐个打分,将得分最高的从库选为新主库,如下图所示:

哨兵选主
哨兵选主

接下来就是,根据一定的条件选择主库,在选择之前有一个前提条件:网络状况良好

所以,在选主时,除了要检查从库的当前在线状态,还要判断它之前的网络连接状态。如果从库总是和主库断连,而且断连次数超出了一定的阈值,我们就有理由相信,这个从库的网络状况并不是太好,就可以把这个从库筛掉了。定量判定它的网络状况可以通过配置项 down-after-milliseconds。这个配置项的含义是:从库断开连接最大连接时间,如果超过 down-after-milliseconds 都没有连接上主库,那么就判定为从库连接断开。这个断开连接次数超过 10 次,这个从库的网络情况就很差,不适合作为新的主库。

在筛选完成以后,根据下面的顺序对存在的从库进行打分排序:

  • 第一轮:优先级 slave-priority 最高的从库得分高
    一般来说,可以通过配置文件配置不同的从库,从库的配置文件中可以配置 slave-priority 预先确定一个顺序。例如,可以手动指定一个内存大的实例,使其优先级更高,减少在哨兵选主过程中的代价。

  • 第二轮:和旧主库同步程度最接近的从库得分最高

    同步程度如何定量计算?

    在上一篇文章中,主从库数据同步的过程中,主库会使用 master_repl_offset 记录当先最新的写操作的位置,而从库会使用 slave_repl_offset 记录从库的复制进度。

    此时就可以通过判断从库的 slave_repl_offset 最接近主库的 master_repl_offset。得分最高的排名靠前。

    从库同步程度
    从库同步程度
  • 第三轮:ID号最小的从库得分高
    每个实例都会有一个ID,这个ID就类似于这里的从库的编号。目前,Redis在选主库时,有一个默认的规定:在优先级和复制进度都相同的情况下,ID号最小的从库得分最高,会被选为新主库

哨兵集群:哨兵挂了该怎么办?

实际上,一旦多个实例组成了哨兵集群,即使有哨兵实例出现故障挂掉了,其他哨兵还能继续协作完成主从库切换的工作,包括判定主库是不是处于下线状态,选择新主库,以及通知从库和客户端。在配置哨兵的信息时,我们只需要用到下面的这个配置项,设置主库的IP和端口,并没有配置其他哨兵的连接信息。

配置解释
1
2
3
4
5
# master-name: 主库名称
# ip: 主库 IP 地址
# redis-port: 主库 Redis 端口
# quorum: 需要多少个哨兵主观认为主库下线,那么就认为其客观下线
sentinel monitor <master-name> <ip> <redis-port> <quorum>

这个配置只配置了主库的相关信息,但是没有配置从库的相关信息,那么哨兵之间如何互相发现互相连接的呢?

基于 PUB/SUB 机制的哨兵集群组成

哨兵只要和主库建立连接,就可以在主库上发布消息。他们也可以在主库上订阅消息,获取其他哨兵的 IP 和端口信息。其他哨兵在主库上面发布消息以后,他们互相之间就能知晓彼此之间的 IP 和端口。

除了哨兵实例,我们自己编写的应用程序也可以通过Redis进行消息的发布和订阅。所以,为了区分不同应用的消息,Redis会以频道的形式,对这些消息进行分门别类的管理。所谓的频道,实际上就是消息的类别。当消息类别相同时,它们就属于同一个频道。反之,就属于不同的频道。只有订阅了同一个频道的应用,才能通过发布的消息进行信息交换

哨兵集群的建立
哨兵集群的建立

哨兵除了执行监控任务以外,在主从库发生变更的时候,还需要通知所有的从库,以便于从库和新主库进行数据同步。哨兵通过给主库发送 INFO 命令来完成从库信息的获取。其他哨兵也可以通过这样的方法获取到从库的信息,并不断监控。

虽然哨兵机制保证了主从库的监控、选主、切换,但是该怎么通知到客户端主库发生了变更,怎么监控主从切换的过程?这个时候,就可以选择依赖 PUB/SUB 机制,来帮助我们完成哨兵和客户端的信息同步。

基于 PUB/SUB 机制的客户端事件通知

从本质上说,哨兵就是一个运行在特定模式下的Redis实例,只不过它并不服务请求操作,只是完成监控、选主和通知的任务。所以,每个哨兵实例也提供pub/sub机制,客户端可以从哨兵订阅消息。哨兵提供的消息订阅频道有很多,不同频道包含了主从库切换过程中的不同关键事件。

客户端订阅事件
客户端订阅事件

了解到这些频道以后就可以让客户端来订阅这些事件了:

1
2
3
4
# 订阅实例“客观下线”的情况
SUBSCRIBE +odown
# 订阅“主库地址发生变化的情况”
SUNSCRIBE +switch-master

由那个哨兵执行主从切换?

哨兵执行主从切换过程,类似于“客观下线”的仲裁过程。在这个过程之间先看,哨兵之间是怎么做到“客观下线”的仲裁的。

任何一个哨兵实例只要自己认为主库“下线”以后,就会给其他所有的哨兵发送 is-master-down-by-addr 命令。接着其他哨兵就会根据自己和主库的连接情况,回复 ‘Y’ 或者 ‘N’ 的响应。

哨兵集群判断“客观下线”
哨兵集群判断“客观下线”

一个哨兵获得了仲裁需要的赞成数目 quorum 以后,就可以标记为“客观下线”。这个时候,这个哨兵就可以给其他哨兵发送消息,希望自己来执行主从切换的动作,让其他的哨兵进行投票。这个过程成为“Leader选举”。最终执行主从切换动作的哨兵被称之为“Leader”,投票过程就是确定 Leader。

在投票过程中,任何一个想成为Leader的哨兵,要满足两个条件:第一,拿到半数以上的赞成票;第二,拿到的票数同时还需要大于等于哨兵配置文件中的quorum值。以3个哨兵为例,假设此时的quorum设置为2,那么,任何一个想成为Leader的哨兵只要拿到2张赞成票,就可以了。

哨兵集群 - Leader选举
哨兵集群 - Leader选举

切片集群:数据太大了该怎么办?

切片集群,也叫分片集群,就是指启动多个Redis实例组成一个集群,然后按照一定的规则,把收到的数据划分成多份,每一份用一个实例来保存。如果把25GB的数据平均分成5份(当然,也可以不做均分),使用5个实例来保存,每个实例只需要保存5GB数据。如下图所示:

Redis 切片集群
Redis 切片集群

如果说,25GB 这个场景其实 32GB 的服务器做一个主从集群也可以,为什么必须切片呢?在使用的过程中发现,Redis的响应有时会非常慢。后来,我们使用INFO命令查看Redis的 latest_fork_usec 指标值(表示最近一次fork的耗时),结果显示这个指标值特别高,快到秒级别了

如何保存更多的数据?

在上面的场景中,保存大量的数据,使用了大内存和切面集群两种办法。其实也对应了 Redis 扩容的两种基本方式:

  • 纵向扩展:升级单个Redis实例的资源配置,包括增加内存容量、增加磁盘容量、使用更高配置的CPU。就像下图中,原来的实例内存是8GB,硬盘是50GB,纵向扩展后,内存增加到24GB,磁盘增加到
    150GB。
  • 横向扩展:横向增加当前Redis实例的个数,就像下图中,原来便用1个8GB内存、50GB磁盘的实例,现在使用三个相同配置的实例。
横向扩展和纵向扩展
横向扩展和纵向扩展

纵向扩展最大的好处就是简单直接,但是也会遇到很多问题。第一个问题就是,RDB 在持久化的时候需要更多的内存;第二个问题就是,纵向扩展会受到硬件和成本的限制

横向扩展和纵向扩展相比,配置会比较复杂。但是可以获得更高的扩展性,而且不会过多的影响性能。在面向百万、千万级别的用户规模时,横向扩展的 Redis 切片集群会是一个很好的选择。但是切片集群不像单机集群,不可避免会涉及到多个实例的分布式管理问题。想要把切片集群用起来,还需要解决两个问题:

  • 切片以后,数据如何分布?
  • 客户端如何确定想要访问的数据在什么位置?

数据切片和实例的对应关系

具体来说,Redis Cluster方案采用哈希槽(Hash Slot,接下来我会直接称之为Slot),来处理数据和实例之间的映射关系。在Redis Cluster方案中,一个切片集群共有 16384 个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的key,被映射到一个哈希槽中。

数据切片和实例
数据切片和实例

在手动分配哈希槽时,需要把16384个槽都分配完,否则Redis集群无法正常工作

客户端如何定位数据?

一般来说,客户端和集群建立连接以后,实例就会把哈希槽的分配信息发送给客户端。但是,在集群刚刚创建的时候,实例只知道自己分配的哈希槽,不知道其他机器。

那么,客户端为什么可以在访问任何一个实例时,都能获得所有的哈希槽信息呢?这是因为,Redis实例会把自己的哈希槽信息发给和它相连接的其它实例,来完成哈希槽分配信息的扩散。当实例之间相互连接后,每个实例就有所有哈希槽的映射关系了。

但是,集群中哈希槽的的对应关系并不是一直不变的:

  • 集群中,实例新增或者删除,Redis需要重新分配哈希槽
  • 为了负载均衡,Redis需要把哈希槽所有的实例分布一遍

当哈希槽发生了变更,集群内部可以通过信息扩散,得知全局的哈希槽分布。但是客户端就难以知道发生什么了。这个时候,Redis Cluster 提供了一种重定向机制,让客户端知道这个数据到了那里。

1
2
GET hello:key
(error) MOVED 13320 172.0.0.1:6379

MOVED代表数据已经全部迁移,同时会更新客户端的哈希槽分配缓存。

哈希槽 MOVED
哈希槽 MOVED
1
2
GET hello :key
(error ) ASK13320 172.16.19.5:6379

ASK代表数据正在迁移过程中,和 MOVED 不同,ASK 不会将客户端的哈希槽分配缓存刷新。

哈希槽 ASK
哈希槽 ASK

参考资料