哨兵 Leader 选举和 Raft 协议
Raft 协议可以用来实现分布式共识,这是一种在分布式系统中实现多节点达成一致性的算法,可以用来在多个节点中选举出 Leader 节点。为了实现这一目标,Raft 协议把节点设计成了三种类型,分别是 Leader、Follower 和 Candidate。
Raft 协议对于 Leader 节点和 Follower 节点之间的交互有两种规定:
- 正常情况下,在一个稳定的系统中,只有 Leader 和 Follower 两种节点,并且 Leader 会向 Follower 发送心跳消息。
- 异常情况下,如果 Follower 节点在一段时间内没有收到来自 Leader 节点的心跳消息,那么,这个 Follower 节点就会转变为 Candidate 节点,并且开始竞选 Leader。
当一个 Candidate 节点开始竞选 Leader 时,它会执行如下操作:
- 给自己投一票;
- 向其他节点发送投票请求,并等待其他节点的回复;
- 启动一个计时器,用来判断竞选过程是否超时。
在这个 Candidate 节点等待其他节点返回投票结果的过程中,如果它收到了 Leader 节点的心跳消息,这就表明,此时已经有 Leader 节点被选举出来了。那么,这个 Candidate 节点就会转换为 Follower 节点,而它自己发起的这轮竞选 Leader 投票过程就结束了。
如果这个 Candidate 节点,收到了超过半数的其他 Follower 节点返回的投票确认消息,也就是说,有超过半数的 Follower 节点都同意这个 Candidate 节点作为 Leader 节点,那么这个 Candidate 节点就会转换为 Leader 节点,从而可以执行 Leader 节点需要运行的流程逻辑。
哨兵的时间事件处理函数 sentinelTimer
哨兵的时间事件处理函数 sentinelTimer(在sentinel.c文件中),因为哨兵 Leader 选举是在这个函数执行过程中触发的。
sentinelTimer 函数本身是在 serverCron 函数(在 server.c 文件中)中调用的,如下所示:
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
…
if (server.sentinel_mode) sentinelTimer(); //如果当前运行的是哨兵,则运行哨兵的时间事件处理函数
…
}
sentinelTimer 会调用 sentinelHandleDictOfRedisInstances 函数。这个函数的原型如下,它的参数是一个哈希表:
void sentinelHandleDictOfRedisInstances(dict *instances)
传入的哈希表参数,就是当前哨兵实例状态信息 sentinelState 结构中维护的 master 哈希表,其中记录了当前哨兵监听的主节点,如下所示:
void sentinelTimer(void) {
...
//将当前哨兵监听的主节点作为参数传入sentinelHandleDictOfRedisInstances函数
sentinelHandleDictOfRedisInstances(sentinel.masters);
...
}
sentinelHandleDictOfRedisInstances 函数会执行一个循环流程,在该流程中,它会从 sentinel.master 哈希表中逐一取出监听的主节点,并调用 sentinelHandleRedisInstance 函数对该主节点进行处理,如下所示:
void sentinelHandleDictOfRedisInstances(dict *instances) {
...
di = dictGetIterator(instances); //获取哈希表的迭代器
while((de = dictNext(di)) != NULL) {
//从哈希表中取出一个实例
sentinelRedisInstance *ri = dictGetVal(de);
//调用sentinelHandleRedisInstance处理实例
sentinelHandleRedisInstance(ri);
...
}
...
}
sentinelHandleRedisInstance 函数的执行流程
sentinelHandleRedisInstance 函数会被周期性执行,用来检测哨兵监听的节点的状态。这个函数主要会依次执行以下四个步骤。
- 第一步:重建连接
sentinelHandleRedisInstance 会调用 sentinelReconnectInstance 函数,尝试和断连的实例重新建立连接。
- 第二步:发送命令
sentinelHandleRedisInstance 会调用 sentinelSendPeriodicCommands 函数,向实例发送 PING、INFO 等命令。
- 第三步:判断主观下线
sentinelHandleRedisInstance 会调用 sentinelCheckSubjectivelyDown 函数,检查监听的实例是否主观下线。
- 第四步:判断客观下线和执行故障切换
在这一步中,sentinelHandleRedisInstance 函数的运行逻辑主要是针对被监听的主节点来执行的,而这一步又可以分成以下四个小步骤:
- 首先,针对监听的主节点,调用 sentinelCheckObjectivelyDown 函数检查其是否客观下线。
- 紧接着,调用 sentinelStartFailoverIfNeeded 函数判断是否要启动故障切换。如果要启动故障切换,就调用 sentinelAskMasterStateToOtherSentinels 函数,获取其他哨兵实例对主节点状态的判断,并向其他哨兵发送 is-master-down-by-addr 命令,发起 Leader 选举。
- 然后,调用 sentinelFailoverStateMachine 执行故障切换。
- 最后,再次调用 sentinelAskMasterStateToOtherSentinels 函数,获取其他哨兵实例对主节点状态的判断。
哨兵在 sentinelHandleDictOfRedisInstances 函数中,调用 sentinelHandleRedisInstance 处理完每个主节点后,还会针对监听主节点的其他哨兵实例,以及主节点的从节点,分别调用 sentinelHandleDictOfRedisInstances 函数进行处理,如下所示:
//如果当前是主节点,那么调用sentinelHandleDictOfRedisInstances分别处理该主节点的从节点,以及监听该主节点的其他哨兵
if (ri->flags & SRI_MASTER) {
sentinelHandleDictOfRedisInstances(ri->slaves);
sentinelHandleDictOfRedisInstances(ri->sentinels);
...
}
sentinelTimer 除了调用 sentinelHandleDictOfRedisInstances 以外,它一开始还会调用 sentinelCheckTiltCondition 函数检查是否需要进入 TILT 模式。这里,你需要注意下,对于哨兵来说,TILT 模式是一种特殊的运行模式,当哨兵连续两次的时间事件处理间隔时长为负值,或是间隔时长过长,那么哨兵就会进入 TILT 模式。在该模式下,哨兵只会定期发送命令收集信息,而不会执行故障切换流程。
sentinelReconnectInstance 函数
sentinelReconnectInstance 函数的主要作用是判断哨兵实例和主节点间连接是否正常,如果发生了断连情况,它会重新建立哨兵和主节点的连接。
sentinelRedisInstance结构中有一个 instanceLink 类型的成员变量 link,该变量就记录了哨兵和主节点间的两个连接:
typedef struct instanceLink {
...
redisAsyncContext *cc; //用于发送命令的连接
redisAsyncContext *pc; //用于发送pub-sub消息的连接
...
}
sentinelReconnectInstance 函数执行时会检查这两个连接是否为 NULL。如果是的话,那么它就会调用 redisAsyncConnectBind 函数(在async.c文件中),重新和主节点建立这两个连接。
在完成了和主节点的连接重建后,哨兵会继续调用 sentinelSendPeriodicCommands 函数。
sentinelSendPeriodicCommands 函数
它先是调用 redisAsyncCommand 函数(在 async.c 文件中),通过哨兵和主节点间的命令连接 cc,向主节点发送 INFO 命令。然后,再通过 sentinelSendPing 函数(在 sentinel.c 文件中)向主节点发送 PING 命令(PING 命令的发送也是通过哨兵和主节点的命令连接 cc 来完成的)。
最后,sentinelSendPeriodicCommands 函数会调用 sentinelSendHello 函数(在 sentinel.c 文件中),通过哨兵和主节点的命令连接 cc,向主节点发送 PUBLISH 命令,将哨兵自身的 IP、端口号和 ID 号信息发送给主节点。
接下来,哨兵就会调用 sentinelCheckSubjectivelyDown 函数,来判断监听的主节点是否主观下线。
sentinelCheckSubjectivelyDown 函数
sentinelCheckSubjectivelyDown 函数首先会计算当前距离上次哨兵发送 PING 命令的时长 elapsed,如下所示:
void sentinelCheckSubjectivelyDown(sentinelRedisInstance *ri) {
...
if (ri->link->act_ping_time) //计算当前距离上一次发送PING命令的时长
elapsed = mstime() - ri->link->act_ping_time;
else if (ri->link->disconnected) //如果哨兵和主节点的连接断开了,那么计算当前距离连接最后可用的时长
elapsed = mstime() - ri->link->last_avail_time;
...
}
计算完 elapsed 之后,sentinelCheckSubjectivelyDown 函数会分别检测哨兵和主节点的命令发送连接,以及 Pub/Sub 连接的活跃程度。如果活跃度不够,那么哨兵会调用 instanceLinkCloseConnection 函数(在 sentinel.c 文件中),断开当前连接,以便重新连接。
紧接着,sentinelCheckSubjectivelyDown 函数会根据以下两个条件,判断主节点是否为主观下线。
- 条件一:当前距离上次发送 PING 的时长已经超过 down_after_period 阈值,还没有收到回复。down_after_period 的值是由 sentinel.conf 配置文件中,down-after-milliseconds 配置项决定的,其默认值是 30s。
- 条件二:哨兵认为当前实例是主节点,但是这个节点向哨兵报告它将成为从节点,并且在 down_after_period 时长,再加上两个 INFO 命令间隔后,该节点还是没有转换成功。
当上面这两个条件有一个满足时,哨兵就判定主节点为主观下线了。然后,哨兵就会调用 sentinelEvent 函数发送“+sdown”事件信息。下面的代码展示了这部分的判断逻辑:
if (elapsed > ri->down_after_period ||
(ri->flags & SRI_MASTER && ri->role_reported == SRI_SLAVE
&& mstime() - ri->role_reported_time > (ri->down_after_period+SENTINEL_INFO_PERIOD*2)))
{
//判断主节点为主观下线
if ((ri->flags & SRI_S_DOWN) == 0) {
sentinelEvent(LL_WARNING,"+sdown",ri,"%@");
ri->s_down_since_time = mstime();
ri->flags |= SRI_S_DOWN;
}
}
此文章为10月Day23学习笔记,内容来源于极客时间《Redis 源码剖析与实战》