MongoDB 节点宕机引发的思考

Wesley13
• 阅读 736

MongoDB 节点宕机引发的思考

简介

最近一个 MongoDB 集群环境中的某节点异常下电了,导致业务出现了中断,随即又恢复了正常。通过ELK 告警也监测到了业务报错日志。

运维部对于节点下电的原因进行了排查,发现仅仅是资源分配上的一个失误导致。在解决了问题之后,大家也对这次中断的也提出了一些问题:

>”当前的 MongoDB集群 采用了分片副本集的架构,其中主节点发生故障会产生多大的影响?”
>”MongoDB 副本集不是能自动倒换吗,这个是不是秒级的?”

带着这些问题,下面针对副本集的自动Failover机制做一些分析。

日志分析

首先可以确认的是,这次掉电的是一个副本集上的主节点,在掉电的时候,主备关系发生了切换。

从另外的两个备节点找到了对应的日志:

备节点1的日志

2019-05-06T16:51:11.766+0800 I REPL     [ReplicationExecutor] Starting an election, since we've seen no PRIMARY in the past 10000ms

备节点2的日志

2019-05-06T16:51:12.816+0800 I ASIO     [NetworkInterfaceASIO-Replication-0] Ending connection to host 172.30.129.78:30071 due to bad connection status; 0 connections to that host remain open

可以看到,备节点1在 16:51:11 时主动发起了选举,并成为了新的主节点,随即备节点2在 16:51:12 获知了最新的主节点信息,因此可以确认此时主备切换已经完成。

同时在日志中出现的,还有对于原主节点(172.30.129.78:30071)大量心跳失败的信息。

那么,备节点具体是怎么感知到主节点已经 Down 掉的,主备节点之间的心跳是如何运作的,这对数据的同步复制又有什么影响?

下面,我们挖掘一下 ** 副本集的故障转移(Failover)** 机制

副本集是如何实现Failover

如下是一个PSS(一主两备)架构的副本集,主节点除了与两个备节点执行数据复制之外,三个节点之间还会通过心跳感知彼此的存活。

MongoDB 节点宕机引发的思考

一旦主节点发生故障以后,备节点将在某个周期内检测到主节点处于不可达的状态,此后将由其中一个备节点事先发起选举并最终成为新的主节点。这个检测周期 由electionTimeoutMillis 参数确定,默认是10s。

MongoDB 节点宕机引发的思考

接下来,我们通过一些源码看看该机制是如何实现的:

<>

db/repl/replication_coordinator_impl_heartbeat.cpp
相关方法
- ReplicationCoordinatorImpl::_startHeartbeats_inlock 启动各成员的心跳
- ReplicationCoordinatorImpl::_scheduleHeartbeatToTarget 调度任务-(计划)向成员发起心跳
- ReplicationCoordinatorImpl::_doMemberHeartbeat 执行向成员发起心跳
- ReplicationCoordinatorImpl::_handleHeartbeatResponse 处理心跳响应
- ReplicationCoordinatorImpl::_scheduleNextLivenessUpdate_inlock 调度保活状态检查定时器
- ReplicationCoordinatorImpl::_cancelAndRescheduleElectionTimeout_inlock 取消并重新调度选举超时定时器
- ReplicationCoordinatorImpl::_startElectSelfIfEligibleV1 发起主动选举

db/repl/topology_coordinator_impl.cpp
相关方法
- TopologyCoordinatorImpl::prepareHeartbeatRequestV1 构造心跳请求数据
- TopologyCoordinatorImpl::processHeartbeatResponse 处理心跳响应并构造下一步Action实例

下面这个图,描述了各个方法之间的调用关系

MongoDB 节点宕机引发的思考

图-主要关系

心跳的实现

首先,在副本集组建完成之后,节点会通过ReplicationCoordinatorImpl::_startHeartbeats_inlock方法开始向其他成员发送心跳:

void ReplicationCoordinatorImpl::_startHeartbeats_inlock() {``const Date_t now = _replExecutor.now(); _seedList.clear();``//获取副本集成员``for (int i = 0; i restartHeartbeats();``//使用V1的选举协议(3.2之后)``if (isV1ElectionProtocol()) {``for (auto&amp;&amp; slaveInfo : _slaveInfo) { slaveInfo.lastUpdate = _replExecutor.now(); slaveInfo.down = false; }``//调度保活状态检查定时器 _scheduleNextLivenessUpdate_inlock(); }``}

在获得当前副本集的节点信息后,调用_scheduleHeartbeatToTarget方法对其他成员发送心跳,这里_scheduleHeartbeatToTarget 的实现比较简单,其真正发起心跳是由 _doMemberHeartbeat 实现的,如下:

void ReplicationCoordinatorImpl::_scheduleHeartbeatToTarget(const HostAndPort&amp; target, int targetIndex, Date_t when) {``//执行调度,在某个时间点调用_doMemberHeartbeat _trackHeartbeatHandle( _replExecutor.scheduleWorkAt(when, stdx::bind(&amp;ReplicationCoordinatorImpl::_doMemberHeartbeat, this, stdx::placeholders::_1, target, targetIndex)));``}

ReplicationCoordinatorImpl::_doMemberHeartbeat 方法的实现如下:

void ReplicationCoordinatorImpl::_doMemberHeartbeat(ReplicationExecutor::CallbackArgs cbData,``const HostAndPort&amp; target,``int targetIndex) {``LockGuard topoLock(_topoMutex);``//取消callback 跟踪 _untrackHeartbeatHandle(cbData.myHandle);``if (cbData.status == ErrorCodes::CallbackCanceled) {``return; }``const Date_t now = _replExecutor.now(); BSONObj heartbeatObj;``Milliseconds timeout(0);``//3.2 以后的版本``if (isV1ElectionProtocol()) {``const std::pair hbRequest = _topCoord-&gt;prepareHeartbeatRequestV1(now, _settings.ourSetName(), target);``//构造请求,设置一个timeout heartbeatObj = hbRequest.first.toBSON(); timeout = hbRequest.second; } else { ... }``//构造远程命令``const RemoteCommandRequest request( target, "admin", heartbeatObj, BSON(rpc::kReplSetMetadataFieldName &lt;getTerm()) {``//取消并重新调度 electionTimeout定时器 cancelAndRescheduleElectionTimeout(); } } ...``//调用topCoord的processHeartbeatResponse方法处理心跳响应状态,并返回下一步执行的Action HeartbeatResponseAction action = _topCoord-&gt;processHeartbeatResponse( now, networkTime, target, hbStatusResponse, lastApplied); ...``//调度下一次心跳,时间间隔采用action提供的信息 _scheduleHeartbeatToTarget( target, targetIndex, std::max(now, action.getNextHeartbeatStartDate()));``//根据Action 执行处理 _handleHeartbeatResponseAction(action, hbStatusResponse, false);``}

这里省略了许多细节,但仍然可以看到,在响应心跳时会包含这些事情的处理:

- 对于主节点的成功响应,会重新调度 electionTimeout定时器(取消之前的调度并重新发起)
- 通过_topCoord对象的processHeartbeatResponse方法解析处理心跳响应,并返回下一步的Action指示
- 根据Action 指示中的下一次心跳时间设置下一次心跳定时任务
- 处理Action指示的动作

那么,心跳响应之后会等待多久继续下一次心跳呢?在 TopologyCoordinatorImpl::processHeartbeatResponse方法中,实现逻辑为:
如果心跳响应成功,会等待heartbeatInterval,该值是一个可配参数,默认为2s;
如果心跳响应失败,则会直接发送心跳(不等待)。

代码如下:

HeartbeatResponseAction TopologyCoordinatorImpl::processHeartbeatResponse(...) {

可以看到,这个定时器主要是用于实现主节点对其他节点的保活探测逻辑:

当主节点发现大多数节点不可达时(不满足大多数原则),将会让自己执行降备

因此,在一个三节点的副本集中,其中两个备节点挂掉后,主节点会自动降备。这样的设计主要是为了避免产生意外的数据不一致情况产生。

MongoDB 节点宕机引发的思考

图- 主自动降备

第二个是_cancelAndRescheduleElectionTimeout_inlock函数,这里则是实现自动Failover的关键了,它的逻辑中包含了一个选举定时器,代码如下:

void ReplicationCoordinatorImpl::_cancelAndRescheduleElectionTimeout_inlock() {

上面代码展示了这个选举定时器的逻辑,在每一个检测周期中,定时器都会尝试执行超时回调,而回调函数指向的是_startElectSelfIfEligibleV1,这里面就实现了主动发起选举的功能,
如果心跳响应成功,通过cancelAndRescheduleElectionTimeout调用将直接取消当次的超时回调(即不会发起选举)
如果心跳响应迟迟不能成功,那么定时器将被触发,进而导致备节点发起选举并成为新的主节点!

同时,这个回调方法(产生选举)被触发必须要满足以下条件:
1. 当前是备节点
2. 当前节点具备选举权限
3. 在检测周期内仍然没有与主节点心跳成功

这其中的检测周期略大于electionTimeout(10s),加入一个随机偏移量后大约是10-11.5s内,猜测这样的设计是为了错开多个备节点主动选举的时间,提升成功率。

最后,将整个自动选举切换的逻辑梳理后,如下图所示:

MongoDB 节点宕机引发的思考

图-超时自动选举

业务影响评估

副本集发生主备切换的情况下,不会影响现有的读操作,只会影响写操作。如果使用3.6及以上版本的驱动,可以通过开启retryWrite来降低影响。

但是如果主节点是属于强制掉电,那么整个 Failover 过程将会变长,很可能需要在Election定时器超时后才被副本集感知并恢复,这个时间窗口会在12s以内。

此外还需要考虑客户端或mongos对于副本集角色的监视和感知行为。但总之在问题恢复之前,对于原主节点的任何读写都会发生超时。

因此,对于极为重要的业务,建议最好在业务层面做一些防护策略,比如设计重试机制。

参考链接

https://docs.mongodb.com/manual/replication/#automatic-failover

https://www.percona.com/blog/2016/05/25/mongodb-3-2-elections-just-got-better/

https://www.percona.com/blog/2018/10/10/mongodb-replica-set-scenarios-and-internals/


欢迎关注"美码师"公众号,笔者是十年老兵一枚,欢迎留言打扰,话题不限于技术、职场或生活..

"写一首代码,做一手好菜",当技术与美走到一起时,生活也可以是诗和远方

MongoDB 节点宕机引发的思考

本文分享自微信公众号 - 美码师(gracebuilding)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Easter79 Easter79
3年前
swap空间的增减方法
(1)增大swap空间去激活swap交换区:swapoff v /dev/vg00/lvswap扩展交换lv:lvextend L 10G /dev/vg00/lvswap重新生成swap交换区:mkswap /dev/vg00/lvswap激活新生成的交换区:swapon v /dev/vg00/lvswap
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Karen110 Karen110
3年前
​一篇文章总结一下Python库中关于时间的常见操作
前言本次来总结一下关于Python时间的相关操作,有一个有趣的问题。如果你的业务用不到时间相关的操作,你的业务基本上会一直用不到。但是如果你的业务一旦用到了时间操作,你就会发现,淦,到处都是时间操作。。。所以思来想去,还是总结一下吧,本次会采用类型注解方式。time包importtime时间戳从1970年1月1日00:00:00标准时区诞生到现在
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这