您好,欢迎访问代理记账网站
  • 价格透明
  • 信息保密
  • 进度掌控
  • 售后无忧

Sentinel-哨兵机制

Redis:Sentinel-哨兵机制

  • 哨兵机制的基本流程
  • 主观下线和客观下线
  • 选举领头sentinel
  • 选举新主库
  • 哨兵集群
    • 基于pub/sub机制的哨兵集群组成
    • 基于pub/sub机制的客户端事件通知
    • 哪个哨兵执行主从切换
  • 参考文献

Redis中采用了主从库模式,如果从库挂了,客户端可以继续向主库或其他从库发送请求,进行相关的操作,但是如果主库发生故障了,那就直接会影响到从库的同步,因为从库没有相应的主库可以进行数据复制操作了。如果Redis在半夜出现故障,还需要运维手动切换,人工运维的成本太高。所以我们必须有一个高可用方案来抵抗节点故障,当故障发生时可以自动进行从主切换,程序可以不用重启,运维可以继续睡大觉,仿佛什么事也没发生一样。Redis 官方提供了这样一种方案 —— Redis Sentinel(哨兵)。

主从模式下,如果主库挂了就会导致写服务中断,就需要运行一个新的主库,势必会涉及到三个问题:

  • 主库真的挂了吗?
  • 选择哪个从库作为主库?
  • 怎么把新主库的相关信息通知到从库和客户端。

哨兵机制的基本流程

哨兵机制

哨兵机制就是:由一个或多个 Sentinel实例(instance)组成的Sentinel系统(system)可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。

从下面这张图中我们能看到主节点挂掉了,原先的主从复制也断开了,客户端和损坏的主节点也断开了。从节点被提升为新的主节点,其它从节点开始和新的主节点建立复制关系。客户端通过新的主节点继续进行交互。Sentinel 会持续监控已经挂掉了主节点
在这里插入图片描述

所以哨兵机制主要负责以下三个任务:

  • 监控:周期性地给所有的主从库发送PING命令,检测它们是否仍然在线运行。如果从库没有在规定时间内响应哨兵的PING命令,哨兵就会把它标记为“下线状态”;同样,如果主库也没有在规定时间内响应哨兵的PING命令,哨兵就会判定主库下线,然后开始自动切换主库的流程;
  • 选主:主库挂了以后,哨兵就需要很多从库,按照一定的规则选择一个从库实例,把它作为新的主库;
  • 通知:在执行通知任务时,哨兵会把新主库的连接信息发给其他从库,让它们执行replicaof命令,和新主库建立连接,并进行数据复制。同时,哨兵会把新主库的连接信息通知给客户端,让它们把请求操作发到新主库上;

哨兵对主库的下线判断有“主观下线”和“客观下线”两种。那么,为什么会存在两种判断呢?它们的区别和联系是什么呢?

主观下线和客观下线

在默认情况下,Sentinel会以每秒一次的频率向所有与它创建了命令连接的实例(包括主 服务器、从服务器、其他Sentinel在内)发送PING命令,并通过实例返回的PING命令回复来 判断实例是否在线。

Sentinel配置文件中的down-after-milliseconds选项指定了Sentinel判断实例进入主观下线 所需的时间长度:如果一个实例在down-after-milliseconds毫秒内,连续向Sentinel返回无效回 复,那么Sentinel会修改这个实例所对应的实例结构,在结构的flags属性中打开 SRI_S_DOWN标识,以此来表示这个实例已经进入主观下线状态。

实例对PING命令的回复可以分为以下两种情况:

  • 有效回复:实例返回+PONG-LOADING-MASTERDOWN三种回复的其中一种;
  • 无效回复:实例返回除+PONG、-LOADING-MASTERDOWN三种回复之外的其他 回复,或者在指定时限内没有返回任何回复;

如果检测到的是从库,哨兵简单的标记为主观下线即可,从库的下线影响不会太大;如果检测的是主库,不能简单的标记为主观下线就开启主从切换,因为一旦主库没有故障,哨兵误判就会带来不必要的计算和通信开销。

为了避免误判,哨兵机制一般会采用多个实例组成的服务器集群的模式进行部署,引入多个哨兵实例一起判断,就可以避免单个哨兵因为自身网络不好而误判主库下线的情况,因为多个哨兵同时不稳定的概率较小,一起做决策可以降低误判率,哨兵集群判断的逻辑如下图所示:

在这里插入图片描述

客观下线:
当有N个哨兵实例时,最好要有N/2 + 1个实例判断主库为“主观下线”,才能最终判定主库为“客观下线”。这样一来,就可以减少误判的概率,也能避免误判带来的无谓的主从库切换。(当然,有多少个实例做出“主观下线”的判断才可以,可以由Redis管理员自行设定)。

选举领头sentinel

当一个主服务器被判断为客观下线时,监视这个下线主服务器的各个Sentinel会进行协商,选举出一个领头Sentine l,并由领头Sentine l对下线主服务器执行故障转移操作。

  • 监视同一个主服务器的 多个在线Sentine l中的任意一个都有可能成为领头Sentinel。
  • 每次进行领头Sentinel选举之后,不论选举是否成功,所有Sentinel的配置纪元 (configuration epoch)的值都会自增一次。配置纪元实际上就是一个计数器,并没有什么特别的。
  • 每个发现主服务器进入客观下线的Sentinel都会要求其他Sentinel将自己设置为局部领头 Sentinel;
  • 当一个Sentinel(源Sentinel)向另一个Sentinel(目标Sentinel)发送SENTINEL is- master-down-by-addr命令,并且命令中的runid参数不是*符号而是源Sentinel的运行ID时, 表示源Sentine l要求目标Sentinel将前者设置为后者的局部领头Sentinel;
  • Sentinel设置局部领头Se ntine l的规则是先到先得:最先向目标Se ntine l发送设置要求的源 Sentinel将成为目标Sentinel的局部领头Sentinel,而之后接收到的所有设置要求都会被目标 Sentinel拒绝。
  • 目标Sentinel在接收到SENTINEL is-master-down-by-addr命令之后,将向源Sentinel返回 一条命令回复,回复中的leader_runid参数和leader_epoch参数分别记录了目标Sentinel的局部 领头Sentinel的运行ID和配置纪元。
  • 源Sentinel在接收到目标Sentinel返回的命令回复之后,会检查回复中leader_epoch参数 的值和自己的配置纪元是否相同,如果相同的话,那么源Sentinel继续取出回复中的 leader_runid参数,如果leader_runid参数的值和源Sentinel的运行ID一致,那么表示目标Sentinel将源Sentinel设置成了局部领头Sentinel。
  • 如果有某个Sentinel被半数以上的Sentinel设置成了局部领头Sentinel,那么这个Sentinel成 为领头Sentinel。举个例子,在一个由10个Sentinel组成的Sentinel系统里面,只要有大于等于 10/2+ 1= 6个Sentinel将某个Sentinel设置为局部领头Sentinel,那么被设置的那个Sentinel就会成为领头Sentinel。
  • 如果在给定时限内,没有一个Sentinel被选举为领头Sentinel,那么各个Sentinel将在一段 时间之后再次进行选举,直到选出领头Sentine l为止。

SENTINEL is-master-down-by- addr <ip> <port> <current_epoch> <runid>
参数意义
ip被sentinel判断为主观下线的主服务器ip
port主观下线主服务器的端口号
current_epoch用于选举领头sentinel
runid可能是*,也可能是sentinel的运行id

选举新主库

在选举产生出领头Sentinel之后,领头Sentinel将对已下线的主服务器执行故障转移操作,该操作包含以下三个步骤:

1)在已下线主服务器属下的所有从服务器里面,挑选出一个从服务器,并将其转换为 主服务器。

2)让已下线主服务器属下的所有从服务器改为复制新的主服务器。

3)将已下线主服务器设置为新的主服务器的从服务器,当这个旧的主服务器重新上线时,它就会成为新的主服务器的从服务器。

领头Sentinel会将已下线主服务器的所有从服务器保存到一个列表里面,然后按照以 下规则,一项一项地对列表进行过滤:

1)删除列表中所有处于下线或者断线状态的从服务器,这可以保证列表中剩余的 从服务器都是正常在线的。

2)删除列表中所有最近五秒内没有回复过领头Sentinel的INFO命令的从服务器,这可以保证列表中剩余的从服务器都是最近成功进行过通信的。

3)删除所有与已下线主服务器连接断开超过down-after-milliseconds*10毫秒的从服务器:down-after-milliseconds选项指定了判断主服务器下线所需的时间,而删除断开时 长超过down-after-milliseconds*10毫秒的从服务器,则可以保证列表中剩余的从服务器 都没有过早地与主服务器断开连接,换句话说,列表中剩余的从服务器保存的数据都是比较新的。

哨兵选择新主库的过程是一个**“筛选+打分”**,在多个从库中,先按照一定的筛选条件,把不符合条件的从库去掉。然后,我们再按照一定的规则,给剩下的从库逐个打分,将得分最高的从库选为新主库,如下图所示:
在这里插入图片描述
筛选的条件:
我们肯定要先保证所选的从库仍然在线运行。不过,在选主时从库正常在线,这只能表示从库的现状良好,并不代表它就是最适合做主库的。

设想一下,如果在选主时,一个从库正常运行,我们把它选为新主库开始使用了。可是,很快它的网络出了故障,此时,我们就得重新选主了。这显然不是我们期望的结果。

在选主时,除了要检查从库的当前在线状态,还要判断它之前的网络连接状态。如果从库总是和主库断连,而且断连次数超出了一定的阈值,我们就有理由相信,这个从库的网络状况并不是太好,就可以把这个从库筛掉了。

使用配置项down-after-milliseconds * 10。其中,down-after-milliseconds是我们认定主从库断连的最大连接超时时间。如果在down-after-milliseconds毫秒内,主从节点都没有通过网络联系上,我们就可以认为主从节点断连了。如果发生断连的次数超过了10次,就说明这个从库的网络状况不好,不适合作为新主库。

打分

分别按照三个规则依次进行三轮打分,这三个规则分别是从库优先级、从库复制进度以及从库ID号。只要在某一轮中,有从库得分最高,那么它就是主库了,选主过程到此结束。如果没有出现得分最高的从库,那么就继续进行下一轮。

  • 第一轮:优先级最高的从库得分高:用户可以通过slave-priority配置项,给不同的从库设置不同优先级。比如,你有两个从库,它们的内存大小不一样,你可以手动给内存大的实例设置一个高优先级。在选主时,哨兵会给优先级高的从库打高分,如果有一个从库优先级最高,那么它就是新主库了。如果从库的优先级都一样,那么哨兵开始第二轮打分。
  • 第二轮:和旧主库同步程度最接近的从库得分高:选择和旧主库同步最接近的那个从库作为主库,那么,这个新主库上就有最新的数据。主库会用master_repl_offset记录当前的最新写操作在repl_backlog_buffer中的位置,而从库会用slave_repl_offset这个值记录当前的复制进度。我们想要找的从库,它的slave_repl_offset需要最接近master_repl_offset。如果在所有从库中,有从库的slave_repl_offset最接近master_repl_offset,那么它的得分就最高,可以作为新主库。

就像下图所示,旧主库的master_repl_offset是1000,从库1、2和3的slave_repl_offset分别是950、990和900,那么,从库2就应该被选为新主库。当然,如果有两个从库的slave_repl_offset值大小是一样的(例如,从库1和从库2的slave_repl_offset值都是990),我们就需要给它们进行第三轮打分了。

在这里插入图片描述

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

首先,哨兵会按照在线状态、网络状态,筛选过滤掉一部分不符合要求的从库,然后,依次按照优先级、复制进度、ID号大小再对剩余的从库进行打分,只要有得分最高的从库出现,就把它选为新主库。

哨兵集群

哨兵机制可是实现主从库的自动切换,通过部署多个实例来共同判断,可以降低对主库下线的误判率。一旦多个实例组成了哨兵集群,即使有哨兵实例出现故障挂掉了,其他哨兵还能继续协作完成主从库切换的工作,包括判定主库是不是处于下线状态,选择新主库,以及通知从库和客户端。

配置哨兵的信息的时,每个哨兵只需要配置以下连接信息,设置主库的IP端口

sentinel monitor <master-name> <ip> <redis-port> <quorum> 

那么各个哨兵之间不知道彼此的配置信息是如何组成集群的,下面会介绍哨兵集群的组成和运行机制。

基于pub/sub机制的哨兵集群组成

哨兵实例之间建立联系是通过Redis的pub/sub机制—发布/订阅机制

在编写应用的时候,可以通过Redis进行消息的发布和订阅。为了区分不同应用的消息,Redis会建立不同的频道进行分类管理。当消息处于同一频道时,就可以通过发布的消息进行信息交换;

当Sentinel与一个主服务器或者从服务器建立起订阅连接之后,Sentinel就会通过订阅连接,向服务器发送以下命令。在主从集群中,主库上有一个名为“__sentinel__:hello”的频道,不同哨兵就是通过它来相互发现,实现互相通信的。

SUBSCRIBE __sentinel__:hello

Sentinel对__sentinel__:hello频道的订阅会一直持续到Sentinel与服务器的连接断开为止。这也就是说,对于每个与Sentinel连接的服务器,Sentinel既通过命令连接向服务器的 __sentinel__:hello频道发送信息,又通过订阅连接从服务器的__sentinel__:hello频道接收信息。

举个例子,假设现在有sentinel1、sentinel2、sentinel3三个Sentinel在监视同一个服务器, 那么当sentinel1向服务器的__se ntinel__:hello频道发送一条信息包含了自己的ip和端口号(172.16.19.3:26579),所有订阅了__sentinel__:hello频道的Sentinel(包括sentinel1自己在内)都会收到这条信息,获取sentinel1的ip地址和端口号。

哨兵2、3可以和哨兵1建立网络连接。通过这个方式,哨兵2和3也可以建立网络连接,这样一来,哨兵集群就形成了。它们相互间可以通过网络连接进行通信,比如说对主库有没有下线这件事儿进行判断和协商。

在这里插入图片描述

哨兵除了彼此之间建立起连接形成集群外,还需要和从库建立连接。这是因为,在哨兵的监控任务中,它需要对主从库都进行心跳判断,而且在主从库切换完成后,它还需要通知从库,让它们和新主库进行同步。

哨兵向主库发送INFO命令来完成,主库接受到这个命令后,就会把从库列表返回给哨兵。接着,哨兵就可以根据从库列表中的连接信息,和每个从库建立连接,并在这个连接上持续地对从库进行监控。哨兵1和3可以通过相同的方法和从库建立连接。

在这里插入图片描述
通过pub/sub机制,哨兵之间可以组成集群,同时,哨兵又通过INFO命令,获得了从库连接信息,也能和从库建立连接,并进行监控。

在实际使用哨兵时,我们有时会遇到这样的问题:如何在客户端通过监控了解哨兵进行主从切换的过程呢?比如说,主从切换进行到哪一步了?这其实就是要求,客户端能够获取到哨兵集群在监控、选主、切换这个过程中发生的各种事件。

此时,我们仍然可以依赖pub/sub机制,来帮助我们完成哨兵和客户端间的信息同步。

基于pub/sub机制的客户端事件通知

每个哨兵实例也提供pub/sub机制,客户端可以从哨兵订阅消息。哨兵提供的消息订阅频道有很多,不同频道包含了主从库切换过程中的不同关键事件,与主从库切换相关的关键事件,例如主库下线判断、新主库选定、从库重新配置。
在这里插入图片描述
具体的操作步骤是,客户端读取哨兵的配置文件后,可以获得哨兵的地址和端口,和哨兵建立网络连接。然后,我们可以在客户端执行订阅命令,来获取不同的事件消息。

SUBSCRIBE +odown // 订阅所有实例处于“客观下线状态的事件”

PSUBSCRIBE  * // 订阅所有事件

switch-master <master name> <oldip> <oldport> <newip> <newport> // 新主库切换事件

当哨兵把新主库选择出来后,客户端就会看到switch-master事件。这个事件表示主库已经切换了,新主库的IP地址和端口信息已经有了。有了pub/sub机制,哨兵和哨兵之间、哨兵和从库之间、哨兵和客户端之间就都能建立起连接。

哪个哨兵执行主从切换

这个过程就是如何在哨兵集群中选取领头的sentinel来执行主从切换。

任何一个哨兵实例只要自身判断主库“主观下线”后,就会给其他实例发送is-master-down-by-addr命令。接着,其他实例会根据自己和主库的连接情况,做出YN的响应,Y相当于赞成票*8,N相当于反对票**。

当有N个哨兵实例时,最好要有N/2 + 1个实例判断主库为“主观下线”,才能最终判定主库为“客观下线”。此时该哨兵可以向其他哨兵发送命令表明希望由自己来执行主从切换,并且让所有其他哨兵给自己投票,这个投票过程称为“Leader选举”。

成为Leader的条件如下:

  • 拿到半数以上的赞成票;
  • 拿到的票数同时还需要大于等于哨兵配置文件中的quorum值;

选举过程如下图所示:

在这里插入图片描述

  • T1时刻:哨兵1判断主库为”客观下线“,先给自己投了赞成票Y,在分别向哨兵2和哨兵3表露自己想成为leader的想法;
  • T2时刻:哨兵3也判断主库为“客观下线”,也想成为Leader,也先给自己投一张赞成票,再分别向S1和S2发送命令,表示要成为Leader;
  • T3时刻:哨兵1收到了哨兵2的Leader投票请求。因为哨兵1已经给自己投了一票Y,所以它不能再给其他哨兵投赞成票了,所以哨兵1回复N表示不同意。同时,哨兵2收到了T2时哨兵3发送的Leader投票请求。因为哨兵1之前没有投过票,它会给第一个向它发送投票请求的哨兵回复Y,给后续再发送投票请求的哨兵回复N,所以,在T3时,哨兵2回复哨兵3,同意哨兵3成为Leader。
  • T4时刻,哨兵2才收到T1时哨兵1发送的投票命令。因为哨兵2已经在T3时同意了哨兵3的投票请求,此时,哨兵2给哨兵1回复N,表示不同意哨兵1成为Leader。发生这种情况,是因为哨兵3和哨兵2之间的网络传输正常,而哨兵1和哨兵2之间的网络传输可能正好拥塞了,导致投票请求传输慢了。
  • T5时刻:哨兵3不仅获得了半数以上的Leader赞成票,也达到预设的quorum值(quorum为2),所以它最终成为了Leader。接着,哨兵3会开始执行选主操作,而且在选定新主库后,会给其他从库和客户端通知新主库的信息。

其实哨兵的投票就是先到先得,最先收到的投票请求就会回复Y。如果哨兵3没有拿到2票Y,那么这轮投票就不会产生Leader。哨兵集群会等待一段时间(也就是哨兵故障转移超时时间的2倍),再重新选举。这是因为,哨兵集群能够进行成功投票,很大程度上依赖于选举命令的正常网络传播。如果网络压力较大或有短时堵塞,就可能导致没有一个哨兵能拿到半数以上的赞成票。所以,等到网络拥塞好转之后,再进行投票选举,成功的概率就会增加。

需要注意的是,如果哨兵集群只有2个实例,此时,一个哨兵要想成为Leader,必须获得2票,而不是1票。所以,如果有个哨兵挂掉了,那么,此时的集群是无法进行主从库切换的。因此,通常我们至少会配置3个哨兵实例。

选出leader的过程就是分布式系统中的共识算法,集群中的多个节点如何就一个问题达成共识,在之后的文章中会介绍Paxos、Raft等共识算法。

还有一个问题是如果哨兵集群中有实例挂了,怎么办,会影响主库状态判断和选主吗?

答:这个属于分布式系统领域的问题了,指的是在分布式系统中,如果存在故障节点,整个集群是否还可以提供服务?而且提供的服务是正确的?这方面最著名的就是分布式领域中的“拜占庭将军”问题了,“拜占庭将军问题”不仅解决了容错问题,还可以解决错误节点的问题,再之后的文章中也会介绍。Redis的哨兵集群中存在故障节点时,只要集群中大多数节点状态正常,集群依旧可以对外提供服务。

参考文献

1、极客时间-Redis核心技术与实战
2、Redis设计与实现;


分享:

低价透明

统一报价,无隐形消费

金牌服务

一对一专属顾问7*24小时金牌服务

信息保密

个人信息安全有保障

售后无忧

服务出问题客服经理全程跟进