主节点客观下线判断
sentinelCheckObjectivelyDown 函数通过遍历主节点记录的 sentinels 哈希表,就可以获取其他哨兵实例对同一主节点主观下线的判断结果。
sentinelCheckObjectivelyDown 函数会使用 quorum 变量,来记录判断主节点为主观下线的哨兵数量。如果当前哨兵已经判断主节点为主观下线,那么它会先把 quorum 值置为 1。然后,它会依次判断其他哨兵的 flags 变量,检查是否设置了 SRI_MASTER_DOWN 的标记。如果设置了,它就会把 quorum 值加 1。
当遍历完 sentinels 哈希表后,sentinelCheckObjectivelyDown 函数会判断 quorum 值是否大于等于预设定的 quorum 阈值,这个阈值保存在了主节点的数据结构中,也就是 master->quorum,而这个阈值是在 sentinel.conf 配置文件中设置的。
如果实际的 quorum 值大于等于预设的 quorum 阈值,sentinelCheckObjectivelyDown 函数就判断主节点为客观下线,并设置变量 odown 为 1,而这个变量就是用来表示当前哨兵对主节点客观下线的判断结果的。
void sentinelCheckObjectivelyDown(sentinelRedisInstance *master) {
…
//当前主节点已经被当前哨兵判断为主观下线
if (master->flags & SRI_S_DOWN) {
quorum = 1; //当前哨兵将quorum值置为1
di = dictGetIterator(master->sentinels);
while((de = dictNext(di)) != NULL) { //遍历监听同一主节点的其他哨兵
sentinelRedisInstance *ri = dictGetVal(de);
if (ri->flags & SRI_MASTER_DOWN) quorum++;
}
dictReleaseIterator(di);
//如果quorum值大于预设的quorum阈值,那么设置odown为1。
if (quorum >= master->quorum) odown = 1;
}
一旦 sentinelCheckObjectivelyDown 函数判断主节点客观下线了,它就会调用 sentinelEvent 函数发送 +odown 事件消息,然后在主节点的 flags 变量中设置 SRI_O_DOWN 标记,如下所示:
//判断主节点为客观下线
if (odown) {
//如果没有设置SRI_O_DOWN标记
if ((master->flags & SRI_O_DOWN) == 0) {
sentinelEvent(LL_WARNING,"+odown",master,"%@ #quorum %d/%d",
quorum, master->quorum); //发送+odown事件消息
master->flags |= SRI_O_DOWN; //在主节点的flags中记录SRI_O_DOWN标记
master->o_down_since_time = mstime(); //记录判断客观下线的时间
}
}
其他哨兵的 SRI_MASTER_DOWN 标记是如何设置的呢?这就和 sentinelAskMasterStateToOtherSentinels 函数(在 sentinel.c 文件中)有关系了
sentinelAskMasterStateToOtherSentinels 函数
sentinelAskMasterStateToOtherSentinels 函数的主要目的,是向监听同一主节点的其他哨兵发送 is-master-down-by-addr 命令,进而询问其他哨兵对主节点的状态判断。
它会调用 redisAsyncCommand 函数(在async.c文件中),依次向其他哨兵发送 sentinel is-master-down-by-addr 命令,同时,它设置了收到该命令返回结果的处理函数为 sentinelReceiveIsMasterDownReply(在 sentinel.c 文件中),如下所示:
void sentinelAskMasterStateToOtherSentinels(sentinelRedisInstance *master, int flags) {
…
di = dictGetIterator(master->sentinels);
//遍历监听同一主节点的其他哨兵
while((de = dictNext(di)) != NULL) {
sentinelRedisInstance *ri = dictGetVal(de);
…
//发送sentinel is-master-down-by-addr命令
retval = redisAsyncCommand(ri->link->cc,
sentinelReceiveIsMasterDownReply, ri,
"%s is-master-down-by-addr %s %s %llu %s",
sentinelInstanceMapCommand(ri,"SENTINEL"),
master->addr->ip, port,
sentinel.current_epoch,
(master->failover_state > SENTINEL_FAILOVER_STATE_NONE) ?
sentinel.myid : "*");
}
…
}
sentinel is-master-down-by-addr 命令的处理
哨兵对于 sentinel 开头的命令,都是在 sentinelCommand 函数(在 sentinel.c 文件)中进行处理的。sentinelCommand 函数会根据 sentinel 命令后面跟的不同子命令,来执行不同的分支,而 is-master-down-by-addr 就是一条子命令。
在 is-master-down-by-addr 子命令对应的代码分支中,sentinelCommand 函数会根据命令中的主节点 IP 和端口号,来获取主节点对应的 sentinelRedisInstance 结构体。
紧接着,它会判断主节点的 flags 变量中是否有 SRI_S_DOWN 和 SRI_MASTER 标记,也就是说,sentinelCommand 函数会检查当前节点是否的确是主节点,以及哨兵是否已经将该节点标记为主观下线了。如果条件符合,那么它会设置 isdown 变量为 1,而这个变量表示的就是哨兵对主节点主观下线的判断结果。
然后,sentinelCommand 函数会把当前哨兵对主节点主观下线的判断结果,返回给发送 sentinel 命令的哨兵。它返回的结果主要包含三部分内容,分别是当前哨兵对主节点主观下线的判断结果、哨兵 Leader 的 ID,以及哨兵 Leader 所属的纪元。
void sentinelCommand(client *c) {
…
// is-master-down-by-addr子命令对应的分支
else if (!strcasecmp(c->argv[1]->ptr,"is-master-down-by-addr")) {
…
//当前哨兵判断主节点为主观下线
if (!sentinel.tilt && ri && (ri->flags & SRI_S_DOWN) && (ri->flags & SRI_MASTER))
isdown = 1;
…
addReplyMultiBulkLen(c,3); //哨兵返回的sentinel命令处理结果中包含三部分内容
addReply(c, isdown ? shared.cone : shared.czero); //如果哨兵判断主节点为主观下线,第一部分为1,否则为0
addReplyBulkCString(c, leader ? leader : "*"); //第二部分是Leader ID或者是*
addReplyLongLong(c, (long long)leader_epoch); //第三部分是Leader的纪元
…}
…}
其他哨兵的 SRI_MASTER_DOWN 标记是如何设置的呢?这实际上是和哨兵在 sentinelAskMasterStateToOtherSentinels 函数中,向其他哨兵发送 sentinel is-master-down-by-addr 命令时,设置的命令结果处理函数 sentinelReceiveIsMasterDownReply 有关。
sentinelReceiveIsMasterDownReply 函数
这个函数会进一步检查,“当前哨兵对主节点主观下线的判断结果”是否为 1。如果是的话,这就表明对应的哨兵已经判断主节点为主观下线了,那么当前哨兵就会把自己记录的对应哨兵的 flags,设置为 SRI_MASTER_DOWN。
//r是当前哨兵收到的其他哨兵的命令处理结果
//如果返回结果包含三部分内容,并且第一,二,三部分内容的类型分别是整数、字符串和整数
if (r->type == REDIS_REPLY_ARRAY && r->elements == 3 &&
r->element[0]->type == REDIS_REPLY_INTEGER &&
r->element[1]->type == REDIS_REPLY_STRING &&
r->element[2]->type == REDIS_REPLY_INTEGER)
{
ri->last_master_down_reply_time = mstime();
//如果返回结果第一部分的值为1,则在对应哨兵的flags中设置SRI_MASTER_DOWN标记
if (r->element[0]->integer == 1) {
ri->flags |= SRI_MASTER_DOWN;
}
一个哨兵调用 sentinelCheckObjectivelyDown 函数,是直接检查其他哨兵的 flags 是否有 SRI_MASTER_DOWN 标记,而哨兵又是通过 sentinelAskMasterStateToOtherSentinels 函数,向其他哨兵发送 sentinel is-master-down-by-addr 命令,从而询问其他哨兵对主节点主观下线的判断结果的,并且会根据命令回复结果,在结果处理函数 sentinelReceiveIsMasterDownReply 中,设置其他哨兵的 flags 为 SRI_MASTER_DOWN。
哨兵选举
sentinelHandleRedisInstance 会先调用 sentinelCheckObjectivelyDown 函数,再调用 sentinelStartFailoverIfNeeded 函数,判断是否要开始故障切换,如果 sentinelStartFailoverIfNeeded 函数的返回值为非 0 值,那么 sentinelAskMasterStateToOtherSentinels 函数会被调用。否则的话,sentinelHandleRedisInstance 就直接调用 sentinelFailoverStateMachine 函数,并再次调用 sentinelAskMasterStateToOtherSentinels 函数。
在这个调用关系中,sentinelStartFailoverIfNeeded 会判断是否要进行故障切换,它的判断条件有三个,分别是:
- 主节点的 flags 已经标记了 SRI_O_DOWN;
- 当前没有在执行故障切换;
- 如果已经开始故障切换,那么开始时间距离当前时间,需要超过 sentinel.conf 文件中的 sentinel failover-timeout 配置项的 2 倍。
sentinelStartFailoverIfNeeded 就会调用 sentinelStartFailover 函数,开始启动故障切换,而 sentinelStartFailover 会将主节点的 failover_state 设置为 SENTINEL_FAILOVER_STATE_WAIT_START,同时在主节点的 flags 设置 SRI_FAILOVER_IN_PROGRESS 标记,表示已经开始故障切换,如下所示:
void sentinelStartFailover(sentinelRedisInstance *master) {
…
master->failover_state = SENTINEL_FAILOVER_STATE_WAIT_START;
master->flags |= SRI_FAILOVER_IN_PROGRESS;
…
}
在实际切换前,sentinelAskMasterStateToOtherSentinels 函数会被调用。这个函数除了会用来向其他哨兵询问对主节点状态的判断,它还可以用来向其他哨兵发起 Leader 选举。
在 sentinel 命令处理函数中,如果检测到 sentinel 命令中的实例 ID 不为 * 号,那么就会调用 sentinelVoteLeader 函数来进行 Leader 选举。
//当前实例为主节点,并且sentinel命令的实例ID不等于*号
if (ri && ri->flags & SRI_MASTER && strcasecmp(c->argv[5]->ptr,"*")) {
//调用sentinelVoteLeader进行哨兵Leader选举
leader = sentinelVoteLeader(ri,(uint64_t)req_epoch, c->argv[5]->ptr,
&leader_epoch);
}
sentinelVoteLeader 函数
sentinelVoteLeader 函数会实际执行投票逻辑。
sentinelVoteLeader 函数让哨兵 B 投票的条件是:master 记录的 Leader 的纪元小于哨兵 A 的纪元,同时,哨兵 A 的纪元要大于或等于哨兵 B 的纪元。这两个条件保证了哨兵 B 还没有投过票,否则的话,sentinelVoteLeader 函数就直接返回当前 master 中记录的 Leader ID 了,这也是哨兵 B 之前投过票后记录下来的。
if (req_epoch > sentinel.current_epoch) {
sentinel.current_epoch = req_epoch;
…
sentinelEvent(LL_WARNING,"+new-epoch",master,"%llu",
(unsigned long long) sentinel.current_epoch);
}
if (master->leader_epoch < req_epoch && sentinel.current_epoch <= req_epoch)
{
sdsfree(master->leader);
master->leader = sdsnew(req_runid);
master->leader_epoch = sentinel.current_epoch;
…
}
return master->leader ? sdsnew(master->leader) : NULL;
此文章为10月Day24学习笔记,内容来源于极客时间《Redis 源码剖析与实战》