1、sdown和odown转换机制
sdown和odown两种失败状态:
- sdown是主观宕机,就一个哨兵如果自己觉得一个master宕机了,那么就是主观宕机。sdown达成的条件很简单,如果一个哨兵ping一个master,超过了is-master-down-after-milliseconds指定的毫秒数之后,就主观认为master宕机
- odown是客观宕机,如果quorum数量的哨兵都觉得一个master宕机了,那么就是客观宕机。
sdown到odown转换的条件很简单,如果一个哨兵在指定时间内,收到了quorum指定数量的其他哨兵也认为那个master是sdown了,那么就认为是odown了,客观认为master宕机。
我们可以通过源码来看看哨兵是如何工作的:
首先,在定时任务severCron()函数中,如果发现是哨兵则会执行哨兵的计时器sentinelTimer()。
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { ... /* Run the Sentinel timer if we are in sentinel mode. */ if (server.sentinel_mode) sentinelTimer(); ...}
在sentinelTimer()计时器中,会针对每一个哨兵进行处理,调用函数为sentinelHandleDictOfRedisInstances(),检查所有的实例。
void sentinelTimer(void) { ... sentinelHandleDictOfRedisInstances(sentinel.masters); ...}
在sentinelHandleDictOfRedisInstances()函数中,主要是递归遍历,调用sentinelHandleRedisInstance()对实例进行处理。
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) { sentinelReconnectInstance(ri); // 发送ping消息给这个实例 sentinelSendPeriodicCommands(ri); if (sentinel.tilt) { if (mstime()-sentinel.tilt_start_time < SENTINEL_TILT_PERIOD) return; sentinel.tilt = 0; sentinelEvent(LL_WARNING,"-tilt",NULL,"#tilt mode exited"); } // 判断是否主观下线 sentinelCheckSubjectivelyDown(ri); if (ri->flags & (SRI_MASTER|SRI_SLAVE)) { /* Nothing so far. */ } // 只针对master节点 if (ri->flags & SRI_MASTER) { // 判断是都客观下线 sentinelCheckObjectivelyDown(ri); if (sentinelStartFailoverIfNeeded(ri)) sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED); sentinelFailoverStateMachine(ri); sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS); }}
如何判断主观下线:
void sentinelCheckSubjectivelyDown(sentinelRedisInstance *ri) { mstime_t elapsed = 0; // act_ping_time:最后一个未完成的ping(之后没有收到pong)发出的时间。当接收到一个pong时,该字段被设置为0,如果该值为0并且发送了一个新的ping,则再次设置为当前时间。(说明已经出现ping过没回复) if (ri->link->act_ping_time) elapsed = mstime() - ri->link->act_ping_time; else if (ri->link->disconnected) // 连接不上,还没有出现ping过没回复 elapsed = mstime() - ri->link->last_avail_time; // 如果检测到连接的活跃度(activity)很低,那么考虑重断开连接,并进行重连 if (ri->link->cc && (mstime() - ri->link->cc_conn_time) > SENTINEL_MIN_LINK_RECONNECT_PERIOD && ri->link->act_ping_time != 0 && /* There is a pending ping... */ (mstime() - ri->link->act_ping_time) > (ri->down_after_period/2) && (mstime() - ri->link->last_pong_time) > (ri->down_after_period/2)) { // 连接时长已超过最短连接间隔 ping已经发出,但在down_after_period/2的时间内没有收到pong响应 // 断开实例的cc连接(命令连接) instanceLinkCloseConnection(ri->link,ri->link->cc); } // 最后一次从这个服务器接收消息的时间大于发布publish的最大时间的3倍,也就是6秒时断开订阅频道信息的连接 if (ri->link->pc && (mstime() - ri->link->pc_conn_time) > SENTINEL_MIN_LINK_RECONNECT_PERIOD && (mstime() - ri->link->pc_last_activity) > (SENTINEL_PUBLISH_PERIOD*3)) { // 断开发布订阅的连接 pc instanceLinkCloseConnection(ri->link,ri->link->pc); } //现在距离上次ping的时间已经超过了down_after_period,可以判定为主观宕机 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; } } else { /* 它活过来了,判定会不宕机*/ if (ri->flags & SRI_S_DOWN) { // 发送事件 sentinelEvent(LL_WARNING,"-sdown",ri,"%@"); // 移除掉之前的宕机标识 ri->flags &= ~(SRI_S_DOWN|SRI_SCRIPT_KILL_SENT); } }}
如何判断客观宕机,sentinelCheckObjectivelyDown(ri):
void sentinelCheckObjectivelyDown(sentinelRedisInstance *master) { dictIterator *di; dictEntry *de; unsigned int quorum = 0, odown = 0; //当前的sentinel判断它为客观单价 if (master->flags & SRI_S_DOWN) { /* Is down for enough sentinels? */ //需要满足条件 quorum = 1; /* the current sentinel. */ /* Count all the other sentinels. */ // 遍历所有的sentinel di = dictGetIterator(master->sentinels); while((de = dictNext(di)) != NULL) { sentinelRedisInstance *ri = dictGetVal(de); if (ri->flags & SRI_MASTER_DOWN) quorum++; } //释放迭代器 dictReleaseIterator(di); //是否满足认同数量大于等于quorum,满足则认为是客观宕机(odown) if (quorum >= master->quorum) odown = 1; } /* 如果是odown*/ if (odown) { if ((master->flags & SRI_O_DOWN) == 0) { // 发送事件 sentinelEvent(LL_WARNING,"+odown",master,"%@ #quorum %d/%d", quorum, master->quorum); // 设置odown标识 master->flags |= SRI_O_DOWN; // 记录事件 master->o_down_since_time = mstime(); } } else { if (master->flags & SRI_O_DOWN) { // 没有进入odown,发送事件 sentinelEvent(LL_WARNING,"-odown",master,"%@"); // 移除标识 master->flags &= ~SRI_O_DOWN; } }}
2、哨兵集群的自动发现机制
哨兵互相之间的发现,是通过redis的pub/sub系统实现的,每个哨兵都会往__sentinel__:hello这个channel里发送一个消息,这时候所有其他哨兵都可以消费到这个消息,并感知到其他的哨兵的存在。
消息主要包括:
- s_ip: sentinel的ip
- s_port: sentinel的端口port
- s_runnid:sentinel的runnid
- s_epoch:纪元,版本
- m_name:master的名称
- m_ip:master的ip
- m_port:master 的端口
- m_epoch:master的epoch
每隔两秒钟,每个哨兵都会往自己监控的某个master+slaves对应的__sentinel__:hellochannel里发送一个消息,内容是自己的host、ip和runid还有对这个master的监控配置
每个哨兵也会去监听自己监控的每个master+slaves对应的__sentinel__:hello channel,然后去感知到同样在监听这个master+slaves的其他哨兵的存在
每个哨兵还会跟其他哨兵交换对master的监控配置,互相进行监控配置的同步
3、slave配置的自动纠正
哨兵会负责自动纠正slave的一些配置,比如slave如果要成为潜在的master候选人,哨兵会确保slave在复制现有master的数据; 如果slave连接到了一个错误的master上,比如故障转移之后,那么哨兵会确保它们连接到正确的master上
4、slave->master选举算法
如果一个master被认为odown了,而且majority哨兵都允许了主备切换,那么某个哨兵就会执行主备切换操作,此时首先要选举一个slave来,优先级会考虑slave的一些信息。
- (1)跟master断开连接的时长,如果一个slave跟master断开连接已经超过了down-after-milliseconds的10倍,外加master宕机的时长,也就是:(down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state ,那么slave就被认为不适合选举为master,被排除掉。(这个仅仅是用于排除,不用于排序),剩下的执行后面的三个比较。
- (2)slave优先级,slave priority越低,优先级就越高,这个是可以进行配置的。默认是100,越小越优先。
- (3)复制offset,如果slave priority相同,那么看replica offset,哪个slave复制了越多的数据,offset越靠后,优先级就越高
- (4)run id,选择一个run id比较小的那个slave
5、quorum和majority
每次一个哨兵要做主备切换,首先需要quorum数量的哨兵认为odown,然后选举出一个哨兵来做切换,这个哨兵还得得到majority哨兵的授权,才能正式执行切换
如果quorum < majority,比如5个哨兵,majority就是3,quorum设置为2,那么就3个哨兵授权就可以执行切换
但是如果quorum >= majority,那么必须quorum数量的哨兵都授权,比如5个哨兵,quorum是5,那么必须5个哨兵都同意授权,才能执行切换。
这个quorum是我们自己设置的, majority是客观存在的。
6、configuration epoch
哨兵会对一套redis master+slave进行监控,有相应的监控的配置,也就是可以理解为配置版本。
执行切换的那个哨兵,会从要切换到的新master(salve->master)那里得到一个configuration epoch,这就是一个version号,每次切换的version号都必须是唯一的
如果第一个选举出的哨兵切换失败了,那么其他哨兵,会等待failover-timeout时间,然后接替继续执行切换,此时会重新获取一个新的configuration epoch,作为新的version号。
7、configuraiton传播
哨兵完成切换之后,会在自己本地更新生成最新的master配置,然后同步给其他的哨兵,就是通过之前说的pub/sub消息机制。要知道,主备切换是一个哨兵在干,干完了总得让别人知道是不是。
这里之前的version号就很重要了,因为各种消息都是通过一个channel去发布和监听的,所以一个哨兵完成一次新的切换之后,新的master配置是跟着新的version号的。其他的哨兵会订阅这个消息,发现更新的epoch就认为master已经切换过了,接着把自己存的信息更新。这样,就可以保持哨兵里面的信息一致性了。
此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~
技术之路不在一时,山高水长,纵使缓慢,驰而不息。