Sentinel(哨兵)监控Redis集群中Master状态,是Redis 的高可用性解决方案。它监视一系列Master-Slave集群(和其他Sentinel)。当某个Master下线时,自动将该Master下的某个Slave升级为Master。
Sentinel的代码阅读难度相对较大,这是因为它是时钟驱动的一个状态机,所以在逻辑上有点不是很连贯。
本文使用的版本和Redis底层对象实现原理分析这篇文章是相同的。有一些基础知识也在这篇文章中列出。
本文中【接上】暗示这个逻辑其实不属于父标题,但在时序上是上个标题的后续环节。例如上一个标题是Request的产生逻辑,下一个就是对端对Request的处理逻辑,下一个就是对对端Response的处理逻辑。
本文中 Master 指的是 Redis 主从复制中的主节点。(Sentinel) Leader 指的是 FailOver 流程中选举出来的领头的 Sentinel。
Redis主从复制
详见文章
总览
Sentinel用法与常见命令
表示监控一个叫 mymaster 的服务器,其 IP:Port 为 127.0.0.1:6379,并且这个 Master 要 Failover 至少需要2个。但注意这里的2并不是下确界,除了这个约束,还需要有满足 majority 的限制条件。
1 | sentinel monitor mymaster 127.0.0.1 6379 2 |
down-after-milliseconds
表示经过这一段时间,如果服务器没有对 Sentinel 的 PING 进行回复,或者一直返回无效回复(注意,回复错误也是有效回复),那么就会被标记为SDOWN。
1 | sentinel down-after-milliseconds mymaster 60000 |
failover-timeout
表示故障恢复的超时时间。
1 | sentinel failover-timeout mymaster 180000 |
parallel-syncs
表示在执行故障转移时,最多可以有多少 Slave 同时对新 Master 进行同步,数字越小,完成故障转移所需的时间就越长。
1 | sentinel parallel-syncs mymaster 1 |
常用结构/模块
在本章节中,主要介绍两个基础对象,即全局上下文 sentinelState
和当前 Sentinel 对其监视的每个对象的描述类 sentinelRedisInstance
。
sentinelRedisInstance
每个被监视的 Redis 实例都会创建一个 sentinelRedisInstance
结构,它具有flag
分别标注了实例的状态。
可以看到
1 |
SDOWN 表示主观下线,即这个节点被一个 Sentinel 认为下线。ODOWN 是客观下线,表示被 Sentinel 集群认为下线。
如果另一个 Sentinel 觉得某个 Master 节点 SDOWN 了,那么它会通知我,并且让我把这个 Master 节点也设置为 SDOWN 么?是的,可以查看 sentinelAskMasterStateToOtherSentinels
函数。
1 |
SRI_MASTER_DOWN
和 SRI_S_DOWN
有什么区别呢?
SRI_MASTER_DOWN
表示对方 Sentinel 关于 Master 有没有宕机的投票结果。用来统计生成 ODOWN 的结果。SRI_S_DOWN
表示我自己认为某个节点有没有 SDOWN,包括 Slave、Master、Sentinel。1
2
3
4
5
下面三个是和 Reconf 有关的 Flag,我们在稍后进行考虑。
1 |
sentinelRedisInstance相关参数解释
runid
runid
是类似183af86ac492235bf97739bcdad0353f1d4e61df
的串,在后面的实验中可以看到。
注意,如果需要获得当前 Sentinel 进程的 runid,通过sentinel.myid
来访问。在 3.0 版本前,可以通过server.runid
来访问。其中server
是1
extern struct redisServer server;
sentinel.myid
在sentinelIsRunning
中被getRandomHexChars
函数设置,似乎新版本中,使用Sentinel独立的runid了。name
ri->runid
和ri->name
是两个不同概念。
对于Master节点来说,表示这个Sentinel中Master节点的名字,这个名字是在配置文件里面用SENTINEL monitor
设置的。
对于Slave和Sentinel节点来说,这个是自动设置的。在createSentinelRedisInstance
中可以看到,Slave的name
是ip:port
;从sentinelProcessHelloMessage
可以看到,Sentinel的name
是runid。
masters
、slaves
、sentinels
这三个dict
的key是name
。parallel_syncs
在failover时,可以同时对新的Master进行同步的Slave数量。通过下面命令进行设置。1
SENTINEL parallel-syncs <master-name> <number>
leader
如果这个 Instance 是 Master,那么leader
表示将负责对这个 Master 进行 FailOver 的 Sentinel 的 runid。
如果这个 Instance 是 Sentinel,那么leader
就是被选举出来的 Sentinel Leader 的 runid。
对于非SRI_MASTER_DOWN
情况,这个不生效。pending_commands
in-flight的命令的数量。即发出还未收到回复。failover_state
注意,还有个flags中的标签SRI_FAILOVER_IN_PROGRESS
。这两个标签都只对Master的Instance是有意义的。addr
表示Master的addr,是一个sentinelAddr *
。master
我们需要区分ri->master
和另外一个sentinel.masters
。
首先,ri->master
只在这个Instance是Slave的时候有效,表示这个Slave对应的Master;而sentinel.masters
表示这个Sentinel监控的所有Master实例。
我们需要记住一点,整个进程里面做事的第一人称“我”是这个Sentinel,因此只有在sentinel.masters
这个全局单例中才会看到我们监视了哪些Master。master->failover_epoch
、master->config_epoch
、master->leader_epoch
和sentinel.current_epoch
我们需要区分这三者,以及sentinelState
中的current_epoch
的区别:master->failover_epoch
表示这次FailOver开始时的Epoch。当某个Sentinel进程决定调用sentinelStartFailover
开始FailOver时,它需要自增自己的current_epoch
,并且同步给failover_epoch
。master->config_epoch
在sentinelProcessHelloMessage
中根据比较自己的和远端的config_epoch
的大小,进行配置更新。master->leader_epoch
leader_epoch
和leader
是联合使用的,表示当前Sentinel认为的处理这个Master的FailOver过程的Leader,发起竞选的epoch。sentinel.current_epoch
因为一个Sentinel可能处理若干个Master的FailOver过程,所以sentinel.current_epoch
作为当前Sentinel的epoch。这有点类似于Raft里面Term的机制。
这是Sentinel自己的属性,而failover_epoch
作为当前被监视的Master的epoch,两个是无法一一对应的。所以说导致有这两个看上去若即若离的对象。
在sentinelProcessHelloMessage
里面会更新。
role_reported
注意,我们通过flag & SRI_MASTER
判断是不是Master。
但是这个是在INFO里面上报的信息。
如果role发生变化了,就需要更新role_reported_time
。
如果说原来的role是SRI_SLAVE
,还需要更新slave_conf_change_time
。
各种索引/遍历sentinelRedisInstance的函数梳理
Redis Sentinel提供了多个函数用来索引/遍历sentinelRedisInstance
- getSentinelRedisInstanceByAddrAndRunID
根据ip:port
和runid
来寻找Instance,这个要三个都相等。但是如果指定了runid
或者ip
为NULL
,那么它也可以不参与比较。
其原理是获得dict *instances
的迭代器,然后遍历一次以比较。 - sentinelGetMasterByName
这个是根据name
找,所以直接是dictFind
+dictGetValue
- sentinelRedisInstanceLookupSlave
这个是根据Slave的ip:port
,从Master的slaves
里面找对应的Instance。
看上去这类似getSentinelRedisInstanceByAddrAndRunID
也要遍历,但由于Slave的特殊取名规则,所以事实上直接生成name,dictFind
即可。
各种time梳理
Instance
last_pub_time
last_hello_time
我们上一次从Pub/Sub听到这个Sentinel的Hello时间(所以只对SRI_SENTINEL
有效)。
在sentinelProcessHelloMessage里面更新。last_master_down_reply_time
对SENTINEL is-master-down
的最后一次回复的时间。s_down_since_time
/o_down_since_time
down_after_period
info_refresh
role_reported_time
slave_conf_change_time
master_link_down_time
slave_reconf_sent_time
failover_state_change_time
故障转移中状态变更的时间failover_start_time
下一次FailOver开启时间,一般更新于:- 当我们投了票:
sentinelVoteLeader
- 当我们开启了FailOver:
sentinelStartFailover
- 当我们投了票:
failover_delay_logged
Link
cc_conn_time
/pc_conn_time
pc_last_activity
last_avail_time
上次对端对我们发送的PING进行回复的时间,注意这个回复我们要认为是合法的才计算。act_ping_time
根据注释,最后一个PING发出的时间,如果为0,表示刚收到一个PONG,还没有来得及发送PING,详见后文的介绍。last_ping_time
根据注释,是我们上一次发送PING的时间。
相比act_ping_time
,它的作用是避免在一段时间内发送较多的PING。last_pong_time
last_reconn_time
各种timeout梳理
failover_timeout
默认为3分钟的故障转移超时时间。election_timeout
是默认值和failover_timeout
之间的最小值。
sentinelState类和sentinel对象
sentinelState
是一个全局单例,表示全局上下文,我们一般用sentinel.*
来访问这个单例的字段。
1 | struct sentinelState { |
masters
是从实例名到sentinelRedisInstance
指针的映射。
1 | ... |
如果给定announce_ip:announce_port
,那么在gossip(实际上就是发送Hello消息)的时候,会用这个地址而不是基于anetSockName
函数检测的本地地址。
1 | ... |
用来支持SIMULATE-FAILURE
命令。
1 | ... |
【辨析】sentinelState和sentinelRedisInstance的区别
在阅读源码的过程中,有一个混淆点,也就是说具有SRI_SENTINEL
标识的sentinelRedisInstance
和sentinelState
有什么区别呢?
其实,sentinelState
表示这个Sentinel进程的描述的Sentinel的上下文,而各个sentinelRedisInstance
表示这个Sentinel进程监视的所有实例,为了监视这些实例,需要从本进程创建连接。而具有SRI_SENTINEL
标识的sentinelRedisInstance
就表示这个进程监视的Sentinel实例。
初始化
消息
主要解决下面的问题:
- Sentinel 如何交流
- Sentinel 如何发现某个 Master 的所有 Slave
INFO消息
这个消息是由哨兵节点发送给Master/Slave的,目的是: - 发现Slave节点
- 确认主从关系
这个消息会用sentinelRefreshInstanceInfo
这个大函数来处理。
PING消息
检查实例(Master/Slave/Sentinel)是否超时/返回错误,从而决定是否进入SDOWN状态。
PUB/SUB __sentinel__:hello 频道
现在,我们有了INFO来检测节点的复杂状态,PING来检测节点心跳。那么如何让所有检测当前Master的Sentinel来互相发现和交流和交流呢?
因此,有了这个用来广播Hello的频道__sentinel__:hello
,其作用是:
- 广播自己(Sentinel)的存在
- 发送Master的当前配置给其他Sentinel用于同步
【一个疑问】看sentinelReconnectInstance
的代码,Sentinel并没有订阅__sentinel__:hello
频道啊?Redis文档中却强调Every Sentinel is subscribed to the Pub/Sub channel
__sentinel__:hello
of every master and replica, looking for unknown sentinels. When new sentinels are detected, they are added as sentinels of this master. (注意,这里的replica应该是由于zz正确的关系改名的,其实就是Slave。)
我在爆栈网提了个问题。 但其实这个原因是这样的,sentinelReconnectInstance
的代码表示这个Sentinel没有订阅自己所监视的Sentinel的__sentinel__:hello
频道,它是订阅的自己监视的Master/Slave的__sentinel__:hello
频道。
这个消息的格式是:
1 | 0 1 2 3 4 5 6 7 |
这个协议在sentinelSendHello
中通过PUBLISH
命令发送,每个Sentinel在sentinelProcessHelloMessage
里面处理这些消息。
此外,注意sentinelEvent
这类事件机制,也可能通过PUB/SUB的方式广播,但不属于hello频道了。客户端可以订阅Sentinel的其他消息,例如配置变更信息
1 | +switch-master <master name> <oldip> <oldport> <newip> <newport> |
连接
相比3.0版本,目前版本的Redis把连接相关的逻辑都放到了instanceLink
里面。
Redis Sentinel会对每个Instance维护2个Hiredis连接cc
和pc
,其中cc
用于发送命令,而pc
用来做Pub/Sub。
为什么不直接用一个连接呢?有一种解释是为了防止command连接断开时,丢失广播的消息。
1 | typedef struct instanceLink { |
此外,如果我们用过redis-cli
,就会发现,在键入PUBLISH
命令之后,整个cli会被阻塞,从而无法再发送其他命令。但通过Hiredis客户端访问时,使用非Async命令,其实也不会阻塞。
1 |
|
通过Async指令,也不会
1 | void getCallback(redisAsyncContext *c, void *r, void *privdata) { |
提供Makefile如下,注意需要配置uv、ev和lua-devel
1 | pubsub_test: pubsub_test.c $(REDIS_SERVER_NAME) $(REDIS_CHECK_AOF_NAME) |
连接的概述
连接需要解决这三个问题:
- 如何连接到监视的Master?
我们会创建到我们监视的Master的cc/pc连接。
其中,cc连接表示Client连接,pc连接表示PubSub连接。 - 如何连接到监视的Slave?
我们会创建到我们监视的Slave的cc/pc连接。
可是,我们如何发现某个Master的新的Slave的呢?答案是通过监视的Master的INFO命令的返回来获取的,我们可以在源码中搜索"+slave"
来追踪这个过程。 - 如何连接到监视的Sentinel?
我们会创建到我们监视的Sentinel的cc连接,不会创建pc连接。
答案是通过__sentinel__:hello
来获取的。在sentinelReconnectInstance
函数中会用SUBSRIBE
订阅所有的Master/Slave的连接。这个命令的回调是sentinelReceiveHelloMessages
,每当有消息过来,这个函数会被触发,从而来更新Sentinel。
连接的基础:Hiredis
Sentinel使用Hiredis中提供的Async系列方法进行通信。
1 | // deps/hiredis/async.h |
初始化
Link使用下面方式进行初始化
1 | /* Create a not yet connected link object. */ |
我们讨论下到底在哪些情况下disconnected
会为1:
- link刚创建
- link被关闭:instanceLinkCloseConnection
- link出错:instanceLinkConnectionError
1 | ... |
将act_ping_time
设置为现在的时间,即使我们现在既没有建立连接,也没有ping。这个常用在我们没有连接成功时产生超时。
1 | ... |
解析地址
createSentinelAddr
这个函数用来把hostname:port
解析为sentinelAddr*
,稍后将会设置到ri->addr
上。
1 | /* Create a sentinelAddr object and return it on success. |
sentinelReconnectInstance
每一次sentinelHandleRedisInstance
的事件都会调用sentinelReconnectInstance
来尝试Reconnect(如果断线了的话)。
在sentinelReconnectInstance
函数中,在3.0版本中,检查如果对sentinelRedisInstance *ri
尚未建立有网络连接,则调用redisAsyncConnect
等函数建立网络连接。但现在进行了一些修改。
注意,只要cc
和pc
之间有一个连接断掉了,那么ri->link->disconnected
就是true了。
这里补充说明一下,在3.0版本中,断线是作为一个单独的flag即SRI_DISCONNECTED
展示的。但在目前版本中,整个连接相关的都放在link里面了,因此ri->link->disconnected
也在里面。
1 | void sentinelReconnectInstance(sentinelRedisInstance *ri) { |
port == 0
是无效地址,那么你一开始在createSentinelAddr
里面判断一下不就行了么?我想这个应该是因为port = 0
表示内核为我们分配一个端口,在TCP/IP层面是有意义的,但是我们这里赋予了port = 0
特殊的意义,在下面sentinelProcessHelloMessage
的讲解中会进行说明。
1 | ... |
我们对断线重连也要设置interval,为SENTINEL_PING_PERIOD
。
1 | ... |
下面处理cc
连接
1 | ... |
下面是两个回调函数,它们的作用都是在连接失败的时候调用instanceLinkConnectionError
,将这个连接置为NULL,并且设置disconnected
1 | ... |
对于Master和Slave节点,我们处理pc
连接。这里我有点疑问,Sentinel肯定需要连接PubSub的发送Hello消息和配置的啊,为啥这里不初始化pc
,甚至Sentinel之间都不用pc呢?
首先,我们注意到在sentinelSendHello
中,SENTINEL_HELLO_CHANNEL
这个通道的消息是通过cc
而不是pc
发送的。
1 | ... |
下面的命令发出一个SUBSCRIBE __sentinel__:hello
指令,并且用sentinelReceiveHelloMessages
作为回调。
真相水落石出,原来pc
端口是专门用来接收Hello信息的。
1 | ... |
连接断线和宕机有什么区别?
我们首先看看link->disconnected
什么时候是1:
- 刚创建连接
- 关闭连接调用
instanceLinkCloseConnection
releaseInstanceLink
sentinelUpdateSentinelAddressInAllMasters
sentinelResetMaster
sentinelReconnectInstance
sentinelCheckSubjectivelyDown
【?】 instanceLinkConnectionError
sentinelLinkEstablishedCallback
sentinelDisconnectCallback
入口
Redis的时钟中断处理例程serverCron
被server.hz
为时间间隔触发。在这个函数中会异步处理很多逻辑,例如:
- 【databasesCron】主动expire key,当然也有在look up的时候lazy expire的,这个lazy逻辑我们介绍过
- Software watchdog:watchdogScheduleSignal
- 更新一些统计,例如cronUpdateMemoryStats
- 【databasesCron】调用dictRehashMilliseconds
- 触发BGSAVE/AOF
- 【clientsCron】客户端超时
- 【replicationCron】Replication相关
run_with_period
表示每隔多少秒执行一次。
1 | // server.h |
sentinelTimer
是Sentinel的主例程。
1 | void sentinelTimer(void) { |
我们持续变更Redis的”timer interrupt”的频率,这样每个Sentinel的频率都是不同的。这种不确定性避免了Sentinel在同一个时间启动,同一时间发起投票,从而导致脑裂。当然,后面还会看到在设置failover_start_time
时,给每个选举epoch也设置了不同的时长,这应该是也是避免此类问题的一个思路。
我们的CONFIG_DEFAULT_HZ
默认是10,表示一秒钟触发10次。
1 | ... |
主体流程综述
这些流程的时间线可能是互相重叠的。
检测S/ODOWN流程
- 定期发送心跳
发送是在sentinelSendPeriodicCommands
中。 - 检查有没有SDOWN
sentinelCheckSubjectivelyDown
注意,SDOWN的检测对Master、Slave和Sentinel都起作用。
一般SDOWN有几种情况:- 连接超时
取决于ri->link->act_ping_time
距离现在的时间。 - 连接关闭
取决于断线,即ri->link->disconnected
后,ri->link->last_avail_time
距离现在的时间。 - Master报告要变成Slave,但是在一定时间内没有完成这个过程
也就是说ri->role_reported
是Slave,但是ri->flags
还是SRI_MASTER
- 连接超时
- 如果是Master,检查有没有ODOWN
在发现了SDOWN之后,Sentinel要做下面的事:- 发起的Sentinel询问其他Sentinel节点它们有没有觉这个节点SDOWN了
sentinelAskMasterStateToOtherSentinels
实际上就是发送is-master-down-by-addr
命令,但这里参数里面不带自己的runid,而是传入*
。 - 其他Sentinel节点回复消息(投票)
在sentinelCommand
里面对sentinelAskMasterStateToOtherSentinels
的回复。 - 发起的Sentinel处理投票消息
sentinelReceiveIsMasterDownReply - 发起的Sentinel统计票数
sentinelHandleRedisInstance
里面的第二个sentinelCheckObjectivelyDown
如何判断是否ODOWN,实际上就是统计具有SRI_MASTER_DOWN
标签的实例数量。
- 发起的Sentinel询问其他Sentinel节点它们有没有觉这个节点SDOWN了
FailOver流程
基于官方文档整理:
- 检查Master是否已经ODOWN
同上面的流程 - 检查自己是否需要开启FailOver
这一部分是在sentinelStartFailoverIfNeeded
中实现的,这个函数被sentinelHandleRedisInstance
调用。
在这里需要检查master->failover_timeout*2
的要求,对于下面的两种情况就不能发起选举- 如果自己在这个时间内投过票
【Q】这是在哪里检查?这个应该是sentinelVoteLeader
里面会设置failover_start_time
- 如果自己在这个时间内发起过FailOver
- 如果自己在这个时间内投过票
- 自增
current_epoch
进行,并开始竞选。
这一部分逻辑以sentinelStartFailover
开始,它设置FailOver状态SRI_FAILOVER_IN_PROGRESS
,从而开启FailOver状态机。
之后FailOver状态机函数sentinelFailoverStateMachine
会发挥作用。
然后,发送带runid的is-master-down-by-addr
,这要和ODOWN状态的is-master-down-by-addr
进行区分。
更新failover_start_time
,这个和election_timeout
一并用来控制选举超时时刻。 - 竞选部分
详见“选举流程”的说明。 - 如果竞选失败,则在
master->failover_timeout*2
之后,重新尝试竞选。 - 如果竞选成功,则
- 选出一个Slave,并将它升级为Master
这个选出的Slave被存放到promoted_slave
里面。
详情见下面“选择合适的Slave”的说明。
在选定Slave之后,FailOver状态切换为SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE
。 - 向被选中的Slave即
promoted_slave
发送SLAVEOF NO ONE
,让它转变为Master
在发送完SLAVEOF之后,FailOver状态会被切换到SENTINEL_FAILOVER_STATE_WAIT_PROMOTION
。
然后,我们会在sentinelRefreshInstanceInfo
中监听这个节点的变化,如果这个转换是成功的,会看到role的转换。那么就可以将状态切换到SENTINEL_FAILOVER_STATE_RECONF_SLAVES
了。否则,在sentinelFailoverWaitPromotion
中就会让这个FailOver过程失败掉。 - 通知所有Sentinel更新配置
通过Pub/Sub,将更新后的配置传播给所有其他Sentinel,其他Sentinel对它们自己的配置进行更新。
【Q】应该通过这个函数来处理sentinelForceHelloUpdateForMaster
。 - 通知所有Slave更新Master
向已下线Master的其他Slave发送SLAVEOF host port
命令,让它们去复制新的Master。
【Q】应该是sentinelFailoverReconfNextSlave
- 当所有Slave都已经开始复制新的Master时,Leader终止这次FailOver操作。
对应于sentinelFailoverSwitchToPromotedSlave
。主要是重新设置master
和master->slaves
到新的地址。期间会调用到sentinelResetMaster
,这个函数会重置failover_state
。
- 选出一个Slave,并将它升级为Master
选举流程
选举流程是嵌套在FailOver流程中的,但是又自成体系,所以拿出来单独说:
- 检查自己是否可以竞选【同FailOver流程的相同步骤】
- 自增
current_epoch
进行,并开始竞选【同FailOver流程的相同步骤】
在这里需要发送带runid的is-master-down-by-addr
,表示请求其他Sentinel投票。 - 给自己投票
这是在sentinelVoteLeader
里面的。
设置master->leader
和master->leader_epoch
为自己。这里leader是在master
下面的一个变量,这也暗指,Sentinel能同时处理多个Master的FailOver。 - 其他Sentinel处理Candidate的
is-master-down-by-addr
在sentinelCommand的某个分支下面:- 处理是否自己觉得SDOWN了。
- 如果是竞选消息,那么会调用
sentinelVoteLeader
来投票。
- Candidate在
sentinelFailoverWaitStart
中自旋,调用sentinelGetLeader
统计票数,如果成功,会成为了Leader,进入下面FailOver的流程- 如果在超时时间
election_timeout
内,Candidate没有获得超过quorum,自己的这次选举就失败了。 - 如果在超时时间内,没有一个Candidate获得更多的票数,那么等待
master->failover_timeout*2
后,Candidate增加epoch重新投票。由于failover_start_time
在设置时会被随机增加一个值,所以导致选举超时的时间也是不定的。
- 如果在超时时间
- 与Raft协议不同,Leader并不会把自己成为Leader的消息发给其他Sentinel。
其他Sentinel等待Leader从Slave选出Master后,检测到新的Master正常工作后,就会去掉客观下线的标识,从而不需要进入故障转移流程
容易看出选举流程借鉴了Raft的思想,但其中还是有相当的区别:
- Sentinel将选举和“RSM/日志”分开了,“维护RSM/日志”的是Master/Slave,而进行选举的是Sentinel。
换句话说,Sentinel管的是别的集群的FailOver,而不是自己的。 - 选举Sentinel Leader和选择合适的Slave是分开的逻辑
- Sentinel Leader选举只考虑epoch,并不需要在
is-master-down-by-addr
中附上自己要Select成为新Master的Slave。 - 在Leader选举过程中,有两个共识需要达成:某个Master是否SDOWN/ODOWN;选择哪个Leader。
- 当Sentinel Leader被选出来之后,由它独自处理FailOver流程,包含Select Slave部分。
- Sentinel Leader选举只考虑epoch,并不需要在
- 一个Sentinel节点能够处理多个Master-Slave集群的FailOver,而Raft协议只能处理自己集群的FailOver。
所以说,我们能看到epoch,比如leader_epoch
、current_epoch
啥的。详见“sentinelRedisInstance”这一章节中,对四个epoch的辨析。 - 在Raft中,Candidate竞选失败后,会过随机的时间后重新竞选,这个在Redis中并没有发现。
选择合适的Slave
这个过程,是由选举出来的Leader单独负责的。
需要辨析的点
master->leader_epoch
和sentinel.current_epoch
见之前“sentinelRedisInstance”这一章节中,对四个epoch的辨析。
Sentinel机制的风险
1 | min-replicas-to-write 1 |
例程详解
“主体流程综述”中已经概括了FailOver等流程的主要内容了,本章节对源码进行说明。
由于sentinelTimer
基本上包括了Redis Sentinel的所有逻辑,所以我们单独开一个章节来讲。
sentinelCheckTiltCondition
判定是否要进入TILT模式。当程序发现两次sentinelTimer
之间的时间差为负值或者大于SENTINEL_TILT_TRIGGER = 2000
时,就会进入 TILT 模式。这通常意味着:
- Sentinel进程被阻塞:需要载入的数据(?)大、机器IO繁重、进程被信号停止
- 系统时钟出现了问题
对于这种情况,会设置tilt=1,此时这个sentinel就会罚站SENTINEL_TILT_PERIOD
这么长时间。
sentinelHandleDictOfRedisInstances
这个其实是Sentinel的主要逻辑。它会遍历监视的所有节点(包括Master、Slave和其他Sentinel),首先执行sentinelHandleRedisInstance
。如果发现这个节点是SRI_MASTER
,则对它的所有Slave和Sentinel也调用这个函数。注意这个函数并不是一个DFS或者递归。事实上他最多就两层,唯一的嵌套就是对于每个Master,找到它所有的Slave和监视它的所有Sentinel。
1 | void sentinelHandleDictOfRedisInstances(dict *instances) { |
当一个Slave在SENTINEL_FAILOVER_STATE_UPDATE_CONFIG
时,说明此时所有Slave已经完成同步,要进行配置更新。此时需要调用sentinelFailoverSwitchToPromotedSlave
函数。这个是FailOver自动机的最后一步,我们将它迁移到后面进行讲解。
1 | ... |
sentinelHandleRedisInstance 监控部分
1 | /* Perform scheduled operations for the specified Redis instance. */ |
sentinelReconnectInstance
见上文连接部分
sentinelSendPeriodicCommands
sentinelSendPeriodicCommands
负责向sentinelRedisInstance *ri
发送命令。
这里的注释说link
是一个instanceLink
对象。这个对象的作用是为了节约hiredis连接建立的数量,如果有5
个Sentinel监视100
个Master,那么会创建5
个连接而不是500
个连接。这一段有点令人费解,因为就算是这5个Sentinel共享链接,那也要它们两两互联的5*4
共同对外的100
个连接啊?这个查看sentinelTryConnectionSharing
会得到解答。TODO
1 | /* Send periodic PING, INFO, and PUBLISH to the Hello channel to |
因为INFO/PING/PUBLISH都不是极端重要的指令,所以我们对pending_commands
设定了一个上限SENTINEL_MAX_PENDING_COMMANDS
。当然还有一个兜底的情况就是当命令太多了,连接就会断掉触发重连。
1 | ... |
如果ri
是一个Slave节点,并且它对应的Master节点出于ODOWN,或者FAILOVER状态,或者,那么我们应该加快发送INFO的频率到1000ms一次,否则就是SENTINEL_INFO_PERIOD = 10000ms
,此时Slaves中会有被其他Sentinel或者sysadmin提升为Master的情况。此外,当master_link_down_time
不等于0(应该是Slave从Master断开了,导致Replication过程中断),也要更频繁的INFO,从而有一个更加清楚的断连时间记录。
1 | ... |
下面设置PING指令的间隔。down_after_period
表示实例经过down_after_period
之后会被认为SDOWN。
这个值等于master->down_after_period
,如果自己是Master的话,就取SENTINEL_DEFAULT_DOWN_AFTER = 30000
1 | ... |
下面调用redisAsyncCommand
对发送INFO,我们不需要向Sentinel发送INFO,这是因为我们不需要知道Sentinel的Slave和主从关系(其实也没有)。如果info_refresh
为0表示我们从来没收到过INFO消息。
1 | ... |
下面是发送PING,我们同样也要PING。相比3.0版本,现在对于last_ping_time
超过ping_period/2
的情况也会发送PING了。这样的目的是什么呢?
1 | ... |
下面通过PubSub发送一个Hello消息。这个消息是为了达成在“消息”章节中提到的两个目标。我们将在后面详细解读。
1 | ... |
【接上】sentinelInfoReplyCallback
INFO命令的回复会被注册到来处理
1 | void sentinelInfoReplyCallback(redisAsyncContext *c, void *reply, void *privdata) { |
这个函数的主干实际上就是调用sentinelRefreshInstanceInfo
,这个函数超级大,用来解析INFO返回的信息,我们不在这里全部列举出来。在sentinelFailoverWaitPromotion
等地方,我们会将这个函数中涉及的相关部分拎出来讲。
sentinelSendHello
这个函数通过Pub/Sub发送Hello指令到ri
,完成“消息”章节中提到的两个目标。消息的格式如下。
1 | sentinel_ip,sentinel_port,sentinel_runid,current_epoch,master_name,master_ip,master_port,master_config_epoch |
如果PUBLISH指令成功入队,那么就返回成功。
在3.0版本中,通过anetSockName
直接获得ip,直接用server.port
。但在这里会复杂一点。
1 | int sentinelSendHello(sentinelRedisInstance *ri) { |
下面我们生成要广播的配置,其格式是
1 | sentinel_ip,sentinel_port,sentinel_runid,current_epoch ,master_name ,master_ip ,master_port ,master_config_epoch |
下面就是执行一个PUBLISH命令。有趣的是这个PUBLISH是通过cc
而不是pc
发送的。
1 | ... |
【接上】sentinelReceiveHelloMessages
1 | /* This is our Pub/Sub callback for the Hello channel. It's useful in order |
sentinelProcessHelloMessage
这个函数会处理包括新Sentinel发现、Sentinel地址变更、TODO之类的逻辑。
这个函数在Master/Slave实例中处理收到的Hello信息,或者sent directly to this sentinel via the (fake) PUBLISH command of Sentinel.
再次复习一下消息的格式
1 | 0 1 2 3 4 5 6 7 |
下面看具体函数,主要过程是:
- 找到监控的master
- 尝试加入新Sentinel
- 检查
runid
- 检查
ip:port
- 检查
- 尝试更新
current_epoch
1 | void sentinelProcessHelloMessage(char *hello, int hello_len) { |
首先根据master_name
找到我们Sentinel中**对应的Instance对象master
**,如果找不到,就忽略这个请求。
【Q】 TODO所以我蛮好奇这个频道像一个大杂烩一样的,会不会造成处理的开销;抑或,我们可以对每一个Master建立一个单独的频道,因为Pub/Sub是允许pattern matching的。
1 | ... |
定位到这个Hello消息涉及的Master节点后,我们尝试更新监视这个Master的Sentinel列表。于是,我们去找符合sentinel_ip:sentinel_port
和master_name
的Sentinel节点si
。这里si
是一个sentinelRedisInstance
,表示sentinel_ip:sentinel_port
这个Sentinel节点,也就是我们消息的发送方。
1 | ... |
如果没找到,说明这是一个新发现的Sentinel,我们需要加到master->sentinels
里面。在加入之前,需要做一些额外处理。首先,要移除掉所有具有相同runid
的Sentinel,也就是说,可能有runid
相同的Sentinel,但是他们的ip:port
不同。这是因为可能同一个Sentinel的地址变了,我们要以runid
为准来区分不同的Sentinel。
这个过程需要扫一遍master->sentinels
。
1 | ... |
removeMatchingSentinelFromMaster
函数会返回removed
表示实际上移除了多少个节点。
1 | ... |
之前,我们检查了是否有runid
相同的实例。然后,我们还要检查sentinels
中是否还有ip:port
相同的Sentinel实例。
1 | ... |
如果有的话,我们要把这个旧的实例的port
改为0,从而表示这个地址是Invalid的。然后,我们要调用sentinelUpdateSentinelAddressInAllMasters
对other
进行修改,这个修改的目的是把所有Master中other
这个Sentinel的连接断开。
1 | ... |
新建一个runid的token[2]
的Sentinel,并使用请求发来的sentinel_ip:sentinel_port
作为地址。token[2]
是传过来的runid
,在createSentinelRedisInstance
中会被设置成Sentinel的name
。createSentinelRedisInstance
函数会直接把si
加到master->sentinel
里面。
1 | ... |
下面这个函数就是之前提到的500个连接复用到5个的函数。
1 | ... |
下面又一次调用sentinelUpdateSentinelAddressInAllMasters
,其目的是和上面的sentinelUpdateSentinelAddressInAllMasters
是类似的。但是上面的调用是用来处理!removed
的情况下,如果还有ip:port
相同的实例的情况。而这个是处理remove
的情况,也就是说已经找到一个runid
相同的。
1 | ... |
接着,我们需要尝试通过对端的current_epoch
来更新自己的current_epoch
。
1 | ... |
master_config_epoch
字段,来源于其他Sentinel在发消息时传输的自己的master->config_epoch
字段。通过比较远端的和自己的master->config_epoch
,如果我们的较小,则需要更新Master的配置信息。
1 | ... |
如果我们发现对方传来的Master的地址token[5]:master_port
和我们的地址master->addr
是不同的,就TODO。
1 | ... |
sentinelUpdateSentinelAddressInAllMasters
当我们发现某一个Sentinel在Hello信息中报告了一个新的地址,我们就需要在所有Master中更新这个Sentinel的上下文,并进行重连。返回有多少地址发生了重连。
1 | int sentinelUpdateSentinelAddressInAllMasters(sentinelRedisInstance *ri) { |
这个用法看起来很奇怪,C语言里面的逗号表达式的含义是从左到右执行,并且以最后一个表达式的值作为返回值。我们展开来得到的是sentinelRedisInstance *master = ((de)->v.val), *match
,可是我们还是不知道match
是在哪里定义的。所
1 | ... |
以我猜想它实际上是可以拆成下面这两行
1 | sentinelRedisInstance *master = dictGetVal(de); |
下面,我们查找这个Master中所有runid
相等的Sentinel,并把它们关掉。
1 | ... |
释放原来的地址match->addr
,复制新的地址ri->addr
。这个地址包含addr和port。
1 | /* Update the address of the matching Sentinel by copying the address |
sentinelTryConnectionSharing
这个函数尝试为传入的ri
(它必须是一个Sentinel)分享已有的link,被分享的连接来自于我们这个Sentinel已有的对这个Master的连接。
有点难懂,我们先看下实现。这个函数会遍历所有的Master Instance,即sentinel.masters
。一旦我们找到ri->master
对应的实例,我们就复用这个实例的连接。
1 | /* |
【接上】sentinelPublishReplyCallback
这个命令的回调主要用来更新last_pub_time
1 | void sentinelPublishReplyCallback(redisAsyncContext *c, void *reply, void *privdata) { |
sentinelHandleRedisInstance 处理部分
首先是对tilt的处理。
1 | /* ============== ACTING HALF ============= */ |
下面检查是否是SDOWN状态
1 | ... |
如果是Master的话,需要通过sentinelCheckObjectivelyDown
查看是否进入的ODOWN状态,并设置对应的SRI_O_DOWN
位。在这之后,就需要通过sentinelStartFailoverIfNeeded
查看是否需要启动FailOver了。
我们需要注意,每一次的主循环,无论是不是在FailOver/SDOWN状态,都会对监视的所有Master节点执行sentinelCheckObjectivelyDown
、sentinelFailoverStateMachine
、sentinelAskMasterStateToOtherSentinels
这几个函数,所以在这些函数里面会看到,他们都会去判断是否是FailOver/ODOWN/SDOWN。
1 | ... |
【Q】sentinelAskMasterStateToOtherSentinels
会被以SENTINEL_ASK_FORCED
和SENTINEL_NO_FLAGS
这两个参数调用两次。可以发现,只要执行了sentinelStartFailoverIfNeeded
并返回true,那sentinelStartFailover
肯定已经被调用过了。这也就意味着此时已经ODOWN了,并且在FailOver了。为什么还要调用这个函数,问对方是不是is-master-down-by-addr
呢?
- 调用A:
sentinelStartFailoverIfNeeded
返回值为true
调用A的那一次是因为我们开启了FailOver,此时我们非常确定ODOWN了,所以加上了SENTINEL_ASK_FORCED
。 - 调用B:在
sentinelFailoverStateMachine
之后再调用一次
调用B是routine work,这是必要的。比如说一个Sentinel发现SDOWN了,此时并不一定要开启FailOver,所以他需要通过这一个sentinelCheckObjectivelyDown
,去询问其他Sentinel是不是这个节点DOWN掉了。收集到的SRI_MASTER_DOWN
会被sentinelCheckObjectivelyDown
用来判定是不是发生了ODOWN。
如果集群状态正常的话,因为ODOWN条件不满足,在函数调用的开始就会被abort掉,我们不用担心副作用。
1 | /* Only masters */ |
sentinelCheckSubjectivelyDown
这个函数用来判断是不是SDOWN。我们判断Sentinel、Master和Slave是不是SDOWN。
首先是计算上次PING之后的未响应时间。这里比3.0的代码多了一个ri->link->disconnected
的判断,这个是为什么呢?last_avail_time
在收到对PING的三个合法回复的时候会进行更新。act_ping_time
表示最后一个PING发出的时间,如果为0,表示刚收到一个PONG(见sentinelPingReplyCallback
函数),并且还没有发出下一个PING。对应地,还有一个last_ping_time
,这个不会在收到PONG的时候置0。
1 | void sentinelCheckSubjectivelyDown(sentinelRedisInstance *ri) { |
如果检测到连接的活跃度(activity)很低,也就是说长时间没有反应,那么考虑断开,等待下一轮重连。这里重新连接是sentinelReconnectInstance
控制的。
首先检查cc连接的活跃度,如果连接了至少SENTINEL_MIN_LINK_RECONNECT_PERIOD
,并且仍然有一个pending了一半超时时间的PING,那么调用instanceLinkCloseConnection
关闭连接。
1 | ... |
然后检查pc连接的活跃度,如果连接了超过SENTINEL_MIN_LINK_RECONNECT_PERIOD
,并且在SENTINEL_PUBLISH_PERIOD * 3
时间内没有再有活动。
1 | ... |
下面就是SDOWN判断了,如果下面两个条件满足,设置SDOWN这个flag
对方Instance无应答
这里的down_after_period
即如下,默认是30秒。1
sentinel down-after-milliseconds mymaster 60000
目前我们认为该Instance是Master
但它报告它将成为Slave
但经过down_after_period
和两个SENTINEL_INFO_PERIOD
之后,这个操作仍然没有完成。
1 | ... |
否则移除SDOWN状态。注意,经过搜寻源码,只有这个地方会移除SRI_S_DOWN
这个标签。
1 | ... |
sentinelCheckObjectivelyDown
注意这里的ODOWN使用的是weak quorum,这里的weak体现在票数统计是在给定时间范围内,而不是在某个时间点上达成一致。这里可以了解一下strong quorum algorithm和流言协议(Gossip)。
下面这个循环遍历所有Sentinel,并且统计具有SRI_MASTER_DOWN
的数量,是否达到master->quorum
。
【Q】容易看到,这里是直接算quorum了,那么是在哪里请求投票的呢?答案是sentinelAskMasterStateToOtherSentinels
,后面会讲。
1 | void sentinelCheckObjectivelyDown(sentinelRedisInstance *master) { |
如果票数达标,就设置ODOWN。
1 | ... |
对于ODOWN的情况,需要调用sentinelEvent
事件发送到日志、PubSub,以及用户提醒脚本,然后在本地也要进行设置。
1 | /* Set the flag accordingly to the outcome. */ |
对从ODOWN恢复的情况,要同理进行恢复。
1 | ... |
检查quorum参数
目前版本提供一个ckquorum
命令用来判断Quorum的数量是否合法。
首先统计有多少能够使用的Sentinel节点。voters
表示从Master中记录的sentinels
数量,加上自己。然后遍历所有的Sentinel,统计出没有SDOWN和ODOWN的节点的数量为usable
。
1 | int sentinelIsQuorumReachable(sentinelRedisInstance *master, int *usableptr) { |
如果需要的quorum数量大于usable
,说明quorum永远达不到,返回错误。
1 | ... |
如果usable
没有达到多数,那么就说明这个决议即使达成也是无效力的。
1 | ... |
注意,对于quorum至少要占voters
绝对多数的控制在sentinelGetLeader
下面。
sentinelStartFailoverIfNeeded
在判断完ODOWN之后,就需要判断是否要启动故障转移(FailOver)过程。
在执行前,需要判断是否满足三个条件。
- 再次检查是否ODOWN
- 如果这个Master在FailOver过程中,我们没必要再重启一个FailOver
- 如果刚FailOver完,也不进行FailOver
这个时间是master->failover_timeout*2
,其中master->failover_timeout
默认值是SENTINEL_DEFAULT_FAILOVER_TIMEOUT =(60*3*1000)
,是3分钟。
【Q】这点也许是为了防止过于频繁的进行FailOver操作,但是既然故障了,又不FailOver那怎么办呢?这里的意思应该是此时这个Sentinel已收到了其他Sentinel的那票,说明其他Sentinel正在FailOver,所以要等待这么长时间再FailOver。这个有点类似于Raft里面的Election Timeout机制。
下面查看代码,这对应了前两点条件
1 | int sentinelStartFailoverIfNeeded(sentinelRedisInstance *master) { |
如上面所讲,第三个条件将“CD时间”设置在了master->failover_timeout*2
,也就是说,要满足failover_start_time
之后这么久时间才能开启FailOver。
那么failover_start_time
是在哪里被设置的呢?可以参考上文的说明。
1 | ... |
在三个return之后,到了FailOver的执行过程。需要注意的是,执行FailOver的Sentinel不一定是发起FailOver的Sentinel,这是因为FailOver总是由Sentinel里面的Leader执行的。但当FailOver发起之后会导致选举,所以可能到时候Leader也换掉了。这个和Raft中有点不一样,Raft中新Leader是不能提交老Leader的日志条目的。
1 | ... |
下面查看函数sentinelStartFailover
,这个函数被执行的时候我们可以assert现在这个Master是ODOWN了。
1 | void sentinelStartFailover(sentinelRedisInstance *master) { |
首先,会更新failover_state
字段,我们来看看这个字段的取值都是什么:
-
SENTINEL_FAILOVER_STATE_NONE
:没有在FailOver -
SENTINEL_FAILOVER_STATE_WAIT_START
:等待开始FailOver -
SENTINEL_FAILOVER_STATE_SELECT_SLAVE
:选择一个Slave作为新Master,即promoted_slave
。 -
SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE
:向该Slave发送SLAVEOF
指令以成为Master -
SENTINEL_FAILOVER_STATE_WAIT_PROMOTION
:向剩余Slave发送SLAVEOF
指令,让它们跟随新Master -
SENTINEL_FAILOVER_STATE_RECONF_SLAVES
:让剩余Slave开始复制新Master SENTINEL_FAILOVER_STATE_UPDATE_CONFIG
:此时所有Slave已经完成同步,需要进行配置更新。
这里的配置更新指的是当前Sentinel节点上的master
和master->slaves
等的配置。
下面继续查看源码,会自增sentinel.current_epoch
并赋值给Master的failover_epoch
,这个类似于Raft中任期的概念。
1 | ... |
接着,我们要更新被FailOver的Master节点的failover_start_time
。这里随机一个FailOver开始时间,类似Raft中的(Follower变成Candidate的?TODO)随机超时时间。
1 | ... |
sentinelAskMasterStateToOtherSentinels
这个函数有下面功能:
- 遍历所有Sentinel
- 清理过期状态
- 判断是否要询问这个Sentinel有关ODOWN的信息。
- 发送不带自己runid的
is-master-down-by-addr
消息,目的是确认是否ODOWN - 发送带自己runid的
is-master-down-by-addr
消息,目的是竞选
如果这个Sentinel认为Master已下线(即SDOWN),会向其他Sentinel发送SENTINEL is-master-down-by-addr
命令,尝试获得足够的票数,将Master标记为ODOWN状态,并开启FailOver。
这个函数会被以SENTINEL_ASK_FORCED
和SENTINEL_NO_FLAGS
这两个参数调用两次,原因在上文解释过了。
1 |
|
我们遍历所有的Sentinel。
1 | ... |
如果在5秒内都没有收到这个Sentinel对SENTINEL is-master-down-by-addr
的回复,那么就清理这个Sentinel的投票状态,因为这个状态可能过期了。
清理过程是清除SRI_MASTER_DOWN
标记,并且将ri->leader
清空。这里再提一下,Sentinel的ri->leader
只有在有SRI_MASTER_DOWN
标记时有效力,指向自己投票的Sentinel Leader。不要误以为始终指向当前的Leader,这个和Raft不一样,Sentinel只有在FailOver时候才会选举。
【Q】TODO 容易看到,这里处理了Sentinel超时的情况,但我们在前面看到,Sentinel节点也会判断其他Sentinel的SDOWN情况,为啥这里不用SDOWN来判断呢?
1 | ... |
只有在满足下面三种情况下才询问这个Sentinel:
- Master必须具有
SRI_S_DOWN
这个flag。
显然,如果我自己都不觉得SDOWN,那为啥还要问是不是ODOWN呢?
【Q】我们有必要在这里重复检查一下么? - 必须能够连接到这个Sentinel。
【Q】我们知道,如果认为SRI_S_DOWN
了,那么也要关闭连接。而SDOWN对Master、Slave和Sentinal都检查的。为什么这里还需要检查呢?
我们仔细看sentinelCheckSubjectivelyDown
的实现,其实里面不仅处理了SDOWN的情况。断开连接实际上对应了包括连接超时和不活跃之类的情况。 - 除非
SENTINEL_ASK_FORCED
,否则我们要在SENTINEL_ASK_PERIOD
时间内都没有收到ri
的SENTINEL is-master-down
的回复,我们才会再次询问。
1 | ... |
下面是具体询问流程,实际上就是发送一个SENTINEL is-master-down-by-addr
指令。**注意,redisAsyncCommand
中指定了处理回复的回调函数为sentinelReceiveIsMasterDownReply
**,方便我们跟踪流程。
**我们会发送自己的sentinel.current_epoch
**,对应到对端拿到的req_epoch
。
1 | ... |
如果启动了FailOver,那么把自己的sentinel.myid
发出去,竞选FailOver Leader,否则就发送*
,表示我们仅仅去监测有没有ODOWN。
1 | ... |
【接上】对sentinelAskMasterStateToOtherSentinels的回复
对端对sentinelAskMasterStateToOtherSentinels
发来的is-master-down-by-addr
消息的处理过程直接在sentinelCommand
里面,主要是两点内容:
- 首先返回这个Sentinel有没有觉得SDOWN。
- 如果这是一个竞选请求,调用
sentinelVoteLeader
来计算是否投票。
首先复习一下这个请求的格式。
1 | SENTINEL is-master-down-by-addr ip port current_epoch runid |
其中ip:port
是我们要检查的Master的地址。Note that the command will not check by name but just by master, in theory different Sentinels may monitor differnet masters with the same name.current-epoch
需要被用来判断我们是否能够对选举FailOver Leader进行投票。每个Sentinel每个epoch只能投一票。
如果runid
为*
,表示我们并没有向这个Sentinel请求投票,否则设置为我们想要它投票的runid
。
下面看具体代码
1 | // 在sentinelCommand函数中 |
首先是返回有没有觉得SDOWN。如果没有处于tilt状态,那么满足下面的情况下,我们返回1
- 如果我们检查到节点SDOWN了
- 并且这个节点还是Master
1 | /* It exists? Is actually a master? Is subjectively down? It's down. |
如果is-master-down-by-addr
这个请求带了runid
参数,说明这是一个带竞选的信息,此时会调用sentinelVoteLeader
来Vote。
1 | /* Vote for the master (or fetch the previous vote) if the request |
回复内容包含三点。
这个shared
实际上是在server.c中定义的struct sharedObjectsStruct shared;
,可以理解成一种共享小对象。
1 | /* Reply with a three-elements multi-bulk reply: |
sentinelVoteLeader
sentinelVoteLeader
函数对应了Raft中Follower投票的逻辑。这个函数返回一个char*
,表示这个节点投票支持的,执行master
这个节点FailOver过程的Leader,即master->leader
。容易看出,Sentinel能同时处理多个Master的FailOver。
这个函数的调用方有两个:
- 当对端收到带竞选的
is-master-down-by-addr
信息时 - 当自己开启竞选时
这个发生在后面的sentinelGetLeader
中
这个函数的参数是req_epoch
和req_runid
,表示处理req_runid
这一台Sentinel在req_epoch
的投票请求。此外,leader_epoch
是用来返回的,表示我们投出来的master->leader
是哪个epoch的。
这里的req_epoch
的取值规则如下:
- 如果是对方发来的
is-master-down-by-addr
,那么值是对方的sentinel.current_epoch
。 - 如果是自己的请求,那么是
ri->failover_epoch
,这里的ri
是我们正在处理的Master节点。
投票逻辑如下:
- 如果自己已经投了
- 如果是一个来自较新的epoch的投票请求
需要满足:- 他发过来的
req_epoch
严格大于我自己的master->leader_epoch
,这里leader_epoch
表示我刚才vote的Leader是哪个epoch的。
这个是对Master的epoch的判定。 - 他发过来的
req_epoch
大于等于我自己的sentinel.current_epoch
。
这个是对Sentinel的epoch的判定。
- 他发过来的
- 否则
返回自己原来投的票。
这通常发生在,有多个Sentinel同时竞选,而我已经给比req_epoch
更高的epoch投过票了,那么返回在这个更高的epoch投的票。
- 如果是一个来自较新的epoch的投票请求
- 如果暂时不能投票(TODO 对应什么情况呢?)就返回NULL
- 如果还没有投:
- 设置
master->leader
为req_runid
,也是函数的返回值 - 设置
master->leader_epoch
和leader_epoch
的值为sentinel.current_epoch
- 设置
1 | /* Vote for the sentinel with 'req_runid' or return the old vote if already |
如果,当前Sentinel节点记录的Master的epoch是陈旧的,并且Sentinel节点自己的current_epoch
不领先于req_epoch
,那么说明这是一个来自于较新的epoch的投票请求,我们应该投票。
1 | ... |
下面,如果我们没有投票给自己,就需要设置**failover_start_time
**。这里的myid
指的是自己的runid,在3.0版本,是用的server.runid
,可以参考前文的说明。failover_start_time
为当前时间,再加上随机值的偏移。这在前面提到过,作用是为了实现类似于Raft中的随机超时时间。
1 | ... |
【接上】sentinelReceiveIsMasterDownReply
我们看看Sentinel如何处理sentinelAskMasterStateToOtherSentinels
的返回结果,即投票结果。
下面是一些Sanity检查,可以跳过直接看主干。
1 | /* Receive the SENTINEL is-master-down-by-addr reply, see the |
首先看第一个参数element[0]
,表示对Master是否宕机的Reply。
1 | ... |
第二个参数element[1]
,如果不是"*"
,表示对方投票了,此时更新ri
的投票结果r->element[1]->str
到ri->leader
。
1 | ... |
sentinelFailoverStateMachine
这个函数是FailOver状态机的入口,每次时钟循环都会调用一次。他负责处理除了SENTINEL_FAILOVER_STATE_NONE
和SENTINEL_FAILOVER_STATE_UPDATE_CONFIG
(实际上是一头一尾)的所有命令。
每一次状态转移都需要更新failover_state_change_time
,这个时间会在一些地方被用来处理Abort逻辑。
这个函数需要在SRI_FAILOVER_IN_PROGRESS
下才会运行,这个位是由sentinelStartFailover
设置的。
主要内容是一个dispatch的逻辑,根据当前FailOver状态机的状态ri->failover_state
去执行对应的函数。
我们容易看出,状态机状态为X的时候,表示前一个状态对应的行为已经完成,但是当前状态的行为并不一定完成。也就是状态X表示正在做X,而不是X已经做完了,这应该是状态机设计的时候一个比较通用的做法吧。
1 | void sentinelFailoverStateMachine(sentinelRedisInstance *ri) { |
这个函数发送sentinalSignal
:
-failover-abort-not-elected
+elected-leader
和+failover-state-select-slave
可能到达状态:
SENTINEL_FAILOVER_STATE_NONE
SENTINEL_FAILOVER_STATE_SELECT_SLAVE
1 | ... |
这个函数发送sentinalSignal
:
-failover-abort-no-good-slave
+selected-slave
和+failover-state-send-slaveof-noone
可能到达状态:
SENTINEL_FAILOVER_STATE_NONE
SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE
1 | ... |
这个函数发送sentinalSignal
:
-failover-abort-slave-timeout
+failover-state-wait-promotion
可能到达状态:
SENTINEL_FAILOVER_STATE_NONE
SENTINEL_FAILOVER_STATE_WAIT_PROMOTION
1 | ... |
这个函数发送sentinalSignal
:
-failover-abort-slave-timeout
可能到达状态:
SENTINEL_FAILOVER_STATE_NONE
1 | ... |
这个函数发送sentinalSignal
:
- 自己
-slave-reconf-sent-timeout
+slave-reconf-sent
sentinelFailoverDetectEnd
+failover-end-for-timeout
+failover-end
+slave-reconf-sent-be
可能到达状态:
SENTINEL_FAILOVER_STATE_UPDATE_CONFIG
1 | ... |
sentinelFailoverWaitStart
首先检查自己是不是这个epoch的FailOver的Leader,这个通常伴随着选举的过程,在选举过程中,Candidate会在这边自旋,并调用sentinelGetLeader
统计票数。TODO 加一个Demo。
注意之前提到过,FailOver的发起者不一定是FailOver的执行者。这个过程涉及调用sentinelGetLeader
函数,可能会触发选举。
这里的ri
是我们正在处理的master节点所对应的Instance。
1 | /* ---------------- Failover state machine implementation ------------------- */ |
如果我自己不是Leader,并且这个FailOver不是由SENTINEL FAILOVER
命令触发的强制FailOver,那么就不进行下面的FailOver流程。这个逻辑很简单,Raft中发起AppendEntries
也都是由Leader来发起的。
1 | ... |
如果超过了election_timeout
,我们会直接Abort整个FailOver过程。
1 | ... |
否则切换状态到SENTINEL_FAILOVER_STATE_SELECT_SLAVE
,也就是说从Slave中挑选一个新的Master。
1 | ... |
【接上】sentinelGetLeader
这个函数用来返回给定的epoch
对应的Leader。这里的epoch
实际上是ri->failover_epoch
,也就是当前进程需要FailOver的Master的failover_epoch
。
【Q】为什么不用sentinel.current_epoch
呢?我认为这是因为同一个Sentinel可能处理若干个Master的FailOver过程,所以sentinel.current_epoch
和ri->failover_epoch
不能一一对应。
扫描这个Master下面的所有Sentinel,检查对于epoch
是否存在一个Leader。为了成为Leader,我们需要有majority的节点都投票给这个Leader,参与投票的节点由上次SENTINEL RESET
的指定。
一个问题是,Leader选举是在什么时候开始的呢?我们需要注意的是sentinelGetLeader
这个函数并不是只会触发一遍,而是在整个sentinelTimer
上都会进行调用。在第一次调用的时候,肯定是一票都没有,winner
是NULL
,此时就给自己投一票,同时函数sentinelAskMasterStateToOtherSentinels
会发送is-master-down-by-addr
给另外一方。
1 | char *sentinelGetLeader(sentinelRedisInstance *master, uint64_t epoch) { |
首先,断言一下Master是ODOWN状态,并且FailOver在进行中
1 | ... |
下面这个循环,对于每一个Sentinel选出的Leader,我们都投上一票。看起来这个很奇怪,感觉和墙头草一样?其实不然,这里“投上一票”的表述并不准确。ri->leader
实际上记录了这个Sentinel的投票结果,因此我认为这个循环实际上是记录了每一个Sentinel到底得了多少票。所以不是“投上一票”,而是记上一票。
1 | ... |
在遍历统计完投票情况后,检查谁胜出,必须满足下面的条件的,才会成为winner
。
- 绝对多数
- 必须大于
master->quorum
1 | ... |
现在需要再统计一下自己的票数。如果自己没投票,那么就投给winner;如果没有winner,就投给自己sentinel.myid
。
【Q】如果作为竞选的发起方,这里是走哪条路径的呢?我觉得根据上面的讨论,因为一开始协议刚发出去,一票都没有,肯定还没有决出winner,所以还是投自己一票的。
1 | ... |
上面只是发送Vote消息,sentinelLeaderIncr
函数是自增counters
的计数,这个发生在本地,把自己的一票也加上去。如果投票之后的票数比最大票数要大,那么更换Leader。
1 | ... |
下面检查winner的票数是否满足quorum和master->quorum
1 | ... |
sentinelFailoverSelectSlave
这个函数用来选择需要promote成Master的Slave。主要逻辑是调用sentinelSelectSlave
,然后根据他的返回值,去进行一些周边的处理,例如状态机的切换。
从原来所有的Slave中,挑选一个作为新的Master,如果没有合格的新Master,那么返回NULL
。这里同样有个sentinelAbortFailover
的分支来处理选不到的情况。
1 | void sentinelFailoverSelectSlave(sentinelRedisInstance *ri) { |
下面就是设置slave->flags
,设置ri->promoted_slave
1 | ... |
sentinelSelectSlave
首先是计算能接受的Slave和Master之间的Replication断连的最大时长,超过这个时间的Slave我们不考虑,从而保证数据库的时效性。down_after_period
表示在这段时间后会被认为短连了,所以max_master_down_time
等于Master的SDOWN到现在的时间,加上十倍的down_after_period
。这个计算方式很神奇。因为从当前Sentinel来看,Master已经处于下线状态,所以正常来说,Slave与Master之间的连接断开时间不应该超过down-after-period * 10
。这听上去有点像黑魔法,不过这个判断的原理是这样的:当Master下线之后,Master和Slave的连接就会断开,但只要先下线的是Master而不是Slave,即连接断开是由Master而不是Slave造成的,那么Master和Slave之间的连接断开时间就不会太长。不过这只是一个辅助手段,因为最终我们都会使用复制偏移量来挑选Slave。
1 | sentinelRedisInstance *sentinelSelectSlave(sentinelRedisInstance *master) { |
下面开始遍历所有的Slave来生成候选集instance
,需要满足下面的条件:
- 不能SDOWN或者ODOWN
- 不能短连
- Link不能超时
slave->slave_priority
不能为0。这个值是从INFO得到的,默认为SENTINEL_DEFAULT_SLAVE_PRIORITY = 100
- 对PING(5xPERIOD)/INFO(3xPERIOD)的回复不能超时
- 和Master的Replication断连的时长不能超过上面的计算值
1 | ... |
下面,我们对候选集进行排序,然后选到最优的
1 | ... |
compareSlavesForPromotion
主要比较优先级分为下面几个:
slave_priority
越小的slave_repl_offset
(replication offset)越大的,这表示从服务器的偏移量runid
字母序越小的
1 | /* |
因为一下老的slave没有publishrunid
,所以这里将其设置为NULL
,默认NULL
的字母序最大
1 | ... |
sentinelFailoverSendSlaveOfNoOne
这个函数负责对要提升的节点发送SLAVEOF NO ONE
,让它提升为Master。
如果link->disconnected
,那么不断重试直到超时failover_timeout
,触发sentinelAbortFailover
。ri->promoted_slave
是在sentinelFailoverSelectSlave
被设置的。
1 | void sentinelFailoverSendSlaveOfNoOne(sentinelRedisInstance *ri) { |
调用sentinelSendSlaveOf
,传入NULL
表示让这个ri->promoted_slave
执行SLAVEOF NO ONE
为Master。
1 | ... |
sentinelSendSlaveOf
这个命令先发送SLAVEOF
。再发送CONFIG REWRITE
,从而在可能的情况下将配置刷到配置上。可能的情况指Redis支持写Config,并且这个服务器是从Config文件启动的。
1 | /* |
如果host
是NULL
,发送SLAVEOF NO ONE
,让这台机器变为Master。
1 | ... |
出于安全因素,我们要开启一个事务,这个也是相比3.0升级的地方。包含:
- SLAVEOF
- 重新配置
- Disconnect all clients (but this one sending the commnad),从而触发
ask-master-on-reconnection
协议(?)
注意CLIENT KILL TYPE <type>
这个指令从2.8才开始有的。不过往低版本的发并没有为题,因为CLIENT
是一个variadic command,所以不会被认为是错误,所以事务不会失败。
1 | ... |
我们不通过命令的返回值来检查实际执行效果,而是观察下一个INFO。
1 | ... |
sentinelFailoverWaitPromotion
这个函数只负责处理失败的情况。对于成功的情况,我们是在监视INFO返回值的sentinelRefreshInstanceInfo
函数里面看的。
1 | /* We actually wait for promotion indirectly checking with INFO when the |
sentinelRefreshInstanceInfo 角色变化部分
函数sentinelRefreshInstanceInfo
作用是解析INFO命令的返回值,通过它来更新节点从Slave到Master的变化。
这个函数很长,我们只节选需要的部分来进行讲解。
下面的代码,可以解析出这个节点认为自己的角色,是Master还是Slave。
1 | // sentinelRefreshInstanceInfo |
跳过后面一堆代码,看到下面这个if。现在ri
在我的视野里是Slave,但是它却宣告自己是Master了。
1 | ··· |
这是怎么回事呢?如果ri
就是我们钦定的要成为Master的节点,并且我们Master确实在FailOver,并且我们的FailOver的状态确实是SENTINEL_FAILOVER_STATE_WAIT_PROMOTION
,这就说明我们对promoted_slave
的提升动作成功了,于是我们就可以进入下一步流程,即切换到状态为SENTINEL_FAILOVER_STATE_RECONF_SLAVES
。其实这些检查就是我们确定了Slave已经被Reconf为Master了,对应的失败情况,就是sentinelFailoverWaitPromotion
中看到的。
我们要将Master的config_epoch
设置为Master的failover_epoch
,这一步的原因是我们将config_epoch
同步为我们赢得选举胜利从而进行FailOver的epoch,这样如果没有一个更大的epoch的话,就会强迫其他Sentinel更新Config。
1 | ... |
下面更新failover_state
1 | ... |
sentinelForceHelloUpdateForMaster
这个函数会强制发送一个”Hello”信息到所有关联到ri->master
的Redis和Sentinel Instance。
从技术上来说,这个并不需要,因为我们每隔SENTINEL_PUBLISH_PERIOD
都会向所有Instance发送一个消息。但是当一个Sentinel更新了配置之后,尽快发送总是好的。
注意虽然Sentinel我们没有设置pc
,但是它实际上是通过cc
去sentinelSendHello
的。
1 | ... |
否则,一个Slave意外地变成了Master,此时如果我们的主服务器看起来正常,并且ri
在wait_time
时间内没有SDOWN或者ODOWN(?),我们要强制它变回Slave。这个通常是因为重启,或者之前下线的Master突然上线了。
但在这之前,我们需要等待一段时间,让它可以接收新配置(指什么呢?)。
对于这种情况,兜底选项是啥呢?如果啥都没有,那么这边就会超时,导致上面sentinelFailoverWaitPromotion
失败。但什么情况下会有这个分支呢?
1 | ... |
看一下服务器正常的条件:
- 它在我们眼里是Master
- 它自己也说自己是Master
- 它还没有SDOWN或者ODOWN
1 | int sentinelMasterLooksSane(sentinelRedisInstance *master) { |
sentinelFailoverReconfNextSlave
这个函数调用来自于FailOver状态机的SENTINEL_FAILOVER_STATE_RECONF_SLAVES
这个状态。它的主要功能是所有剩余的似乎还没有更新配置的Slave发送SLAVE OF
指令,也就是让他们去跟随新的Master,即promoted_slave
。
我们对指令的发送进行限流,同时只能存在master->parallel_syncs
条in-flight的指令。
这里的“似乎没有更新配置”指的是具有以下这个条件slave->flags & (SRI_RECONF_SENT|SRI_RECONF_INPROG)
,其实就表示已经在更新过程中,但是没更新完。
1 | /* Send SLAVE OF <new master address> to all the remaining slaves that |
下面这个循环统计已经在更新过程中的Slave数量到in_progress
里面。
1 | ... |
下面,我们又遍历一次循环,在这个循环里面,如果in_progress
的数量还小于设定的master->parallel_syncs
,我们将尝试对新的Slave发送SLAVE OF
指令。
我感觉每一次Tick都跑这个遍历,开销是不是有点大呢?其实不然,因为这个循环还要做其他事情,反倒是上一个循环也许可以优化下,比如说搞一个全局的in_progress
?
1 | ... |
如果对这个Slave已经发送了SLAVE OF
,但是这个Slave还是停留在这个状态,我们就认为它已经被Reconf了。这个处理正确么?注释的解释是Sentinel能够在稍后检测出这个Slave被错误地配置了,并且进行修复。
【Q】这个过程在哪里呢?
1 | ... |
对于过程中,或者断连了的节点,我们跳过。
1 | ... |
对于剩下来的情况,我们发送命令。
1 | ... |
下面的sentinelFailoverDetectEnd
检查如果所有的Slave都被配置了,则切换到SENTINEL_FAILOVER_STATE_UPDATE_CONFIG
。
1 | /* Check if all the slaves are reconfigured and handle timeout. */ |
【Q】和sentinelFailoverSwitchToPromotedSlave的区别在哪里?
详见后面对sentinelFailoverSwitchToPromotedSlave的介绍。
- 阶段
sentinelFailoverSwitchToPromotedSlave
的处理阶段是SENTINEL_FAILOVER_STATE_UPDATE_CONFIG
,而这个函数是SENTINEL_FAILOVER_STATE_RECONF_SLAVES
。
【Q】SRI_RECONF_SENT和SRI_RECONF_INPROG的区别是什么?
我们可以看到SRI_RECONF_
有三个子状态SRI_RECONF_SENT
、SRI_RECONF_INPROG
、SRI_RECONF_DONE
,是从左到右递进的。
在我们调用sentinelSendSlaveOf
后,会设置SRI_RECONF_SENT
。
在我们在sentinelRefreshInstanceInfo
中检测到这个Slave指向的Master的地址等于新Master即ri->master->promoted_slave
的地址时,会取消SRI_RECONF_SENT
,设置SRI_RECONF_INPROG
。
1 | // sentinelRefreshInstanceInfo |
因为此时状态已经是SRI_RECONF_INPROG
,所以当Slave到Master(肯定是新的而不是旧的)的连接是SENTINEL_MASTER_LINK_STATUS_UP
时,切换到SRI_RECONF_DONE
。
这个是通过INFO消息中的master_link_status:
字段来获得的。
1 | /* SRI_RECONF_INPROG -> SRI_RECONF_DONE */ |
当然,后面我们还会看到因为超时等原因设置为SRI_RECONF_DONE
等flag的。
sentinelFailoverDetectEnd
这个函数用来判断是否这个Master的所有Slave的同步都已经完成,此时,master
还表示当前Fail的Master。
下面的两个条件,主要是:
- 检查是否有合法的
master->promoted_slave
主要检查是不是SDOWN,不过为啥不检查是否ODOWN呢? - 具有Reconf完成的
flag
,或者SDOWN
【Q】为什么Slave发生SDOWN是可以接受的呢?
1 | void sentinelFailoverDetectEnd(sentinelRedisInstance *master) { |
下面是日经的超时检测,在前面的状态机中超时就经常会导致sentinelAbortFailover
。但是我们会忽略未完成的Slave,这个可能也是Sentinel可以在后面进行修复吧。
1 | ... |
如果此时,发现都Reconf了,那么就更新状态为SENTINEL_FAILOVER_STATE_UPDATE_CONFIG
。
1 | ... |
但是,如果超时的话,就需要再发送一次?
1 | /* If I'm the leader it is a good idea to send a best effort SLAVEOF |
sentinelFailoverSwitchToPromotedSlave
【Q】和sentinelFailoverReconfNextSlave
的关系是什么呢?见对sentinelFailoverReconfNextSlave
的论述。
这个函数主要负责配置更新。什么是配置呢?
于是将旧的Master即master
移出表,并把新的Slave即master->promoted_slave
加进去,其中master->promoted_slave
指向被提升为新Master的Slave。
1 | /* This function is called when the slave is in |
这里的ref
表示最新的Master。
1 | sentinelRedisInstance *ref = master->promoted_slave ? |
sentinelResetMasterAndChangeAddress
sentinelFailoverSwitchToPromotedSlave
实际上是FailOver自动机的最后一步。
主要逻辑是sentinelResetMasterAndChangeAddress
,这个函数用来设置Master的ip:port
,但保持Master名字不变,并最终调用sentinelResetMaster
。sentinelResetMaster
函数会重置master
这个sentinelRedisInstance
对象。与此同时,failover_state
这个状态也会被清空成SENTINEL_FAILOVER_STATE_NONE
。
这个函数通常用来处理+switch-master
消息,检索源码,我们确实看到所有+switch-master
的sentinelEvent
后面都会有这个函数。
这个函数返回错误,当ip:port
不能被resolve。
1 | int sentinelResetMasterAndChangeAddress(sentinelRedisInstance *master, char *ip, int port) { |
newaddr
是最新的Master的地址。
1 | newaddr = createSentinelAddr(ip,port); |
我们遍历原来Master的所有Slave,如果这些Slave的指向的Master地址slave->addr
还没有更新,我们就将它加入slaves
列表中,等待后续更新。
1 | /* Make a list of slaves to add back after the reset. |
如果Master的地址变了,我们就要把老Master放到slaves
列表中。
1 | /* If we are switching to a different address, include the old address |
下面,我们重置Master指针指向的对象,并且将slaves
列表中的对象加到这个新的Master中。
1 | /* Reset and switch address. */ |
实验
配置
我们的配置如下(推荐用自己编译的Redis,否则Redis服务会自动重启,需要用/etc/init.d/redis-server stop
关闭)
1 | Master localhost 6379 |
Slave
对于Slave1
1 | # redis2.conf |
运行
1 | ./src/redis-server redis2.conf & |
对于Slave2同理
INFO一下主节点
1 | # Replication |
Sentinel
对于Sentinel2,我们配置如下。注意log文件配置相对路径似乎会打印不了log
1 | port 26479 |
其他如法炮制
1 | ./src/redis-sentinel sentinel.conf |
启动后可以,注意我们可以看到runid,他是一个十六进制的串183af86ac492235bf97739bcdad0353f1d4e61df
1 | $ redis-cli -p 26379 |
还可以INFO一下
1 | 127.0.0.1:26379> info sentinel |
但似乎没有办法从Master找到其他的Sentinel。
注意,如果有Duplicated master name.
的错误,可以考虑删除掉conf下面这段自动生成的代码。这是因为sentinelFlushConfig
函数会触发对conf的回写。
1 | protected-mode no |
测试正常FailOver
现在我们Kill掉Master
Sentinel1
观察Sentinel1,它被选举为Leader
1 | 10467:X 22 Oct 2020 04:27:19.771 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo |
调用sentinelIsRunning
生成myid。
1 | 10468:X 22 Oct 2020 04:27:19.773 # Sentinel ID is 2ddc47e7af3cebe8b1642127ed72002bb918550c |
调用sentinelStartFailover
。
1 | 10468:X 22 Oct 2020 04:29:33.353 # +new-epoch 1 |
通过sentinelVoteLeader
投了自己一票,这也会产生一条消息。
1 | 10468:X 22 Oct 2020 04:29:33.355 # +vote-for-leader 2ddc47e7af3cebe8b1642127ed72002bb918550c 1 |
sentinelReceiveIsMasterDownReply
回调中收到了其他两个Sentinel的投票。
1 | 10468:X 22 Oct 2020 04:29:33.358 # 0833444de1d44b73ce6382af2ff0aeb21951b19a voted for 2ddc47e7af3cebe8b1642127ed72002bb918550c 1 |
sentinelFailoverWaitStart
在sentinelGetLeader
中的轮询结束了,进入下面的failover-state-select-slave
。
1 | 10468:X 22 Oct 2020 04:29:33.426 # +elected-leader master mymaster 127.0.0.1 6379 |
选择了6579这个Slave
1 | 10468:X 22 Oct 2020 04:29:33.489 # +selected-slave slave 127.0.0.1:6579 127.0.0.1 6579 @ mymaster 127.0.0.1 6379 |
Sentinel1
观察Sentinel2,它是一个Follower
1 | 10473:X 22 Oct 2020 04:27:22.770 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo |
投票给了Sentinel1
1 | 10474:X 22 Oct 2020 04:29:33.358 # +vote-for-leader 2ddc47e7af3cebe8b1642127ed72002bb918550c 1 |
即使已经票,Follower还是会执行sentinelCheckSubjectivelyDown
和sentinelCheckObjectivelyDown
的
1 | 10474:X 22 Oct 2020 04:29:33.395 # +sdown master mymaster 127.0.0.1 6379 |
在sentinelStartFailoverIfNeeded
函数中,发现已经有人在FailOver了(这是因为自己刚投完票),所以不会开启。
1 | 10474:X 22 Oct 2020 04:29:33.457 # Next failover delay: I will not start a failover before Thu Oct 22 04:35:33 2020 |
在PubSub中获得最新配置,处理函数sentinelProcessHelloMessage
1 | 10474:X 22 Oct 2020 04:29:34.291 # +config-update-from sentinel 2ddc47e7af3cebe8b1642127ed72002bb918550c 127.0.0.1 26379 @ mymaster 127.0.0.1 6379 |
1 | 10474:X 22 Oct 2020 04:29:34.291 # +switch-master mymaster 127.0.0.1 6379 127.0.0.1 6579 |
FailOver完毕的配置
观察最后的配置
1 | # Sentinel |
在当前版本下,还可以借助sentinel simfailure_flags
命令模拟Crash。