上一章我们讲如果Master宕机会导致部分数据未同步还会丧失写的功能。如何避免这种情况呢?哨兵登场了。
什么是哨兵
Sentinel(哨兵)是用于监控Redis集群中Master状态的工具,是Redis高可用解决方案,哨兵可以监视一个或者多个redis master服务,以及这些master服务的所有从服务。 某个master服务宕机后,会把这个master下的某个从服务升级为master来替代已宕机的master继续工作。(顺带提一句,即使后来之前的master重启服务,也不会变回master了,而是作为slave从服务)
哨兵的主要功能
- 监控(Monitoring):持续监控Redis主节点、从节点是否处于预期的工作状态。
- 通知(Notification):哨兵可以把Redis实例的运行故障信息通过API通知监控系统或者其他应用程序。
- 自动故障恢复(Automatic failover):当主节点运行故障时,哨兵会启动自动故障恢复流程:某个从节点会升级为主节点,其他从节点会使用新的主节点进行主从复制,通知客户端使用新的主节点进行。
- 配置中心(Configuration provider):哨兵可以作为客户端服务发现的授权源,客户端连接到哨兵请求给定服务的Redis主节点地址。如果发生故障转移,哨兵会通知新的地址。这里要注意:哨兵并不是Redis代理,只是为客户端提供了Redis主从节点的地址信息。
多哨兵模式
在实际生产情况中,Redis Sentinel 是集群的高可用的保障,为避免 Sentinel 发生意外,它一般是由 3~5 个节点组成,这样就算挂了个别节点,该集群仍然可以正常运转。其结构图如下所示:
上图所示,多个哨兵之间也存在互相监控,这就形成了多哨兵模式,现在对该模式的工作过程进行讲解,介绍如下:
1) 主观下线
主观下线,适用于主服务器和从服务器。如果在规定的时间内(配置参数:down-after-milliseconds),Sentinel 节点没有收到目标服务器的有效回复,则判定该服务器为“主观下线”。比如 Sentinel1 向主服务发送了PING命令,在规定时间内没收到主服务器PONG回复,则 Sentinel1 判定主服务器为“主观下线”。
2) 客观下线
客观下线,只适用于主服务器。 Sentinel1 发现主服务器出现了故障,它会通过相应的命令,询问其它 Sentinel 节点对主服务器的状态判断。如果超过半数以上的 Sentinel 节点认为主服务器 down 掉,则 Sentinel1 节点判定主服务为“客观下线”。
3) 投票选举
投票选举,所有 Sentinel 节点会通过投票机制,按照谁发现谁去处理的原则,选举 Sentinel1 为领头节点去做 Failover(故障转移)操作。Sentinel1 节点则按照一定的规则在所有从节点中选择一个最优的作为主服务器,然后通过发布订功能通知其余的从节点(slave)更改配置文件,跟随新上任的主服务器(master)。至此就完成了主从切换的操作。
对上对述过程做简单总结:
Sentinel 负责监控主从节点的“健康”状态。当主节点挂掉时,自动选择一个最优的从节点切换为主节点。客户端来连接 Redis 集群时,会首先连接 Sentinel,通过 Sentinel 来查询主节点的地址,然后再去连接主节点进行数据交互。当主节点发生故障时,客户端会重新向 Sentinel 要地址,Sentinel 会将最新的主节点地址告诉客户端。因此应用程序无需重启即可自动完成主从节点切换。
核心配置
port 26379
# 保护模式关闭,这样其他服务起就可以访问此台redis
protected-mode no
# 哨兵模式是否后台启动,默认no,改为yes
daemonize yes
pidfile /var/run/redis-sentinel.pid
# log日志保存位置
logfile /usr/local/redis/sentinel/redis-sentinel.log
# 工作目录
dir /usr/local/redis/sentinel
###核心配置
# 核心配置。
# 第三个参数:哨兵名字,可自行修改。(若修改了,那后面涉及到的都得同步)
# 第四个参数:master主机ip地址
# 第五个参数:redis端口号
# 第六个参数:哨兵的数量。比如2表示,当至少有2个哨兵发现master的redis挂了,
# 那么就将此master标记为宕机节点。
# 这个时候就会进行故障的转移,将其中的一个从节点变为master
sentinel monitor mymaster 192.168.217.151 6379 2
# master中redis的密码
sentinel auth-pass mymaster 123456
# 哨兵从master节点宕机后,等待多少时间(毫秒),认定master不可用。
# 默认30s,这里为了测试,改成10s
sentinel down-after-milliseconds mymaster 10000
# 当替换主节点后,剩余从节点重新和新master做同步的并行数量,默认为 1
sentinel parallel-syncs mymaster 1
# 主备切换的时间,若在3分钟内没有切换成功,换另一个从节点切换
sentinel failover-timeout mymaster 180000
Sentinel工作原理
哨兵是Redis的一种工作模式,以监控节点状态及执行故障转移为主要工作,哨兵总是以固定的频率去发现节点、故障检测,然后在检测到主节点故障时以安全的方式执行故障转移,确保集群的高可用性。
以下为哨兵周期性检查的核心逻辑,哨兵模式的原理部分也将以下面的代码为主线进行说明。
/* Perform scheduled operations for the specified Redis instance. */
/* Sentinel模式下,对节点执行的定时操作。属于最核心的函数了,包含节点发现、信息更新、故障检测、故障转移等操作 */
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
/* ========== MONITORING HALF ============ */
/* Every kind of instance */
sentinelReconnectInstance(ri);
/* 定时向主从节点、其他Sentinel节点发送PING、INFO、Hello(仅Sentinel)消息;
* 以此实现了节点发现、节点信息更新、收集健康检测信息*/
sentinelSendPeriodicCommands(ri);
/* ============== ACTING HALF ============= */
/* We don't proceed with the acting half if we are in TILT mode.
* TILT happens when we find something odd with the time, like a
* sudden change in the clock. */
if (sentinel.tilt) {
if (mstime()-sentinel.tilt_start_time < SENTINEL_TILT_PERIOD) return;
sentinel.tilt = 0;
sentinelEvent(LL_WARNING,"-tilt",NULL,"#tilt mode exited");
}
/* 检查节点是否主观下线,这个操作对主从节点、其他Sentinel节点生效 */
sentinelCheckSubjectivelyDown(ri);
/* Masters and slaves */
if (ri->flags & (SRI_MASTER|SRI_SLAVE)) {
/* Nothing so far. */
}
/* 以下操作仅针对主节点 */
if (ri->flags & SRI_MASTER) {
/* 判断主节点是否满足客观宕机条件 */
sentinelCheckObjectivelyDown(ri);
/* 检查是否可以开始故障转移操作 */
if (sentinelStartFailoverIfNeeded(ri)) {
/* 监控该主节点的Sentinel执行选主操作 */
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
}
/* 根据failover_state状态机,逐步执行故障转移操作 */
sentinelFailoverStateMachine(ri);
/* 这一步是向监控该主节点的其他Sentinel询问该主节点是否主观宕机,为主观宕机收集选票 */
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
}
}
节点自动发现
首先说明一下,这里的节点指的是其他哨兵节点及从节点。
回过头看下上面哨兵的配置文件,会发现我们仅配置了主节点的地址信息(host和port),并没有配置从节点及其他哨兵节点的信息。但是,我们在哨兵节点和主节点客户端通过info replication或info sentinel命令时却能够发现它们的存在。这一功能是由哨兵的自动发现机制实现的,我们来了解下。
先看数据节点
一般情况下,哨兵节点每隔10秒(故障转移时每隔1秒)向主从节点发送INFO命令,以此获取主从节点的信息。第一次执行时,哨兵仅知道我们给出的主节点信息,通过对主节点执行INFO命令就可以获取其从节点列表。如此周期性执行,就可以不断发现新加入的节点。
- 如果INFO命令目标是从节点:哨兵从返回信息中获取从节点所属的最新主节点ip和port,如果与历史记录不一致,则执行更新;获取从节点的优先级、复制偏移量以及与主节点的链接状态并更新。
- 如果INFO命令目标是主节点:哨兵从返回信息中获取主节点的从机列表,如果从节点是新增的,则将其加入监控列表。
- 无论目标是主节点还是从节点,都会记录其runId。
- 如果节点的角色发生变化,哨兵会记录节点新的角色及上报时间。若此时哨兵运行在TILT模式下,则什么都不做。否则,会执行主从切换相关的逻辑,我们后面再细说。
再来看哨兵节点
为了相互检查可用性及信息交互,哨兵之间是一直保持连接的,但是我们并没有显示的告知它们彼此的存在,它们之间是怎么发现对方并交互的呢?
是这样的:通过刚才的介绍,我们了解到哨兵通过INFO命令发现了主节点及从节点的地址信息,而Redis提供了一种发布订阅的消息通信模式,即Pub/Sub,哨兵们就是通过一个约定好的通道(channel)发布/订阅hello信息进行通信。结合图示说明一下:
如上图所示:
- 每隔2秒,每个哨兵会通过它所监控的主节点、从节点向
__sentinel__:hello
通道发布一条hello消息。 - 每个哨兵会通过它所监控的主节点、从节点订阅
__sentinel__:hello
通道的消息,以此接收其他哨兵发布的信息。
我们可以通过Redis客户端直接订阅该channel:连接到其中一个节点,然后输入命令:SUBSCRIBE __sentinel__:hello
,如下图所示:
如上图所示,hello信息中包含8个信息,依次是:哨兵ip、哨兵端口、哨兵runId、当前纪元、主节点名称、主节点ip、主节点端口、主节点纪元,除了纪元应该都可以理解。这样每个哨兵发布的消息都会被其他哨兵接收到,从而达到信息交换的目的。
- 每个哨兵都会维护其监控的主节点信息,如果它接收到其他哨兵消息后,发现自己维护的信息已经过时,则立即执行更行过程。
- 如果哨兵接受到的信息没有在已有的监控列表中,就意味着发现了一个新的哨兵实例,此时会创建一个新的哨兵实例加入监控列表。在处理新增哨兵实例时,如果它与已存在的哨兵实例runId或者ip、port一致,将只保存最新的实例信息。
关于其他哨兵节点及从节点的发现过程就介绍到这里了,整体还是比较容易理解的。简单总结一下:对于数据节点采用INFO命令询问,从一个主节点得到从节点,再通过从节点校验主节点,实现节点发现;对于其他哨兵节点,借助正在被监控的数据节点以类似广播的方式,实现节点的发现。
故障检测
故障检测是哨兵执行故障转移的前提,在知晓需要监控的目标(主从节点)后,哨兵通过PING命令实现对主从节点的故障检测。
哨兵以集群方式工作,官方建议至少要有三个节点,每个节点都以相同的方式对主从节点进行监控与故障检测。由于网络抖动或者网络分区,单个哨兵对节点的故障检测可能无法代表其真实的状态,为了降低误判,哨兵之间还需要对节点的故障状态进行协商。所以这里需要引入两个概念:
- 主观宕机(Subjective Down, SDOWN):是指一个哨兵实例通过检测发现某个主节点发生故障的一种状态。
- 客观宕机(Objective Down, ODOWN):是指哨兵检测到某个主节点发生故障,通过命令SENTINEL is-master-down-by-addr与其他哨兵节点协商,并且在指定时间内接收到指定数量的其他哨兵的确认反馈时的一种状态。
简单来说,SDOWN是哨兵自己认为节点宕机,而ODOWN是不但哨兵自己认为节点宕机,而且该哨兵与其他节点沟通后,达到一定数量的哨兵都认为节点宕机了。
这里的“一定数量”是一个法定数量(Quorum),是由哨兵监控配置决定的。
还有一个概念:config-epoch,配置纪元,它是维护集群内主从关系信息版本的配置。每次执行故障转移都会加1,用于表明一个新的集群主从关系版本。数值越大,版本越新。它由哨兵节点维护,并在哨兵节点之间相互传播。
主观宕机
Redis以类似心跳检测的PING命令对节点进行健康检查,然后根据节点的回复情况进行状态管理。
哨兵以字段act_ping_time维护对节点执行PING命令的时间,并把它作为超时未回复的依据,通过下面的过程了解它的变化:
-
默认情况下,每隔1秒哨兵向节点发送一次PING命令;发送成功后,设置last_ping_time为当前时间,按如下规则修改act_ping_time:
- 若act_ping_time为0,则设置为当前时间。
- 若act_ping_time不是0,则不做任何修改。
-
若收到节点回复:修改last_pong_time为当前时间,并检查是否为有效回复,哨兵仅认为+PONG、-LOADING、-MASTERDOWN`是有效的,其他回复或未回复都是无效的。
- 若为有效回复,则修改last_avail_time为当前时间,修改act_ping_time为0;
- 若为无效回复,不做任何修改
-
每隔100毫秒,哨兵逐个检查节点是否达到SDOWN状态,具体方法在sentinelCheckSubjectivelyDown,当满足以下条件(两者满足其一)时,哨兵会把节点状态置为SDOWN:
- 在预设时间范围内哨兵未收到节点对PING命令的有效回复。这个预设时间是由配置项down-after-milliseconds决定的,默认值是30秒。
- 哨兵认为它是主节点,而节点上报它正在切换为从节点,但是在指定时间范围内它没有完成角色切换。这个时间计算公式为:down-after-milliseconds + 2 SENTINEL_INFO_PERIOD,默认值是50秒(30 + 2 10)。
SDOWN状态是指在down-after-milliseconds未收到节点的PING命令回复,如果该配置项为30秒,但是哨兵在29秒时收到节点的回复,哨兵也会认为节点是正常工作的。
SDOWN无法触发故障转移,仅仅说明是一个哨兵认为节点发生故障(不可用)了,若要触发故障转移,必须达到ODOWN状态。
客观宕机
当Sentinel将一个主节点判断为主观下线之后,为了确认这个主服务器是否真的下线 了,它会向同样监视这一主服务器的其他Sentinel进行询问,看它们是否也认为主服务器已经进入了下线状态。当Sentinel从其他Sentinel那里接收到足够数量的已下线判断之后,Sentinel就会将从服务器判定为客观下线,并对主服务器执行故障转移操作。
发送Sentinel is-master-down-by-addr命令。
Sentinel使用Sentinel is-master-down-by-addr命令询问其他Sentinel节点是否已经下线。命令及参数说明如下:
Sentinel is-master-down-by-addr <ip> <port> <epoch> <runId>
# 既可用于询问其他哨兵主节点是否下线,也可以用于后续故障转移的投票,这里先说第一种情况:
# ip:被Sentinel判断为主观下线主节点的ip;
# port:被Sentinel判断为主观下线主节点的port;
# epoch:Sentinel的配置纪元;
# runId:询问场景中它始终为`*`。
如果被Sentinel判断为主观下线的主节点IP为127.0.0.1,端口号为6379,Sentinel的配置纪元为0,那么Sentinel将向其他Sentinel发送以下命令:
SENTINEL is-master-down-by-addr 127.0.0.1 6379 0 *
接收Sentinel is-master-down-by-addr命令。
当Sentinel接收到其他Sentinel发过来的Sentinel is-master-down-by-addr命令后,会解析命令中的主节点的ip和port,检查本地缓存中主节点是否为SDOWN,然后以下三个参数进行回复:
# 返回目标Sentinel对主节点的检查结果,1说明为SDOWN,0说明正常。
1) <down_state>
# 在询问主节点是否下线的场景下,始终为 *
2) <leader_runid>
# 在询问主节点是否下线的场景下,始终为 0
3) <leader_epoch>
处理Sentinel is-master-down-by-addr回复。
当Sentinel接收到其他Sentinel对Sentinel is-master-down-by-addr的回复后,会解析其返回的内容,然后设置该Sentinel对询问主节点的在线状态。
这样,Sentinel就完成了通过其他Sentinel对主节点在线状态的询问过程。
在Sentinel的周期性函数中,会检查主节点是否满足客观宕机的条件。判断的过程比较简单:
- 从字典中取出监控当前主节点的所有Sentinel节点,然后遍历这些Sentinel的主节点状态是否为SDOWN,是就累加。
- 遍历完成后,检查累加结果是否大于等于quorum(法定数量)。成立则修改主节点状态为客观宕机(ODOWN),并设置宕机时间;不成立则设置为非客观宕机。
ODOWN状态仅应用在主节点上,不对从节点及其他哨兵节点应用,但是SDOWN状态对他们都是有效的。
故障转移
Sentinel判定主节点客观宕机(ODOWN)后,将进入故障转移过程。
进入故障转移过程有几个前提:主节点为客观宕机状态、当前没有故障转移在执行、上次故障转移已经超时。Sentinel确认可以执行故障转移后,会进行以下几项准备工作:
- 设置failover_state:
SENTINEL_FAILOVER_STATE_WAIT_START
(故障转移等待开始); - 设置当前主节点标识位:
SRI_FAILOVER_IN_PROGRESS
(主节点处于故障转移过程中); - 配置纪元加1,并以此作为故障转移的纪元;
- 记录故障转移开始时间及failover_state状态修改时间;
整个故障转移过程是依靠Sentinel周期性函数及failover_state状态机来驱动的(具体函数是sentinelFailoverStateMachine),通过图示来说明:
Sentinel Leader选举
当一个主节点被判断为客观下线时,监控这个主节点的所有Sentinel会进行协商,选举一个Leader对下线的主节点执行故障转移操作。怎么选呢?
思考一下,我们可以知道:故障检测是多个Sentinel同时执行的,也就是说可能多个Sentinel在相近的时间内都判定主节点客观宕机了,因此Leader的选举过程在Sentinel集群内可能是同步进行的。所以,Sentinel需要在集群内进行“拉票”,“拉票”的依据就是配置纪元及“拉票”的时间。配置纪元越大,优先级越高;“拉票”请求越早,优先级越高。我们来看下:
-
当Sentinel判断主节点客观下线后,会把自己的配置纪元加1,未检测到主节点ODOWN或检测慢的,自然落后于当前纪元;
-
Sentinel会使用Sentinel is-master-down-by-addr命令向其他所有Sentinel发起投票请求,与故障检测过程中的“询问“不同,这里的runId将被设置为当前Sentinel的runId,epoch为最新的纪元。
-
其他Sentinel接收到“投票”请求后,执行以下过程:
- 若请求纪元大于自身配置纪元,则更新替换;若监控主节点的配置纪元小于请求纪元,则更新替换,并“投票”给发起请求的Sentinel。这个过程是抢占式的,同一纪元,先到先得。(Redis命令处理是单线程,无并发冲突)。
- 根据判断结果,回复“投票”请求:回复内容为该Sentinel选举的Leader的runId。
-
Sentinel接收并处理Sentinel is-master-down-by-addr回复:把投票结果(runId)更新到该Sentinel的节点信息中。
“投票”完成后就到了“唱票”环节,该过程是在SENTINEL_FAILOVER_STATE_WAIT_START
状态下执行的。Sentinel会遍历当前主节点下所有的Sentinel节点,把它们的投票信息进行统计;然后判断是否有Sentinel胜出。这里胜出的条件是:
- Sentinel必须获取集群内大多数Sentinel的选票,即票数大于50%(防止“脑裂“);
- Sentinel所获票数必须大于等于法定人数(quorum);
因为Sentinel Leader的产生需要半数以上Sentinel的支持,并且每个Sentinel在每个配置纪元里面只能设置一次Leader,所以在一个配置纪元立main,只会出现一个Leader。
如果在给定时限内,没有一个Sentinel被选举为Leader,那么各个Sentinel将在一段时间后再次进行选择,直到选出Leader为止。
Sentinel Leader选举完成,设置failover_state为SENTINEL_FAILOVER_STATE_SELECT_SLAVE
。
新主节点选举
主节点已经客观宕机,Sentinel Leader会从该主节点存活的从节点中选出一个新的主节点。
首先,Sentinel Leader会按照以下条件剔除从节点:
- 主观宕机(SDOWN)或与处于断线状态的从节点;
- 最近5秒内未回复过Sentinel Leader INFO命令的从节点;
- 从节点的优先级为0的从节点,由配置项replica-priority决定;
- 与主节点断开连接超过10倍down-after-milliseconds的从节点;
筛选过后,剩下的从节点都是数据比较新、与Sentinel Leader通信正常的,可以保证故障转移后最小的数据丢失。
然后,按照以下规则选择新的主节点:
- 选择replica-priority最低的节点。如果存在相同,则继续;
- 选择复制偏移量最大的的从节点。如果存在相同,则继续;
- 选择runId最小的从节点;
如果新主节点选举失败,将等待重试。选举成功,则将此从节点提升,并设置failover_state为SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE
。
配置新主节点&新主节点角色提升
选出新的主节点之后,Sentinel Leader会向它发送slaveof NO ONE,把这个从节点转为主节点(这是在从节点自身来看的角色转换)。
从节点接收slaveof NO ONE命令后,会重置其主节点信息,断开与其主节点、从节点的网络连接,重置其复制ID,并执行持久化重写操作。
发送命令后,Sentinel Leader会设置failover_state为SENTINEL_FAILOVER_STATE_WAIT_PROMOTION
,等待从节点角色提升。
Sentinel Leader会向它发送slaveof NO ONE命令后,每隔一秒发送一次INFO命令(正常是10秒一次),并观察命令回复中的角色信息。当被升级的从节点的角色从原来的slave变为master时,Sentinel Leader就直到该从节点已经升级主节点了。
从节点角色提升成功,设置failover_state状态为SENTINEL_FAILOVER_STATE_RECONF_SLAVES
。
配置其他从节点
新的主节点已经配置完成,接下来就是要让其他存活的从节点以该节点为主节点,然后向该节点发起主从复制。
该过程原理比较简单:遍历原主节点的从节点,向这些从节点发送slaveof
所有从节点配置完成后,就会修改failover_state为SENTINEL_FAILOVER_STATE_UPDATE_CONFIG
。
不过,这一过程受配置项parallel_syncs(同时执行主从复制的节点数量)的影响。由于主从复制过程中从节点数据加载阶段无法对外提供服务,所以,如果同时进行主从复制的从节点数量较多,可能会导致短时间内系统不可用。
该配置越小,从节点完成配置的时间越长;反之,时间越短。实际环境中,我们需要根据从节点的数量,系统压力,按照比例合理设置。
更新配置
故障转移过程中,新主节点是以“储君”的身份在工作,其他所有从节点切换至新的主节点后,就要正式把新主节点“立”起来了。简单来说有三步(实现方法在sentinelFailoverSwitchToPromotedSlave,由周期函数触发):
- 重置新主节点的信息状态、清空从节点、Sentinel节点等,failover_state修改为
SENTINEL_FAILOVER_STATE_NONE
。 - 从旧主节点中迁移Sentinel节点、从节点,迁移至新的主节点中。
- 释放就主节点配置信息。
至此,故障转移工作完成。