【Redis】客观下线

在sentinelHandleRedisInstance函数中,如果是主节点,需要做如下处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) { 
// 省略...

// 如果是主节点
if (ri->flags & SRI_MASTER) {
// 检查是否主观下线
sentinelCheckObjectivelyDown(ri);
// 是否需要开始故障切换
if (sentinelStartFailoverIfNeeded(ri))
// 获取其他哨兵实例对节点状态的判断
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
// 故障切换状态机
sentinelFailoverStateMachine(ri);
// 获取其他哨兵实例对节点状态的判断
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
}
}

节点的状态定义

1
2
3
4
5
6
7
8
#define SRI_MASTER  (1<<0) /* master主节点 */
#define SRI_SLAVE (1<<1) /* slave从节点 */
#define SRI_SENTINEL (1<<2) /* 哨兵节点 */
#define SRI_S_DOWN (1<<3) /* 主观下线 */
#define SRI_O_DOWN (1<<4) /* 客观下线 */
#define SRI_MASTER_DOWN (1<<5) /* 节点下线 */
#define SRI_FAILOVER_IN_PROGRESS (1<<6) /* 正在执行master节点的故障切换
换 */

客观下线

sentinelCheckObjectivelyDown

sentinelCheckObjectivelyDown函数用于判断master节点是否客观下线:

  1. 首先确认主节点是否已经被哨兵标记为主观下线,如果已经主观下线,quorum的值置为1,表示当前已经有1个哨兵认为master主观下线,进行第2步
  2. master->sentinels存储了监控当前master的其他哨兵节点,遍历其他哨兵节点,通过flag标识中是否有SRI_MASTER_DOWN状态判断其他哨兵对MASTER节点下线的判断,如果有则认为MASTER下线,对quorum数量加1
  3. 遍历结束后判断quorum的数量是否大于master->quorum设置的数量,也就是是否有过半的哨兵认为主节点下线,如果是将odown置为1,认为主节点客观下线
  4. 根据odown的值判断主节点是否客观下线
    • 如果客观下线,确认master->flags是否有SRI_O_DOWN状态,如果没有,发布+odown客观下线事件并将master->flags置为SRI_O_DOWN状态
    • 如果没有客观下线,校验master->flags是否有SRI_O_DOWN状态,如果有,需要发布-odown事件,取消master的客观下线标记(master->flags的SRI_O_DOWN状态取消)

可以看到,master节点客观下线需要根据其他哨兵实例对主节点的判断来共同决定,具体是通过其他哨兵实例的flag中是否有SRI_MASTER_DOWN状态来判断的,如果认为master下线的哨兵个数超过了master节点中的quorum设置,master节点将被认定为客观下线,发布+odown客观下线事件,关于SRI_MASTER_DOWN状态是在哪里设置的在后面会讲到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void sentinelCheckObjectivelyDown(sentinelRedisInstance *master) {
dictIterator *di;
dictEntry *de;
unsigned int quorum = 0, odown = 0;
// 判断主节点是否被标记为主观下线SRI_S_DOWN
if (master->flags & SRI_S_DOWN) {
/* quorum初始化为1 */
quorum = 1;
/* 遍历监听主节点的其他哨兵实例. */
di = dictGetIterator(master->sentinels);
while((de = dictNext(di)) != NULL) {
// 获取哨兵实例
sentinelRedisInstance *ri = dictGetVal(de);
// 判断其他哨兵实例是否把master节点标记为下线
if (ri->flags & SRI_MASTER_DOWN) quorum++;
}
dictReleaseIterator(di);
// 如果大于master->quorum,也就是过半的哨兵认为主节点已经下线,标记客观下线
if (quorum >= master->quorum) odown = 1;
}

// 如果客观下线
if (odown) {
if ((master->flags & SRI_O_DOWN) == 0) {
// 发布+odown客观下线事件
sentinelEvent(LL_WARNING,"+odown",master,"%@ #quorum %d/%d",
quorum, master->quorum);
// 将MASTER节点标记为SRI_O_DOWN
master->flags |= SRI_O_DOWN;
// 标记客观下线的时间
master->o_down_since_time = mstime();
}
} else {
// 非客观下线,判断master是否有客观下线标识
if (master->flags & SRI_O_DOWN) {
// 发布-odown事件
sentinelEvent(LL_WARNING,"-odown",master,"%@");
// 取消master的客观下线标识
master->flags &= ~SRI_O_DOWN;
}
}
}

是否需要执行故障切换

sentinelStartFailoverIfNeeded

sentinelStartFailoverIfNeeded用于判断是否需要执行故障切换,可以开始故障切换的条件有三个:

  1. master节点被认为客观下线(SRI_O_DOWN)
  2. 当前没有在进行故障切换(状态不是SRI_FAILOVER_IN_PROGRESS)
  3. 距离上次执行故障切换的时间,超过了故障切换超时时间设置的2倍,意味着上一次执行故障切换的时间已超时,可以重新进行故障切换

同时满足以上三个条件,达到执行故障切换的标准,调用sentinelStartFailover函数,将故障切换的状态改为待执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int sentinelStartFailoverIfNeeded(sentinelRedisInstance *master) {
/* 如果MASTER不是客观下线,直接返回 */
if (!(master->flags & SRI_O_DOWN)) return 0;

/* 如果当前已经在执行故障切换直接返回 */
if (master->flags & SRI_FAILOVER_IN_PROGRESS) return 0;

/* 判断距离上次执行故障切换的时间,如果小于failover_timeout配置项的2倍,表示上次故障切换还未达到超时设置,所以本次不能执行*/
if (mstime() - master->failover_start_time <
master->failover_timeout*2)
{
if (master->failover_delay_logged != master->failover_start_time) {
time_t clock = (master->failover_start_time +
master->failover_timeout*2) / 1000;
char ctimebuf[26];

ctime_r(&clock,ctimebuf);
ctimebuf[24] = '\0'; /* Remove newline. */
master->failover_delay_logged = master->failover_start_time;
serverLog(LL_WARNING,
"Next failover delay: I will not start a failover before %s",
ctimebuf);
}
return 0;
}
// 开始故障切换(只更改了故障切换状态)
sentinelStartFailover(master);
return 1;
}

sentinelStartFailover

可以看到sentinelStartFailover函数并没有直接进行故障切换,而是更改了一些状态

  1. 将failover_state置为了SENTINEL_FAILOVER_STATE_WAIT_START等待开始执行状态
  2. 将master节点的flags设置为SRI_FAILOVER_IN_PROGRESS故障切换执行中状态
  3. 将master的failover_epoch设置为当前哨兵的投票轮次current_epoch + 1 ,在选举leader时会用到
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void sentinelStartFailover(sentinelRedisInstance *master) {
serverAssert(master->flags & SRI_MASTER);
// 将状态更改为等待开始执行故障切换
master->failover_state = SENTINEL_FAILOVER_STATE_WAIT_START;
// 设置为故障切换执行中状态
master->flags |= SRI_FAILOVER_IN_PROGRESS;
// 设置failover_epoch故障切换轮次
master->failover_epoch = ++sentinel.current_epoch;
sentinelEvent(LL_WARNING,"+new-epoch",master,"%llu",
(unsigned long long) sentinel.current_epoch);
// 发布事件
sentinelEvent(LL_WARNING,"+try-failover",master,"%@");
master->failover_start_time = mstime()+rand()%SENTINEL_MAX_DESYNC;
master->failover_state_change_time = mstime();
}

获取哨兵实例对主节点状态判断

在sentinelHandleRedisInstance函数中,可以看到sentinelStartFailoverIfNeeded条件成立时以及函数的最后都调用了sentinelAskMasterStateToOtherSentinels,接下来就去看看sentinelAskMasterStateToOtherSentinels里面都做了什么:

1
2
3
4
5
6
7
8
9
if (ri->flags & SRI_MASTER) {
sentinelCheckObjectivelyDown(ri);
if (sentinelStartFailoverIfNeeded(ri))
// 获取其他哨兵实例对主节点的状态判断,这里传入的参数是SENTINEL_ASK_FORCED
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
sentinelFailoverStateMachine(ri);
// 获取其他哨兵实例对主节点的状态判断,这里传入的参数是SENTINEL_NO_FLAGS
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
}

is-master-down-by-addr命令发送

sentinelAskMasterStateToOtherSentinels

sentinelAskMasterStateToOtherSentinels函数用于向其他哨兵实例发送is-master-down-by-addr命令获取其他哨兵实例对主节点状态的判断,它会遍历监听同一主节点的其他哨兵实例进行处理:

  1. 获取每一个哨兵实例
  2. 计算距离每个哨兵实例上一次收到IS-MASTER-DOWN-BY-ADDR命令回复时间的间隔
  3. 如果距离上次收到回复的时间已经超过了SENTINEL_ASK_PERIOD周期的5倍,清空哨兵节点flag中的SRI_MASTER_DOWN状态和leader
  4. 如果master节点已经是下线状态SRI_S_DOWN,不需要进行处理,回到第一步处理下一个哨兵
  5. 如果哨兵节点用于发送命令的link连接处于未连接状态,不处理,回到第一步处理下一个哨兵
  6. 如果不是强制发送命令(入参的flag是SENTINEL_ASK_FORCED时),并且距离上次收到回复命令的时间还在SENTINEL_ASK_PERIOD周期内,不处理,回到第一步处理下一个哨兵
  7. 通过redisAsyncCommand函数发送发送is-master-down-by-addr命令,sentinelReceiveIsMasterDownReply为处理函数,redisAsyncCommand函数有如下参数:
    • 用于发送请求的连接:ri->link->cc
    • 收到命令返回结果时对应的处理函数:sentinelReceiveIsMasterDownReply
    • master节点的ip:announceSentinelAddr(master->addr)
    • master节点端口:port
    • 当前哨兵的投票轮次:sentinel.current_epoch
    • 实例ID:master->failover_state > SENTINEL_FAILOVER_STATE_NONE时表示要执行故障切换,此时传入当前哨兵的myid,否则传入*
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
void sentinelAskMasterStateToOtherSentinels(sentinelRedisInstance *master, int flags) {
dictIterator *di;
dictEntry *de;
// 遍历监听主节点的其他哨兵实例
di = dictGetIterator(master->sentinels);
while((de = dictNext(di)) != NULL) {
// 获取每一个哨兵实例
sentinelRedisInstance *ri = dictGetVal(de);
// 计算距离上一次收到IS-MASTER-DOWN-BY-ADDR命令回复时间的间隔
mstime_t elapsed = mstime() - ri->last_master_down_reply_time;
char port[32];
int retval;

/* 如果距离上次收到回复的时间已经超过了SENTINEL_ASK_PERIOD周期的5倍,清空相关设置 */
if (elapsed > SENTINEL_ASK_PERIOD*5) {
// 取消SRI_MASTER_DOWN状态
ri->flags &= ~SRI_MASTER_DOWN;
sdsfree(ri->leader);
// leader置为null
ri->leader = NULL;
}

// 如果master已经是下线状态
if ((master->flags & SRI_S_DOWN) == 0) continue;
// 如果已经连接中断
if (ri->link->disconnected) continue;
// 如果不是强制发送命令状态SENTINEL_ASK_FORCED,并且距离上次收到回复命令的时间还在SENTINEL_ASK_PERIOD周期内,不处理,回到第一步处理下一个哨兵
if (!(flags & SENTINEL_ASK_FORCED) &&
mstime() - ri->last_master_down_reply_time < SENTINEL_ASK_PERIOD)
continue;

ll2string(port,sizeof(port),master->addr->port);
// 发送is-master-down-by-addr命令,sentinelReceiveIsMasterDownReply为处理函数
retval = redisAsyncCommand(ri->link->cc,
sentinelReceiveIsMasterDownReply, ri,
"%s is-master-down-by-addr %s %s %llu %s",
sentinelInstanceMapCommand(ri,"SENTINEL"),
announceSentinelAddr(master->addr), port,
sentinel.current_epoch,
(master->failover_state > SENTINEL_FAILOVER_STATE_NONE) ?
sentinel.myid : "*");
if (retval == C_OK) ri->link->pending_commands++;
}
dictReleaseIterator(di);
}

is-master-down-by-addr命令处理

sentinelCommand

其他哨兵实例收到is-master-down-by-addr命令之后的处理逻辑在sentinelCommand函数中可以找到:

  1. 根据请求传入的ip和端口信息获取主节点的sentinelRedisInstance实例对象(在发送is-master-down-by-addr命令的redisAsyncCommand函数中传入了主节点的ip和端口)
  2. 如果不是TILT模式,校验sentinelRedisInstance对象是否是主节点并且主节点被标记为主观下线,如果条件都成立表示主节点已经主观下线,将isdown置为1
  3. 判断请求参数中的runid是否不为*,如果不为*表示当前需要进行leader选举,调用sentinelVoteLeader选举哨兵Leader
  4. 发送is-master-down-by-addr命令的回复,将对主节点主观下线的判断、选出的leader节点的runid、投票轮次leader_epoch返回给发送命令哨兵
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
void sentinelCommand(client *c) {
if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"help")) {

}
// 省略其他else if...
// 如果是is-master-down-by-addr命令
else if (!strcasecmp(c->argv[1]->ptr,"is-master-down-by-addr")) {
/* SENTINEL IS-MASTER-DOWN-BY-ADDR <ip> <port> <current-epoch> <runid>
*
* 参数说明:
* ip和端口:哨兵检测的主节点的ip和端口
* current-epoch:是故障切换中当前投票的轮次,每一个哨兵在一轮投票中只能投一次
* runid:如果需要执行故障切换,传入的是哨兵的myid,否则传入的是 *
*/
sentinelRedisInstance *ri;
long long req_epoch;
uint64_t leader_epoch = 0; // 默认为0
char *leader = NULL;
long port;
int isdown = 0;

if (c->argc != 6) goto numargserr;
if (getLongFromObjectOrReply(c,c->argv[3],&port,NULL) != C_OK ||
getLongLongFromObjectOrReply(c,c->argv[4],&req_epoch,NULL)
!= C_OK)
return;
// 根据请求传入的ip和端口信息获取对应的哨兵实例,也就是监控的master节点
ri = getSentinelRedisInstanceByAddrAndRunID(sentinel.masters,
c->argv[2]->ptr,port,NULL);

/* 如果不是TILT模式,校验是否是主节点并且主节点被标记为主观下线 */
if (!sentinel.tilt && ri && (ri->flags & SRI_S_DOWN) &&
(ri->flags & SRI_MASTER))
isdown = 1;// 确定主观下线

/* 如果是主节点并且传入的runid不为*,调用sentinelVoteLeader选举Leader执行故障切换 */
if (ri && ri->flags & SRI_MASTER && strcasecmp(c->argv[5]->ptr,"*")) {
// 选举leader
leader = sentinelVoteLeader(ri,(uint64_t)req_epoch,
c->argv[5]->ptr,
&leader_epoch);
}

/* 发送回复包含三部分:
* 下线状态, 选出的leader, leader的投票轮次leader_epoch */
addReplyArrayLen(c,3);
// 下线状态
addReply(c, isdown ? shared.cone : shared.czero);
// leader不为空传入leader否则传入*
addReplyBulkCString(c, leader ? leader : "*");
// 投票轮次
addReplyLongLong(c, (long long)leader_epoch);
if (leader) sdsfree(leader);
}
// 省略其他else if...
else {
addReplySubcommandSyntaxError(c);
}
return;
}

is-master-down-by-addr回复处理

sentinelReceiveIsMasterDownReply

在sentinelCommand中对命令处理之后发送了返回数据,数据里面包含主观下线的判断、leader的runid以及投票轮次leader_epoch,对返回数据的处理在sentinelReceiveIsMasterDownReply函数中:

  1. 如果回复者也标记了节点主观下线,将哨兵实例的flags状态置为SRI_MASTER_DOWN下线状态,SRI_MASTER_DOWN状态就是在这里设置的
  2. 如果返回的leader runid不是*,意味着哨兵实例对leader进行了投票,需要更新哨兵实例中的leader和leader_epoch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 这里的privdata指向回复is-master-down-by-addr命令的那个哨兵实例
void sentinelReceiveIsMasterDownReply(redisAsyncContext *c, void *reply, void *privdata) {
// 每个哨兵实例监控的master节点中,保存了其他监控该主节点的哨兵实例,这里的privdata就指向master节点存储的其他哨兵实例中回复了is-master-down-by-addr命令的那个哨兵实例
sentinelRedisInstance *ri = privdata;
instanceLink *link = c->data;
redisReply *r;

if (!reply || !link) return;
link->pending_commands--;
r = reply;
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();
// 如果回复主观下线
if (r->element[0]->integer == 1) {
// 将回复命令的哨兵节点的flags状态改为SRI_MASTER_DOWN
ri->flags |= SRI_MASTER_DOWN;
} else {
ri->flags &= ~SRI_MASTER_DOWN;
}
// 如果runid不是*
if (strcmp(r->element[1]->str,"*")) {
sdsfree(ri->leader);
if ((long long)ri->leader_epoch != r->element[2]->integer)
serverLog(LL_WARNING,
"%s voted for %s %llu", ri->name,
r->element[1]->str,
(unsigned long long) r->element[2]->integer);
// 更新回复命令的哨兵存储的leader
ri->leader = sdsnew(r->element[1]->str);
// 更新投票轮次
ri->leader_epoch = r->element[2]->integer;
}
}
}

故障切换状态机

在sentinelHandleRedisInstance函数中,判断是否需要执行故障切换之后,就会调用sentinelFailoverStateMachine函数进入故障切换状态机,根据failover_state故障切换状态调用不同的方法,我们先关注以下两种状态:

  1. SENTINEL_FAILOVER_STATE_WAIT_START:等待执行状态,表示需要执行故障切换但还未开始,在sentinelStartFailoverIfNeeded函数中可以看到如果需要执行故障切换,会调用sentinelStartFailover函数将状态置为SENTINEL_FAILOVER_STATE_WAIT_START,对应的处理函数为sentinelFailoverWaitStart,sentinelFailoverWaitStart中会判断是当前哨兵节点是否是执行故障切换的leader,如果是将状态改为SENTINEL_FAILOVER_STATE_SELECT_SLAVE。

  2. SENTINEL_FAILOVER_STATE_SELECT_SLAVE:从SLAVE节点中选举Master节点的状态,处于这个状态意味着需要从Master的从节点中选举出可以替代Master节点的从节点,进行故障切换,对应的处理函数为sentinelFailoverSelectSlave

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void sentinelFailoverStateMachine(sentinelRedisInstance *ri) {
serverAssert(ri->flags & SRI_MASTER);
// 如果已经在故障切换执行中,直接返回
if (!(ri->flags & SRI_FAILOVER_IN_PROGRESS)) return;
// 状态机
switch(ri->failover_state) {
case SENTINEL_FAILOVER_STATE_WAIT_START:// 等待执行
sentinelFailoverWaitStart(ri);
break;
case SENTINEL_FAILOVER_STATE_SELECT_SLAVE: // 选举master节点
sentinelFailoverSelectSlave(ri);
break;
case SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE:
sentinelFailoverSendSlaveOfNoOne(ri);
break;
case SENTINEL_FAILOVER_STATE_WAIT_PROMOTION:
sentinelFailoverWaitPromotion(ri);
break;
case SENTINEL_FAILOVER_STATE_RECONF_SLAVES:
sentinelFailoverReconfNextSlave(ri);
break;
}
}

sentinelFailoverWaitStart

sentinelFailoverWaitStart函数的处理逻辑如下:

  1. 调用sentinelGetLeader获取执行故障切换的leader
  2. 对比当前哨兵是与获取到执行故障切换leader的myid是否一致,判断当前哨兵是否是执行故障切换的leader
  3. 如果当前哨兵不是故障切换leader, 并且不是强制执行状态SRI_FORCE_FAILOVER,当前哨兵不能执行故障切换
  4. 如果当前哨兵是故障切换的leader节点,将故障切换状态改为SENTINEL_FAILOVER_STATE_SELECT_SLAVE状态,在下一次执行故障切换状态机时会从slave节点选出master节点进行故障切换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void sentinelFailoverWaitStart(sentinelRedisInstance *ri) {
char *leader;
int isleader;

/* 获取执行故障切换的leader */
leader = sentinelGetLeader(ri, ri->failover_epoch);
// leader不为空并且与当前哨兵的myid一致
isleader = leader && strcasecmp(leader,sentinel.myid) == 0;
sdsfree(leader);

/* 如果当前哨兵不是leader, 并且不是强制执行状态SRI_FORCE_FAILOVER,当前哨兵不能执行故障切换 */
if (!isleader && !(ri->flags & SRI_FORCE_FAILOVER)) {
int election_timeout = SENTINEL_ELECTION_TIMEOUT;
if (election_timeout > ri->failover_timeout)
election_timeout = ri->failover_timeout;
/* 在超时时终止故障切换 */
if (mstime() - ri->failover_start_time > election_timeout) {
sentinelEvent(LL_WARNING,"-failover-abort-not-elected",ri,"%@");
sentinelAbortFailover(ri);
}
return;
}
sentinelEvent(LL_WARNING,"+elected-leader",ri,"%@");
if (sentinel.simfailure_flags & SENTINEL_SIMFAILURE_CRASH_AFTER_ELECTION)
sentinelSimFailureCrash();
// 更改为SENTINEL_FAILOVER_STATE_SELECT_SLAVE状态,在下一次执行故障切换状态机时会从slave节点选出master节点
ri->failover_state = SENTINEL_FAILOVER_STATE_SELECT_SLAVE;
ri->failover_state_change_time = mstime();
sentinelEvent(LL_WARNING,"+failover-state-select-slave",ri,"%@");
}

Leader选举

sentinelGetLeader

sentinelGetLeader函数用于从指定的投票轮次epoch中获取Leader节点,成为一个Leader节点需要获取大多数的投票,处理逻辑如下:

  1. 创建了一个counters字典,里面记录了每个哨兵得到的投票数,其中key为哨兵实例的id
    • counters的数据来源:遍历master->sentinels获取其他哨兵实例,判断哨兵实例记录的leader是否为空并且投票轮次与当前指定的epoch一致,如果一致加入counters中并将投票数增加一票
  2. 从counters中获取投票数最多的哨兵实例记为winner,最大投票数记为max_votes
  3. 判断winner是否为空
    • 如果不为空,在master节点中记录的leader节点和winner节点中,选出纪元(投票轮次)最新的节点记为myvote
    • 如果为空,在master节点中记录的leader节点和当前哨兵实例中,选出纪元(投票轮次)最新的节点记为myvote
  4. 经过上一步之后,如果myvote不为空并且leader_epoch与调用sentinelGetLeader函数时指定的epoch一致,当前哨兵给myvote增加一票,然后判断myvote得到的投票数是否大于max_votes,如果是将winner获胜者更新为myvote
  5. 到这里,winner中记录了本轮投票的获胜者,也就是得到票数最多的那个,max_votes记录了获得投票数,能否成为leader还需满足以下两个条件,,保证选举出的leader得到了过半哨兵的投票
    • 条件一:得到的投票数max_votes大于voters_quorum,voters_quorum为哨兵实例个数的一半+1,也就是需要有过半的哨兵实例
    • 条件二:得到的投票数max_votes大于master->quorum,这个值是在 sentinel.conf 配置文件中设置的,一般设置为哨兵总数的一半+1
  6. 选举结束,返回winner中记录的实例id作为leader
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
char *sentinelGetLeader(sentinelRedisInstance *master, uint64_t epoch) {
dict *counters;
dictIterator *di;
dictEntry *de;
unsigned int voters = 0, voters_quorum;
char *myvote;
char *winner = NULL;
uint64_t leader_epoch;
uint64_t max_votes = 0;

serverAssert(master->flags & (SRI_O_DOWN|SRI_FAILOVER_IN_PROGRESS));
// 创建字典
counters = dictCreate(&leaderVotesDictType,NULL);
// 获取所有的哨兵实例个数包含当前哨兵
voters = dictSize(master->sentinels)+1;

/* 遍历哨兵实例 */
di = dictGetIterator(master->sentinels);
while((de = dictNext(di)) != NULL) {
// 获取每一个哨兵节点
sentinelRedisInstance *ri = dictGetVal(de);
// 如果某个哨兵实例的leader不为空并且leader的轮次等于指定的轮次
if (ri->leader != NULL && ri->leader_epoch == sentinel.current_epoch)
sentinelLeaderIncr(counters,ri->leader); // ri->leader的投票数加1
}
dictReleaseIterator(di);
di = dictGetIterator(counters);
while((de = dictNext(di)) != NULL) {
// 获取投票数
uint64_t votes = dictGetUnsignedIntegerVal(de);
// 获取投票数最多的节点的runid,记录在winner中
if (votes > max_votes) {
max_votes = votes;
winner = dictGetKey(de);
}
}
dictReleaseIterator(di);

/* 如果winner不为空,在master节点和winner节点中获取epoch投票轮次最新的当做当前节点的投票者*/
/* 如果winner为空,在master节点和当前哨兵节点中选取epoch投票轮次最新的节点*/
if (winner)
myvote = sentinelVoteLeader(master,epoch,winner,&leader_epoch);
else
myvote = sentinelVoteLeader(master,epoch,sentinel.myid,&leader_epoch);
//
if (myvote && leader_epoch == epoch) {
// 为myvote增加一票
uint64_t votes = sentinelLeaderIncr(counters,myvote);
// 如果超过了max_votes
if (votes > max_votes) {
max_votes = votes; // 更新max_votes
winner = myvote; // 更新winner
}
}
// voters_quorum,为哨兵实例个数的一半+1
voters_quorum = voters/2+1;
// 如果投票数量max_votes小于quorum,说明未达到过半的投票数
if (winner && (max_votes < voters_quorum || max_votes < master->quorum))
winner = NULL;

winner = winner ? sdsnew(winner) : NULL;
sdsfree(myvote);
dictRelease(counters);
// 返回获胜的节点,也就是leader节点
return winner;
}

投票

sentinelVoteLeader

sentinelVoteLeader函数入参中可以看到传入了master节点,和候选节点的IDreq_runid以及候选节点的投票纪元(轮次)req_epoch。master节点中记录了当前哨兵节点选举的执行故障切换的leader节点,在master->leader中保存。sentinelVoteLeader就用于在这两个节点中选出获胜的那个节点:

  1. 如果候选节点的req_epoch大于当前sentinel实例的epoch,将当前哨兵实例的current_epoch置为请求轮次req_epoch
  2. 如果master节点记录的leader_epoch小于候选节点的req_epoch,并且当前实例的轮次小于等于候选节点轮次,将master节点中的leader改为候选节点
  3. 返回master节点中记录的leader
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
char *sentinelVoteLeader(sentinelRedisInstance *master, uint64_t req_epoch, char *req_runid, uint64_t *leader_epoch) {
// 如果请求的轮次req_epoch大于当前sentinel实例的轮次
if (req_epoch > sentinel.current_epoch) {
// 将哨兵实例的current_epoch置为请求轮次
sentinel.current_epoch = req_epoch;
sentinelFlushConfig();
sentinelEvent(LL_WARNING,"+new-epoch",master,"%llu",
(unsigned long long) sentinel.current_epoch);
}
// 如果master节点记录的轮次leader_epoch小于请求轮次,并且当前实例的轮次小于等于请求轮次
if (master->leader_epoch < req_epoch && sentinel.current_epoch <= req_epoch)
{
sdsfree(master->leader);
// 将leader置为传入的runid
master->leader = sdsnew(req_runid);
// 更改master的leader_epoch
master->leader_epoch = sentinel.current_epoch;
sentinelFlushConfig();
sentinelEvent(LL_WARNING,"+vote-for-leader",master,"%s %llu",
master->leader, (unsigned long long) master->leader_epoch);
if (strcasecmp(master->leader,sentinel.myid))
master->failover_start_time = mstime()+rand()%SENTINEL_MAX_DESYNC;
}

*leader_epoch = master->leader_epoch;
return master->leader ? sdsnew(master->leader) : NULL;
}

总结

参考

极客时间 - Redis源码剖析与实战(蒋德钧)

Redis版本:redis-6.2.5