一、概述
这篇文章主要用来讲述redis高可用的实现原理,学习redis高可用实现,可以让我们更好的进行系统设计。也可以学习redis中的设计,将其迁移到其他系统的实现中去。
二、整体架构
- 监控:Sentinel节点会定期检测Redis数据节点(主从)、其余Sentinel节点是否可达。
- 主节点故障转移:主节点不可用,将从节点晋升为主节点并维护后续正确的主从关系。
- 通知:Sentinel节点会将故障转移的结果通知给应用方。
- 配置提供者:在Redis Sentinel架构中,客户端在初始化的时候连接的是Sentinel节点集合,从中获取主节点信息
三、实现原理
节点通信(互相通信,实现功能)
1.每10s,sentinel会发送info给所有data节点。
- info命令获取从节点信息
- 新节点加入可立刻感知,并加入集群 2.每2s,向_sentinel_:hello频道发送主节点判断以及当前sentinel节点的信息。
- 发现其他sentinel节点信息
- 交换主节点状态。做一些逻辑处理(比如发现其他节点的master比自己新,则进行故障转移)。
<Sentinel节点IP> <Sentinel节点端口> <Sentinel节点runId> <Sentinel节点配置版本> <主节点名字> <主节点Ip> <主节点端口> <主节点配置版本>
3.每1s,向其他节点发送ping(alive check)
- 判断节点是否存活 通过1和2,在sentinel启动的时候只需要配置要监控的master,即可实现对其他slave和sentinel的自动发现机制。主要依赖对redis data节点的发布订阅和info操作去做这个事情。
主观下线
主观上判断某个节点下线(在超时时间没有ping通过)
客观下线
如果主观下线的是主节点,则会通过sentinel is- master-down-by-addr命令向其他sentinel节点询问对主节点的判断。超高quorum数量,会进行客观下线。
is-master-down-by-addr命令
所有sentinel节点除了定时pub hello消息,还通过这个命令进行p2p通信。及时告知其他sentinel信息。 该命令作用:
- 向其他sentinel寻问对主节点的判断
- 领主选举投票
sentinel is-master-down-by-addr <ip> <port> <current_epoch> <runid>
如果runid为*代表主节点下线命令
如果不为*代表请求投票命令
主节点下线命令
sentinel is-master-down-by-addr 127.0.0.1 6379 0 *
领导选举投票 使用raft的领主选举算法,通过该命令投票选举
命令返回结果
- down_state
- leader_runid *代表用来做主节点不可达,有具体的runid代表同意runid成为领导者。
- leader_epoch 领导者任期
故障转移
主节点故障,选举从节点 1.选哪个?
(1)过滤不健康的
(2)选slave-priority最高的从节点列表
(3)选择复制偏移量最大的。
(4)选择runid最小的
2.执行slaveof no one
3.向剩余从节点发送命令,让其成为新master的从
4.将原来的主更新为从。并保持对其关注
四、源码分析
sentienl的存储结构。
图中每个粉色的方框都代表一个SentinelRedisInstance,这是sentienl中统一的一个数据结构,简单来讲就是一块内存,里面存储了节点的信息。每一个redis data节点或者sentinel节点都会有一个SentinelRedisInstance(除了当前sentienl)。就好比每一条网络连接都有一个socket一样。
master代表监控的master信息。每个sentinel有多个master(上面只画了一个),代表可以同时监控多个master节点。
SentinelRedisInstance结构有两个hash:key为名字,val为SentinelRedisInstance。
一个用来存储当前master的slave,另一个用来存储同样监控这个master的其他sentinel节点(如上图结构),使用hash 是为了通过key进行快速查找对应结构。
// 其他同样监控这个主服务器的所有 sentinel
dict *sentinels; /* Other sentinels monitoring the same master. */
// 如果这个实例代表的是一个主服务器
// 那么这个字典保存着主服务器属下的从服务器
// 字典的键是从服务器的名字,字典的值是从服务器对应的 sentinelRedisInstance 结构
dict *slaves; /* Slaves for this master instance. */
当然只有SentinelRedisInstance为master的时候,这两个hash才会被用到。所以sentinel的所有操作都围绕这个master结构进行。sentinel通过info命令以及publish hello消息发现其他的slave和sentinel,新加入的节点会加入对应hash中。
flags
唯一需要强调的是SentinelRedisInstance中的flags变量。该变量记录了当前节点的状态。(flags通过标志位记录多种状态,每个状态一个位,通过|~进行更新)
- 如果是master,则记录master的状态。
- 如果是sentinel,则记录当前sentienl一些判断状态。
所以sentinel完全围绕这些SentinelRedisInstance去维护整个集群的逻辑。比如ping,info,就是遍历SentinelRedisInstance,对其hostport进行ping,info等操作。
遍历逻辑:先拿到master的SentinelRedisInstance,执行逻辑,然后再遍历两个hash处理。
服务启动
sentinel 模式启动会执行下面逻辑
if (server.sentinel_mode) {
initSentinelConfig();
initSentinel();
}
initSentinel主要就是一些初始化工作,包括sentinel结构创建、一些全局变量的初始化、以及注册命令处理器。
sentinelHandleConfiguration
服务启动会先加载配置。这个方法主要是sentinel配置的解析逻辑。
if (!strcasecmp(argv[0],"monitor") && argc == 5) {
/* monitor <name> <host> <port> <quorum> */
// 读入 quorum 参数
int quorum = atoi(argv[4]);
// 检查 quorum 参数必须大于 0
if (quorum <= 0) return "Quorum must be 1 or greater.";
// 创建主服务器实例
if (createSentinelRedisInstance(argv[1],SRI_MASTER,argv[2],
atoi(argv[3]),quorum,NULL) == NULL)
{
switch(errno) {
case EBUSY: return "Duplicated master name.";
case ENOENT: return "Can't resolve master instance hostname.";
case EINVAL: return "Invalid port number";
}
}
}else if (!strcasecmp(argv[0],"down-after-milliseconds") && argc == 3) {
/* down-after-milliseconds <name> <milliseconds> */
// 查找主服务器
ri = sentinelGetMasterByName(argv[1]);
if (!ri) return "No such master with specified name.";
// 设置选项
ri->down_after_period = atoi(argv[2]);
if (ri->down_after_period <= 0)
return "negative or zero time parameter.";
sentinelPropagateDownAfterPeriod(ri);
}
核心就是对master的SentinelRedisInstance结构创建。然后将其他配置初始化到该结构中。上面只列举了部分代码。其他配置类似down-after-milliseconds的解析逻辑。
createSentinelRedisInstance
用来创建sentinel中实例。包括sentinel、被监控的redis的主从。每次有新增都会调用该方法创建一个实例,并加入到master的SentinelRedisInstance中(除了master自身)
- SRI_MASTER 创建一个被监控的master实例。并将其加入到sentinel.masters这个hash表中
- SRI_SLAVE 创建一个被监控的slave实例。并将其加入到master->slaves这个hash表中
- SRI_SENTINEL 创建一个被监控的sentinel实例。并将其加入到master->sentinels这个hash表中
上面基本就是一些初始化工作。sentinel核心逻辑是需要时间函数驱动的,所以我们直接看sentinelTimer逻辑。
sentinelTimer
该函数100ms执行一次。
void sentinelTimer(void) {
// 记录本次 sentinel 调用的事件,判断是否需要进入 TITL 模式
sentinelCheckTiltCondition();
// 执行定期操作
// 比如 PING 实例、分析主服务器和从服务器的 INFO 命令
// 向其他监视相同主服务器的 sentinel 发送问候信息
// 并接收其他 sentinel 发来的问候信息
// 执行故障转移操作,等等
sentinelHandleDictOfRedisInstances(sentinel.masters);
// 运行等待执行的脚本
sentinelRunPendingScripts();
// 清理已执行完毕的脚本,并重试出错的脚本
sentinelCollectTerminatedScripts();
// 杀死运行超时的脚本
sentinelKillTimedoutScripts();
/* We continuously change the frequency of the Redis "timer interrupt"
* in order to desynchronize every Sentinel from every other.
* This non-determinism avoids that Sentinels started at the same time
* exactly continue to stay synchronized asking to be voted at the
* same time again and again (resulting in nobody likely winning the
* election because of split brain voting). */
server.hz = REDIS_DEFAULT_HZ + rand() % REDIS_DEFAULT_HZ;
}
sentinelCheckTiltCondition方法用来判断是否进入ttl,并且记录执行时间。
TITL模式:因为sentinel依赖本机时间驱动,如果系统时间出问题,或者因为进程阻塞导致的时间函数延迟调用。这时再去参与集群逻辑会出现不正确的决策。因此如果当前时间和上一次执行时间差为负值或者超过2s,该节点会进入TILT模式。
sentinelHandleDictOfRedisInstances
void sentinelHandleDictOfRedisInstances(dict *instances) {
dictIterator *di;
dictEntry *de;
sentinelRedisInstance *switch_to_promoted = NULL;
/* There are a number of things we need to perform against every master. */
// 遍历多个实例,这些实例可以是多个主服务器、多个从服务器或者多个 sentinel
di = dictGetIterator(instances);
while((de = dictNext(di)) != NULL) {
sentinelRedisInstance *ri = dictGetVal(de);
// 执行调度操作
sentinelHandleRedisInstance(ri);
// 如果被遍历的是主服务器,那么递归地遍历该主服务器的所有从服务器
if (ri->flags & SRI_MASTER) {
// 所有从服务器
sentinelHandleDictOfRedisInstances(ri->slaves);
// 所有 sentinel
sentinelHandleDictOfRedisInstances(ri->sentinels);
if (ri->failover_state == SENTINEL_FAILOVER_STATE_UPDATE_CONFIG) {
// 已选出新的主服务器
switch_to_promoted = ri;
}
}
}
// 将原主服务器(已下线)从主服务器表格中移除,并使用新主服务器代替它
if (switch_to_promoted)
sentinelFailoverSwitchToPromotedSlave(switch_to_promoted);
dictReleaseIterator(di);
}
这个方法就递归遍历所有SentinelRedisInstance。。核心就是对被 Sentinel 监视的所有实例(包括主服务器、从服务器和其他 Sentinel ) 进行定期操作。
逻辑很简单,其实就是执行sentinelHandleRedisInstance方法。
如果有故障转移(从节点升级为主节点),则调用sentinelFailoverSwitchToPromotedSlave替换新主服务。
sentinelHandleRedisInstance方法
Sentinel的主逻辑流程,后面会一一介绍每个方法的逻辑。该方法定期调用,非阻塞(中间的io命令都会异步出去)。
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {
/* Every kind of instance */
// 如果有需要的话,创建连向实例的网络连接
sentinelReconnectInstance(ri);
// 根据情况,向实例发送 PING、 INFO 或者 PUBLISH 命令
sentinelSendPeriodicCommands(ri);
/* ============== ACTING HALF ============= */
/* We don't proceed with the acting half if we are in TILT mode.
* TILT happens when we find something odd with the time, like a
* sudden change in the clock. */
if (sentinel.tilt) {
// 如果 TILI 模式未解除,那么不执行动作
if (mstime()-sentinel.tilt_start_time < SENTINEL_TILT_PERIOD) return;
// 时间已过,退出 TILT 模式
sentinel.tilt = 0;
sentinelEvent(REDIS_WARNING,"-tilt",NULL,"#tilt mode exited");
}
/* Every kind of instance */
// 检查给定实例是否进入 SDOWN 状态
sentinelCheckSubjectivelyDown(ri);
/* Masters and slaves */
if (ri->flags & (SRI_MASTER|SRI_SLAVE)) {
/* Nothing so far. */
}
/* Only masters */
/* 对主服务器进行处理 */
if (ri->flags & SRI_MASTER) {
// 判断 master 是否进入 ODOWN 状态
sentinelCheckObjectivelyDown(ri);
// 如果主服务器进入了 ODOWN 状态,那么开始一次故障转移操作
if (sentinelStartFailoverIfNeeded(ri))
// 强制向其他 Sentinel 发送 SENTINEL is-master-down-by-addr 命令
// 刷新其他 Sentinel 关于主服务器的状态
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
// 执行故障转移
sentinelFailoverStateMachine(ri);
// 如果有需要的话,向其他 Sentinel 发送 SENTINEL is-master-down-by-addr 命令
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
}
}
1.sentinelReconnectInstance主要就是创建和实例的连接。如果是master或者slave实例,则订阅对应节点的__sentinel__:hello频道,用来接受其他sentinel广播的消息。每个sentinel会定期给该频道publish。 因为启动的时候只是初始化了实例数据,并没有创建连接。对于每一个实例,都是在这里去创建连接的。
2.sentinelSendPeriodicCommands主要发送ping、info、publish命令。
3.如果当前在tilt模式中,直接返回,非tilt模式才会执行后续操作。
4.sentinelCheckSubjectivelyDown,检查给定实例是否进入 SDOWN 状态
5.如果当前实例为主服务器,则执行一些故障判断和故障转移操作。
可以看出来sentienl的所有逻辑都是围绕一个主服务进行的。
sentinelSendPeriodicCommands方法
这是我们的核心关注点,sentinel之间的通信逻辑。
这个方法主要是ping、info以及publish hello消息。到达执行时刻会执行对应逻辑。
- ping每s执行一次,如果比设置的down_after_period(超时下线时间)小,则为down_after_period。
- info每10s执行一次。如果一次未执行则直接执行。如果主节点SDOWN或SRI_FAILOVER_IN_PROGRESS(故障转移)则每s执行一次,为了更快的捕捉服务器变动。
- publish每2s执行一次。(用于发现其他sentinel以及和其他sentinel通信)
redis通过pending_commands记录当前正在异步执行的命令。如果超过100,则不再发送,避免命令堆积。因为sentinel是依赖上一次响应时间来判断是否发送命令,如果出现网络阻塞或者波动,会导致频繁发送。
Ping命令
发送逻辑在sentinelSendPeriodicCommands方法中。
Ping命令回调方法sentinelPingReplyCallback:,如果回调正常,更新对应字段(last_avail_time、last_pong_time),sentienl依赖这些字段进行判活。如果节点执行lua超时,则调用SCRIPT KILL尝试杀死脚本。
info命令
info为获取redis节点信息的命令。处理返回结果的方法为sentinelRefreshInstanceInfo
sentinelRefreshInstanceInfo方法主要是解析并提取需要的数据信息。下面介绍一些核心信息。
1.如果是主节点,提取其从节点的hostport,并为从节点创建实例信息(如果是新发现的),sentinel依赖此方式发现其他的slave节点。
if (sentinelRedisInstanceLookupSlave(ri,ip,atoi(port)) == NULL) {
if ((slave = createSentinelRedisInstance(NULL,SRI_SLAVE,ip,
atoi(port), ri->quorum, ri)) != NULL)
{
sentinelEvent(REDIS_NOTICE,"+slave",slave,"%@");
}
}
2.如果是从节点,记录当前从服务器对应主节点的信息。因为sentinel的记录的主节点不一定是正确的(网络分区导致切换延迟),所以通过info获取到该从服务器最新信息。以供后面逻辑处理。
这里其实两种主(从)节点状态:一种是sentienl认为当前节点为主(从)节点,另一种是当前节点认为的主(从)节点。
3.如果发生了角色转变(info返回的和当前sentinel记录的节点状态不一致),更新转变时间。如果tilt,直接返回。否则,根据该节点返回的role和sentinel记录的role进行一些逻辑,具体逻辑我们后面再研究。
Publish Hello消息
给对应channel发送的信息如下,主要包括对应sentinel的信息和当前master的信息
snprintf(payload,sizeof(payload),
"%s,%d,%s,%llu," /* Info about this sentinel. */
"%s,%s,%d,%llu", /* Info about current master. */
ip, server.port, server.runid,
(unsigned long long) sentinel.current_epoch,
/* --- */
master->name,master_addr->ip,master_addr->port,
(unsigned long long) master->config_epoch);
订阅处理hello消息 (sentinelProcessHelloMessage)
该方法为sentinel处理hello信息的方法。
1.如果hello消息中sentinel节点为新发现的节点(当前setinel不存在的),为新节点创建实例,加入到列表中。这是sentinel发现其他sentinel的唯一方式。
2.更新current_epoch=max(当前current_epoch,hello的current_epoch)
3.如果hello消息的master_config_epoch比该节点master的config_epoch大。则调用sentinelResetMasterAndChangeAddress方法切换当前master。config_epoch为故障转移使用的纪元。故障转移之后会递增。如果发现比较大的,说明进行了故障转移,则信任hello中的master为最新master
if (master->config_epoch < master_config_epoch) {
master->config_epoch = master_config_epoch;
if (master_port != master->addr->port ||
strcmp(master->addr->ip, token[5]))
{
sentinelAddr *old_addr;
sentinelEvent(REDIS_WARNING,"+config-update-from",si,"%@");
sentinelEvent(REDIS_WARNING,"+switch-master",
master,"%s %s %d %s %d",
master->name,
master->addr->ip, master->addr->port,
token[5], master_port);
old_addr = dupSentinelAddr(master->addr);
sentinelResetMasterAndChangeAddress(master, token[5], master_port);
sentinelCallClientReconfScript(master,
SENTINEL_OBSERVER,"start",
old_addr,master->addr);
releaseSentinelAddr(old_addr);
}
}
sentinelCheckSubjectivelyDown方法
检查实例是否进入 SDOWN 状态
1.如果实例符合断线重连的条件,则断开该实例连接,等待下次重新连接。其实就是对不活跃实例进行断线重连。
2.设置或者取消SDOWN标志
达到下面两个条件设置为SDOWN,否则取消SDOWN标志位
- 超过超时时间没有回复命令,则设置为SDOWN
- Sentinel认为实例是主服务器,这个服务器向 Sentinel 报告它将成为从服务器,但在超过给定时限之后,服务器仍然没有完成这一角色转换
主节点单独逻辑
if (ri->flags & SRI_MASTER) {
// 判断 master 是否进入 ODOWN 状态
sentinelCheckObjectivelyDown(ri);
// 如果主服务器进入了 ODOWN 状态,那么开始一次故障转移操作
if (sentinelStartFailoverIfNeeded(ri))
// 强制向其他 Sentinel 发送 SENTINEL is-master-down-by-addr 命令
// 刷新其他 Sentinel 关于主服务器的状态
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
// 执行故障转移
sentinelFailoverStateMachine(ri);
// 如果有需要的话,向其他 Sentinel 发送 SENTINEL is-master-down-by-addr 命令
// 刷新其他 Sentinel 关于主服务器的状态
// 这一句是对那些没有进入 if(sentinelStartFailoverIfNeeded(ri)) { /* ... */ }
// 语句的主服务器使用的
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
}
sentinelCheckObjectivelyDown
判断当前主节点是否进入ODown状态。
通过遍历所有sentinel实例的flags标志位进行判断。如果一半以上主观下线,则变更为客观下线。这个状态位是在is-master-down-by-addr命令回调中更新的。
sentinelStartFailoverIfNeeded
判断是否需要进行故障转移
void sentinelStartFailover(sentinelRedisInstance *master) {
redisAssert(master->flags & SRI_MASTER);
// 更新故障转移状态
master->failover_state = SENTINEL_FAILOVER_STATE_WAIT_START;
// 更新主服务器状态
master->flags |= SRI_FAILOVER_IN_PROGRESS;
// 更新纪元
master->failover_epoch = ++sentinel.current_epoch;
sentinelEvent(REDIS_WARNING,"+new-epoch",master,"%llu",
(unsigned long long) sentinel.current_epoch);
sentinelEvent(REDIS_WARNING,"+try-failover",master,"%@");
// 记录故障转移状态的变更时间
master->failover_start_time = mstime()+rand()%SENTINEL_MAX_DESYNC;
master->failover_state_change_time = mstime();
}
判断条件故障转移:
1.进入ODOWN并且没有在故障转移中。
2.如果发现故障转移过于频繁也不执行。
如果需要故障转移,则更新当前master的信息,主要是failover_state、failover_epoch等字段。
failover_epoch为当前master纪元+1。
failover_epoch作用:
- 其他sentinel依赖这个字段判断是否需要进行故障转移。这个在之前的hello中有说到。
- 当前sentinel依赖这个纪元选出执行故障转移的leader。因为选举使用的也是该纪元。选举出来的leader的纪元应该一致。每次选举都会产生一个新的leader,最新的纪元最权威,这是raft领导选举的核心概念。虽然raft使用的是term。
failover_state代表当前故障转移状态。故障转移操作需要依赖该状态。
故障转移操作需要从sentienl中选举一个执行。所以这只是先更新状态。
sentinelAskMasterStateToOtherSentinels
向其他sentine询问master状态。
这会遍历所有sentinel,如果当前sentinel认为master下线,并且连接正常,会发送is-master-down-by-addr命令
sentinel is-master-down-by-addr <ip> <port> <current_epoch> <runid>
如果本sentinel检测到master 主观下线(通过failover_state判断),则runid为当前server的runid,代表让其他sentienl给自己投票。
如果是master客观下线,则runid=*,代表告诉其他sentienl,主节点下线。这是sentinel通知其他sentinel主节点下线的唯一方式。
is-master-down-by-addr命令处理
该逻辑在sentinelCommand中执行
ri = getSentinelRedisInstanceByAddrAndRunID(sentinel.masters,
c->argv[2]->ptr,port,NULL);
if (!sentinel.tilt && ri && (ri->flags & SRI_S_DOWN) &&
(ri->flags & SRI_MASTER))
isdown = 1;
/* Vote for the master (or fetch the previous vote) if the request
* includes a runid, otherwise the sender is not seeking for a vote. */
if (ri && ri->flags & SRI_MASTER && strcasecmp(c->argv[5]->ptr,"*")) {
leader = sentinelVoteLeader(ri,(uint64_t)req_epoch,
c->argv[5]->ptr,
&leader_epoch);
}
/* Reply with a three-elements multi-bulk reply:
* down state, leader, vote epoch. */
// 多条回复
// 1) <down_state> 1 代表下线, 0 代表未下线
// 2) <leader_runid> Sentinel 选举作为领头 Sentinel 的运行 ID
// 3) <leader_epoch> 领头 Sentinel 目前的配置纪元
addReplyMultiBulkLen(c,3);
addReply(c, isdown ? shared.cone : shared.czero);
addReplyBulkCString(c, leader ? leader : "*");
addReplyLongLong(c, (long long)leader_epoch);
如果节点下线回复down_state1,否则为0
根据ip和port获取ri(sentinelRedisInstance),如果ri是主节点,并且runid不为*,则进行选举投票。
投票逻辑:
请求的req_epoch>当前sentinel的current_epoch(更新sentinel的current_epoch)
req_epoch>master的master.leader_epoch,并且>=sentinel.current_epoch,更新master的leader为req_runid。然后投票给当前req_runid节点。
其实就是判断epoch。如果请求的epoch比较大,那就投票即可。和raft的领导选举一样。
is-master-down-by-addr命令回调(sentinelReceiveIsMasterDownReply)
// 更新最后一次回复询问的时间
ri->last_master_down_reply_time = mstime();
// 设置 SENTINEL 认为主服务器的状态
if (r->element[0]->integer == 1) {
// 已下线
ri->flags |= SRI_MASTER_DOWN;
} else {
// 未下线
ri->flags &= ~SRI_MASTER_DOWN;
}
// 如果运行 ID 不是 "*" 的话,那么这是一个带投票的回复
if (strcmp(r->element[1]->str,"*")) {
/* If the runid in the reply is not "*" the Sentinel actually
* replied with a vote. */
sdsfree(ri->leader);
// 打印日志
if (ri->leader_epoch != r->element[2]->integer)
redisLog(REDIS_WARNING,
"%s voted for %s %llu", ri->name,
r->element[1]->str,
(unsigned long long) r->element[2]->integer);
// 设置实例的领头
ri->leader = sdsnew(r->element[1]->str);
ri->leader_epoch = r->element[2]->integer;
}
- <down_state> 1 代表下线, 0 代表未下线
- <leader_runid> Sentinel 选举作为领头 Sentinel 的运行 ID
- <leader_epoch> 领头 Sentinel 目前的配置纪元 1.更新回复时间
2.更新被询问的sentinel的flag。用于ODOWN判断。
3.如果leader_runid非*,代表投票信息,更新该sentinel的leader信息。
ri的leader字段有两个状态:
- 如果ri一个主服务器实例,那么 leader 将是负责进行故障转移的 Sentinel 的运行 ID 。
- 如果ri一个 Sentinel 实例,那么 leader 就是被选举出来的领头 Sentinel 。
执行故障转移(sentinelFailoverStateMachine)
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;
// 等待升级生效,如果升级超时,那么重新选择新主服务器
// 具体情况请看 sentinelRefreshInstanceInfo 函数
case SENTINEL_FAILOVER_STATE_WAIT_PROMOTION:
sentinelFailoverWaitPromotion(ri);
break;
// 向从服务器发送 SLAVEOF 命令,让它们同步新主服务器
case SENTINEL_FAILOVER_STATE_RECONF_SLAVES:
sentinelFailoverReconfNextSlave(ri);
break;
}
整个流程如上:
1.sentinelFailoverWaitStart会根据当前所有sentinel的leader以及当前故障转移纪元选出来leader。如果发现leader是自己,则切换failover_state为SENTINEL_FAILOVER_STATE_SELECT_SLAVE,执行下一个case。否则跳出。
2.根据逻辑选举从服务器作为新的主节点,如果没有选出,清空故障转移状态。选出成功后继续更新状态执行下一个case。选择逻辑上面有说过。
3.升级新节点为主节点,如果发现断线并超时,则终止故障转移逻辑。这个其实就是异步发送slave of命令。让从节点升级为主节点。这个命令在redis复制源码解析中说过。
4.SENTINEL_FAILOVER_STATE_WAIT_PROMOTION状态只是等待升级的另一个逻辑,如果升级超时则终止故障转移。 如何检测从节点升级主节点成功?
其实在info中有一段逻辑:
if ((ri->master->flags & SRI_FAILOVER_IN_PROGRESS) &&
(ri->master->failover_state ==
SENTINEL_FAILOVER_STATE_WAIT_PROMOTION))
{
// 更新从服务器的主服务器(已下线)的配置纪元
ri->master->config_epoch = ri->master->failover_epoch;
// 设置从服务器的主服务器(已下线)的故障转移状态
// 这个状态会让从服务器开始同步新的主服务器
ri->master->failover_state = SENTINEL_FAILOVER_STATE_RECONF_SLAVES;
// 更新从服务器的主服务器(已下线)的故障转移状态变更时间
ri->master->failover_state_change_time = mstime();
// 将当前 Sentinel 状态保存到配置文件里面
sentinelFlushConfig();
// 发送事件
sentinelEvent(REDIS_WARNING,"+promoted-slave",ri,"%@");
sentinelEvent(REDIS_WARNING,"+failover-state-reconf-slaves",
ri->master,"%@");
// 执行脚本
sentinelCallClientReconfScript(ri->master,SENTINEL_LEADER,
"start",ri->master->addr,ri->addr);
}
在这里代表当前从节点已经升级成功,则更新config_epoch。config_epoch其实在hello消息中被用到。如果其他sentienl发现它的config_epoch小于hello消息中的config_epoch,则会重置master的地址。
5.SENTINEL_FAILOVER_STATE_RECONF_SLAVES
Info更新成功后,会执行sentinelFailoverReconfNextSlave方法。其实就是向所有从服务器发送slave of命令。 这里会受parallel_syncs参数限制。控制并行slave of数量,避免主节点网络压力。
成功之后更新failover_state状态为SENTINEL_FAILOVER_STATE_UPDATE_CONFIG
sentinelFailoverSwitchToPromotedSlave
最终会调用该方法切换主服务器,整个转移过程结束。
故障转移失败
如果执行故障转移的leader存活的情况,转移超时,则会调用sentinelAbortFailover方法终止故障转移。 如果转移过场中leader宕机,其他节点会继续执行故障转移逻辑。
五、开发运维
配置参数
down-after-milliseconds 节点alive check的ttl
sentinel parallel-syncs 主节点宕机,允许并行复制数量
sentinel failover-timeout 故障转移的超时时间,如果当前时间-上次转移状态更新时间大于该值,则会终止转移。
sentinel notification-script 故障监控,sentinel警告级别的事件发生,会出发对应路径下的脚本,通过脚本可接收参数。进行监控。
客户端连接sentinel
启动通过遍历sentinel获取redis主节点。然后订阅每个sentinel的switch事件,保证在主备切换的时候能监听到。如果监听到变化 重新初始化连接池即可。
需要注意的是,只有故障转移完成才会发送此事件。
六、总结
整个实现比较复杂,但是按照每个点还是可以理清楚。通过ping、info、以及pubsub hello消息实现通信。保证整个系统的一致性。每次故障转移只会由一个sentinel执行,这个选举过程依赖raft算法leader选举逻辑。故障转移逻辑依赖超时时间避免死状态。整个逻辑依赖状态机进行切换,有条不紊,值得借鉴。