故障转移
监控到有Redis主服务器宕机, Sentinel就开始进行故障转移。故障转移的目的是把有问题的主服务器摘掉,然后选择一台从服务器提升为主服务器。因为在主从架构中,主服务器负责处理所有的写操作,所以快速完成故障转移可以减少故障带来的损失。故障转移在sentinelFailoverStateMachine()函数中实现,其代码如下:
代码语言:javascript复制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:
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;
}
}
从代码可以看出,sentinelFailoverStateMachine()是由一个状态机组成的。我们看到状态机有5种状态,分别为:
1. SENTINEL_FAILOVER_STATE_WAIT_START
2. SENTINEL_FAILOVER_STATE_SELECT_SLAVE
3. SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE
4. SENTINEL_FAILOVER_STATE_WAIT_PROMOTION
5. SENTINEL_FAILOVER_STATE_RECONF_SLAVES
这5种状态分别对应不同的处理,如SENTINEL_FAILOVER_STATE_WAIT_START状态由sentinelFailoverWaitStart()函数处理,下面我们分析一下故障转移的处理过程。
WAIT_START状态处理
Sentinel集群中由多台Sentinel服务器组成,但故障转移处理只需要一台Sentinel就可以完成,所以在故障转移处理之前必须选举一台Leader来完成。WAIT_START状态由sentinelFailoverWaitStart()函数处理,代码如下:
代码语言:javascript复制void sentinelFailoverWaitStart(sentinelRedisInstance *ri) {
char *leader;
int isleader;
/* Check if we are the leader for the failover epoch. */
leader = sentinelGetLeader(ri, ri->failover_epoch);
isleader = leader && strcasecmp(leader,sentinel.myid) == 0;
sdsfree(leader);
/* If I'm not the leader, and it is not a forced failover via
* SENTINEL FAILOVER, then I can't continue with the failover. */
if (!isleader && !(ri->flags & SRI_FORCE_FAILOVER)) {
int election_timeout = SENTINEL_ELECTION_TIMEOUT;
/* The election timeout is the MIN between SENTINEL_ELECTION_TIMEOUT
* and the configured failover timeout. */
if (election_timeout > ri->failover_timeout)
election_timeout = ri->failover_timeout;
/* Abort the failover if I'm not the leader after some time. */
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();
ri->failover_state = SENTINEL_FAILOVER_STATE_SELECT_SLAVE;
ri->failover_state_change_time = mstime();
sentinelEvent(LL_WARNING," failover-state-select-slave",ri,"%@");
}
代码首先判断当前Sentinel是否为Leader,如果不是,故障转移会被延时到下一个定时器触发时再次检测自己是否是Leader,如果超过一定的时间,那么就调用sentinelAbortFailover()函数停止故障转移操作。如果当前Sentinel是Leader,那么就会进入下一个状态“SELECT_SLAVE”。
前面说过,选举Leader是通过Raft协议来完成的,关于Raft协议前面也作了简单的介绍,所以这里说说Sentinel是怎么完成选举操作。
在Sentinel检测到Redis主服务器客观下线时会发送“is-master-down-by-addr”请求给所有监控此Redis服务器的Sentinel,此请求的作用是向其他Sentinel询问投票结果。如果还没有投票,那么就会投给发送“is-master-down-by-addr”请求的Sentinel,请求在sentinelAskMasterStateToOtherSentinels()函数中发送,代码如下:
代码语言:javascript复制void sentinelAskMasterStateToOtherSentinels(sentinelRedisInstance *master, int flags) {
...
di = dictGetIterator(master->sentinels);
while((de = dictNext(di)) != NULL) {
...
ll2string(port,sizeof(port),master->addr->port);
retval = redisAsyncCommand(ri->link->cc,
sentinelReceiveIsMasterDownReply, ri,
"SENTINEL is-master-down-by-addr %s %s %llu %s",
master->addr->ip, port,
sentinel.current_epoch,
(master->failover_state > SENTINEL_FAILOVER_STATE_NONE) ?
sentinel.myid : "*");
if (retval == C_OK) ri->link->pending_commands ;
}
dictReleaseIterator(di);
}
这个投票过程跟Raft协议是一致的,当其他Sentinel接收到此请求后会调用sentinelVoteLeader()函数进行投票。sentinelVoteLeader()代码如下:
char *sentinelVoteLeader(sentinelRedisInstance *master, uint64_t req_epoch, char *req_runid, uint64_t *leader_epoch) {
if (req_epoch > sentinel.current_epoch) {
sentinel.current_epoch = req_epoch;
sentinelFlushConfig();
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;
sentinelFlushConfig();
sentinelEvent(LL_WARNING," vote-for-leader",master,"%s %llu",
master->leader, (unsigned long long) master->leader_epoch);
/* If we did not voted for ourselves, set the master failover start
* time to now, in order to force a delay before we can start a
* failover for the same master. */
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;
}
如果发送请求的Sentinel的Epoch(对应Raft协议的Term)比较新,那么就会投票给此Sentinel。通过一轮的投票之后,Sentinel接着会统计投票结果,如果当前的Sentinel获得一半以上的票数,那么此Sentinel便会成为Leader,统计投票结果在sentinelGetLeader()函数中实现。代码如下:
代码语言:javascript复制char *sentinelGetLeader(sentinelRedisInstance *master, uint64_t epoch) {
...
di = dictGetIterator(master->sentinels);
while((de = dictNext(di)) != NULL) {
sentinelRedisInstance *ri = dictGetVal(de);
if (ri->leader != NULL && ri->leader_epoch == sentinel.current_epoch)
sentinelLeaderIncr(counters,ri->leader);
}
dictReleaseIterator(di);
di = dictGetIterator(counters);
while((de = dictNext(di)) != NULL) {
uint64_t votes = dictGetUnsignedIntegerVal(de);
if (votes > max_votes) {
max_votes = votes;
winner = dictGetKey(de); // 选择一个票数最高的sentinel
}
}
dictReleaseIterator(di);
if (winner)
myvote = sentinelVoteLeader(master,epoch,winner,&leader_epoch);
else
myvote = sentinelVoteLeader(master,epoch,sentinel.myid,&leader_epoch);
if (myvote && leader_epoch == epoch) {
uint64_t votes = sentinelLeaderIncr(counters,myvote);
if (votes > max_votes) {
max_votes = votes;
winner = myvote;
}
}
voters_quorum = voters/2 1;
if (winner && (max_votes < voters_quorum || max_votes < master->quorum))
winner = NULL;
winner = winner ? sdsnew(winner) : NULL;
...
return winner;
}
虽然sentinelGetLeader()的代码有点长,但逻辑比较简单,就是统计监控Redis主服务器的所有Sentinel的投票结果,如果某个Sentinel获取的票数超过一半以上,那么将会成为Leader。因为故障转移是由Leader完成的,所以如果当前Sentinel被选中为Leader,那么便会进入下一个状态。
SELECT_SLAVE状态处理
SELECT_SLAVE状态通过sentinelFailoverSelectSlave()函数处理,主要完成的工作是从隶属于宕机的主服务器的所有从服务器中选择一台来作为新的主服务器。
选择哪一台作为新主机呢?Sentinel会通过以下原则来选择:
1. 不能有下面三个标记中的一个:SRI_S_DOWN|SRI_O_DOWN|SRI_DISCONNECTED。
2. ping心跳不能超时。
3. 优先级不能为 0(slave->slave_priority)。
4. 回复INFO 数据不能超时。
5. 主从连接断线时间不能超过一段时间。
通过上面的条件筛选后可能会得到很多候选服务器,此时还需要按下面的条件排序:
1. 优先选择优先级高的从机。
2. 优先选择主从复制偏移量高的从机,即从机从主机复制的数据越多。
3. 优先选择有 runid 的从机。
4. 如果上面条件都一样,那么将 runid 按字典顺序排序。
通过上面的筛选过程后最终会得到一台最合适的从服务器来成为新的主服务器,接着Sentinel会进入下一个状态“SLAVEOF_NOONE”。
SLAVEOF_NOONE状态处理
这个状态由sentinelFailoverSendSlaveOfNoOne()函数处理,会向新主服务器发送slaveof noone命令,Redis从服务器接收到此命令后会升级为主服务器。关于Redis从服务器升级为主服务器的过程,这里就不作详细的介绍了,有兴趣可以参考Redis的源码实现。
发送完slaveof noone命令后,Sentinel便会进入下一个状态“WAIT_PROMOTION”。
WAIT_PROMOTION状态处理
WAIT_PROMOTION状态处理非常简单,就是等待上一个状态的执行结果。判断上一个状态处理是否完毕,是通过向候选主服务器发送info命令来获取信息,如果上一个状态执行完毕,那么就会进入下一个状态“RECONF_SLAVES”。
RECONF_SLAVES状态处理
这是故障转移状态机的最后一个状态,这个状态的主要处理向其他从服务器发送slaveof命令,即通知它们让候选主服务器成为它们的新主机。至此,故障转移操作完毕。
总结
本文主要介绍了Sentinel的使用和实现原理,但由于Sentinel的实现有点复杂(4000多行代码),所以很多细节都没有涉及,如果想更深入的了解Sentinel的实现原理,可以通过阅读Redis源码从中找到答案。