MongoDB一次節(jié)點宕機(jī)引發(fā)的思考
簡介
最近一個 MongoDB 集群環(huán)境中的某節(jié)點異常下電了,導(dǎo)致業(yè)務(wù)出現(xiàn)了中斷,隨即又恢復(fù)了正常。
通過ELK 告警也監(jiān)測到了業(yè)務(wù)報錯日志。
運(yùn)維部對于節(jié)點下電的原因進(jìn)行了排查,發(fā)現(xiàn)僅僅是資源分配上的一個失誤導(dǎo)致。 在解決了問題之后,大家也對這次中斷的也提出了一些問題:
"當(dāng)前的 MongoDB集群 采用了分片副本集的架構(gòu),其中主節(jié)點發(fā)生故障會產(chǎn)生多大的影響?"
"MongoDB 副本集不是能自動倒換嗎,這個是不是秒級的?"
帶著這些問題,下面針對副本集的自動Failover機(jī)制做一些分析。
日志分析
首先可以確認(rèn)的是,這次掉電的是一個副本集上的主節(jié)點,在掉電的時候,主備關(guān)系發(fā)生了切換。
從另外的兩個備節(jié)點找到了對應(yīng)的日志:
備節(jié)點1的日志
- 2019-05-06T16:51:11.766+0800 I REPL [ReplicationExecutor] Starting an election, since we've seen no PRIMARY in the past 10000ms
- 2019-05-06T16:51:11.766+0800 I REPL [ReplicationExecutor] conducting a dry run election to see if we could be elected
- 2019-05-06T16:51:11.766+0800 I ASIO [NetworkInterfaceASIO-Replication-0] Connecting to 172.30.129.78:30071
- 2019-05-06T16:51:11.767+0800 I REPL [ReplicationExecutor] VoteRequester(term 3 dry run) received a yes vote from 172.30.129.7:30071; response message: { term: 3, voteGranted: true, reason: "", ok: 1.0 }
- 2019-05-06T16:51:11.767+0800 I REPL [ReplicationExecutor] dry election run succeeded, running for election
- 2019-05-06T16:51:11.768+0800 I ASIO [NetworkInterfaceASIO-Replication-0] Connecting to 172.30.129.78:30071
- 2019-05-06T16:51:11.771+0800 I REPL [ReplicationExecutor] VoteRequester(term 4) received a yes vote from 172.30.129.7:30071; response message: { term: 4, voteGranted: true, reason: "", ok: 1.0 }
- 2019-05-06T16:51:11.771+0800 I REPL [ReplicationExecutor] election succeeded, assuming primary role in term 4
- 2019-05-06T16:51:11.771+0800 I REPL [ReplicationExecutor] transition to PRIMARY
- 2019-05-06T16:51:11.771+0800 I REPL [ReplicationExecutor] Entering primary catch-up mode.
- 2019-05-06T16:51:11.771+0800 I ASIO [NetworkInterfaceASIO-Replication-0] Ending connection to host 172.30.129.78:30071 due to bad connection status; 2 connections to that host remain open
- 2019-05-06T16:51:11.771+0800 I ASIO [NetworkInterfaceASIO-Replication-0] Connecting to 172.30.129.78:30071
- 2019-05-06T16:51:13.350+0800 I REPL [ReplicationExecutor] Error in heartbeat request to 172.30.129.78:30071; ExceededTimeLimit: Couldn't get a connection within the time limit
備節(jié)點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
- 2019-05-06T16:51:12.816+0800 I REPL [ReplicationExecutor] Error in heartbeat request to 172.30.129.78:30071; ExceededTimeLimit: Operation timed out, request was RemoteCommand 72553 -- target:172.30.129.78:30071 db:admin expDate:2019-05-06T16:51:12.816+0800 cmd:{ replSetHeartbeat: "shard0", configVersion: 96911, from: "172.30.129.7:30071", fromId: 1, term: 3 }
- 2019-05-06T16:51:12.821+0800 I REPL [ReplicationExecutor] Member 172.30.129.160:30071 is now in state PRIMARY
可以看到,備節(jié)點1在 16:51:11 時主動發(fā)起了選舉,并成為了新的主節(jié)點,隨即備節(jié)點2在 16:51:12 獲知了最新的主節(jié)點信息,因此可以確認(rèn)此時主備切換已經(jīng)完成。
同時在日志中出現(xiàn)的,還有對于原主節(jié)點(172.30.129.78:30071)大量心跳失敗的信息。
那么,備節(jié)點具體是怎么感知到主節(jié)點已經(jīng) Down 掉的,主備節(jié)點之間的心跳是如何運(yùn)作的,這對數(shù)據(jù)的同步復(fù)制又有什么影響?
下面,我們挖掘一下 ** 副本集的 自動故障轉(zhuǎn)移(Failover)** 機(jī)制
副本集 如何實現(xiàn) Failover
如下是一個PSS(一主兩備)架構(gòu)的副本集,主節(jié)點除了與兩個備節(jié)點執(zhí)行數(shù)據(jù)復(fù)制之外,三個節(jié)點之間還會通過心跳感知彼此的存活。

一旦主節(jié)點發(fā)生故障以后,備節(jié)點將在某個周期內(nèi)檢測到主節(jié)點處于不可達(dá)的狀態(tài),此后將由其中一個備節(jié)點事先發(fā)起選舉并最終成為新的主節(jié)點。 這個檢測周期 由electionTimeoutMillis 參數(shù)確定,默認(rèn)是10s。

接下來,我們通過一些源碼看看該機(jī)制是如何實現(xiàn)的:
<<來自 MongoDB 3.4源碼>>
db/repl/replication_coordinator_impl_heartbeat.cpp
相關(guān)方法
- ReplicationCoordinatorImpl::_startHeartbeats_inlock 啟動各成員的心跳
- ReplicationCoordinatorImpl::_scheduleHeartbeatToTarget 調(diào)度任務(wù)-(計劃)向成員發(fā)起心跳
- ReplicationCoordinatorImpl::_doMemberHeartbeat 執(zhí)行向成員發(fā)起心跳
- ReplicationCoordinatorImpl::_handleHeartbeatResponse 處理心跳響應(yīng)
- ReplicationCoordinatorImpl::_scheduleNextLivenessUpdate_inlock 調(diào)度保活狀態(tài)檢查定時器
- ReplicationCoordinatorImpl::_cancelAndRescheduleElectionTimeout_inlock 取消并重新調(diào)度選舉超時定時器
- ReplicationCoordinatorImpl::_startElectSelfIfEligibleV1 發(fā)起主動選舉
db/repl/topology_coordinator_impl.cpp
相關(guān)方法
- TopologyCoordinatorImpl::prepareHeartbeatRequestV1 構(gòu)造心跳請求數(shù)據(jù)
- TopologyCoordinatorImpl::processHeartbeatResponse 處理心跳響應(yīng)并構(gòu)造下一步Action實例
下面這個圖,描述了各個方法之間的調(diào)用關(guān)系

圖-主要關(guān)系
心跳的實現(xiàn)
首先,在副本集組建完成之后,節(jié)點會通過ReplicationCoordinatorImpl::_startHeartbeats_inlock方法開始向其他成員發(fā)送心跳:
- void ReplicationCoordinatorImpl::_startHeartbeats_inlock() {
- const Date_t now = _replExecutor.now();
- _seedList.clear();
- //獲取副本集成員
- for (int i = 0; i < _rsConfig.getNumMembers(); ++i) {
- if (i == _selfIndex) {
- continue;
- }
- //向其他成員發(fā)送心跳
- _scheduleHeartbeatToTarget(_rsConfig.getMemberAt(i).getHostAndPort(), i, now);
- }
- //僅僅是刷新本地的心跳狀態(tài)數(shù)據(jù)
- _topCoord->restartHeartbeats();
- //使用V1的選舉協(xié)議(3.2之后)
- if (isV1ElectionProtocol()) {
- for (auto&& slaveInfo : _slaveInfo) {
- slaveInfo.lastUpdate = _replExecutor.now();
- slaveInfo.down = false;
- }
- //調(diào)度?;顮顟B(tài)檢查定時器
- _scheduleNextLivenessUpdate_inlock();
- }
- }
在獲得當(dāng)前副本集的節(jié)點信息后,調(diào)用_scheduleHeartbeatToTarget方法對其他成員發(fā)送心跳,
這里_scheduleHeartbeatToTarget 的實現(xiàn)比較簡單,其真正發(fā)起心跳是由 _doMemberHeartbeat 實現(xiàn)的,如下:
- void ReplicationCoordinatorImpl::_scheduleHeartbeatToTarget(const HostAndPort& target,
- int targetIndex,
- Date_t when) {
- //執(zhí)行調(diào)度,在某個時間點調(diào)用_doMemberHeartbeat
- _trackHeartbeatHandle(
- _replExecutor.scheduleWorkAt(when,
- stdx::bind(&ReplicationCoordinatorImpl::_doMemberHeartbeat,
- this,
- stdx::placeholders::_1,
- target,
- targetIndex)));
- }
ReplicationCoordinatorImpl::_doMemberHeartbeat 方法的實現(xiàn)如下:
- void ReplicationCoordinatorImpl::_doMemberHeartbeat(ReplicationExecutor::CallbackArgs cbData,
- const HostAndPort& 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<ReplSetHeartbeatArgsV1, Milliseconds> hbRequest =
- _topCoord->prepareHeartbeatRequestV1(now, _settings.ourSetName(), target);
- //構(gòu)造請求,設(shè)置一個timeout
- heartbeatObj = hbRequest.first.toBSON();
- timeout = hbRequest.second;
- } else {
- ...
- }
- //構(gòu)造遠(yuǎn)程命令
- const RemoteCommandRequest request(
- target, "admin", heartbeatObj, BSON(rpc::kReplSetMetadataFieldName << 1), nullptr, timeout);
- //設(shè)置遠(yuǎn)程命令回調(diào),指向_handleHeartbeatResponse方法
- const ReplicationExecutor::RemoteCommandCallbackFn callback =
- stdx::bind(&ReplicationCoordinatorImpl::_handleHeartbeatResponse,
- this,
- stdx::placeholders::_1,
- targetIndex);
- _trackHeartbeatHandle(_replExecutor.scheduleRemoteCommand(request, callback));
- }
上面的代碼中存在的一些細(xì)節(jié):
- 心跳的超時時間,在_topCoord.prepareHeartbeatRequestV1方法中就已經(jīng)設(shè)定好了
- 具體的算法就是:
- **hbTimeout=_rsConfig.getHeartbeatTimeoutPeriodMillis() - alreadyElapsed**
其中heartbeatTimeoutPeriodMillis是可配置的參數(shù),默認(rèn)是10s, 那么alreadyElapsed是指此前連續(xù)心跳失敗(最多2次)累計的消耗時間,在心跳成功響應(yīng)或者超過10s后alreadyElapsed會置為0。因此可以判斷,隨著心跳失敗次數(shù)的增加,超時時間會越來越短(心跳更加密集)
心跳執(zhí)行的回調(diào),指向自身的_handleHeartbeatResponse方法,該函數(shù)實現(xiàn)了心跳響應(yīng)成功、失敗(或是超時)之后的流程處理。
ReplicationCoordinatorImpl::_handleHeartbeatResponse方法的代碼片段:
- void ReplicationCoordinatorImpl::_handleHeartbeatResponse(
- const ReplicationExecutor::RemoteCommandCallbackArgs& cbData, int targetIndex) {
- LockGuard topoLock(_topoMutex);
- // remove handle from queued heartbeats
- _untrackHeartbeatHandle(cbData.myHandle);
- ...
- //響應(yīng)成功后
- if (responseStatus.isOK()) {
- networkTime = cbData.response.elapsedMillis.value_or(Milliseconds{0});
- const auto& hbResponse = hbStatusResponse.getValue();
- // 只要primary 心跳響應(yīng)成功,就會重新調(diào)度 electionTimeout定時器
- if (hbResponse.hasState() && hbResponse.getState().primary() &&
- hbResponse.getTerm() == _topCoord->getTerm()) {
- //取消并重新調(diào)度 electionTimeout定時器
- cancelAndRescheduleElectionTimeout();
- }
- }
- ...
- //調(diào)用topCoord的processHeartbeatResponse方法處理心跳響應(yīng)狀態(tài),并返回下一步執(zhí)行的Action
- HeartbeatResponseAction action = _topCoord->processHeartbeatResponse(
- now, networkTime, target, hbStatusResponse, lastApplied);
- ...
- //調(diào)度下一次心跳,時間間隔采用action提供的信息
- _scheduleHeartbeatToTarget(
- target, targetIndex, std::max(now, action.getNextHeartbeatStartDate()));
- //根據(jù)Action 執(zhí)行處理
- _handleHeartbeatResponseAction(action, hbStatusResponse, false);
- }
這里省略了許多細(xì)節(jié),但仍然可以看到,在響應(yīng)心跳時會包含這些事情的處理:
對于主節(jié)點的成功響應(yīng),會重新調(diào)度 electionTimeout定時器(取消之前的調(diào)度并重新發(fā)起)
通過_topCoord對象的processHeartbeatResponse方法解析處理心跳響應(yīng),并返回下一步的Action指示
根據(jù)Action 指示中的下一次心跳時間設(shè)置下一次心跳定時任務(wù)
處理Action指示的動作
那么,心跳響應(yīng)之后會等待多久繼續(xù)下一次心跳呢? 在 TopologyCoordinatorImpl::processHeartbeatResponse方法中,實現(xiàn)邏輯為:
如果心跳響應(yīng)成功,會等待heartbeatInterval,該值是一個可配參數(shù),默認(rèn)為2s;
如果心跳響應(yīng)失敗,則會直接發(fā)送心跳(不等待)。
代碼如下:
- HeartbeatResponseAction TopologyCoordinatorImpl::processHeartbeatResponse(...) {
- ...
- const Milliseconds alreadyElapsed = now - hbStats.getLastHeartbeatStartDate();
- Date_t nextHeartbeatStartDate;
- // 計算下一次 心跳啟動時間
- // numFailuresSinceLastStart 對應(yīng)連續(xù)失敗的次數(shù)(2次以內(nèi))
- if (hbStats.getNumFailuresSinceLastStart() <= kMaxHeartbeatRetries &&
- alreadyElapsed < _rsConfig.getHeartbeatTimeoutPeriod()) {
- // 心跳失敗,不等待,直接重試心跳
- nextHeartbeatStartDate = now;
- } else {
- // 心跳成功,等待一定間隔后再次發(fā)送(一般是2s)
- nextHeartbeatStartDate = now + heartbeatInterval;
- }
- ...
- // 決定下一步的動作,可能發(fā)生 tack over(本備節(jié)點優(yōu)先級更高,且數(shù)據(jù)與主節(jié)點一樣新時)
- HeartbeatResponseAction nextAction;
- if (_rsConfig.getProtocolVersion() == 0) {
- ...
- } else {
- nextAction = _updatePrimaryFromHBDataV1(memberIndex, originalState, now, myLastOpApplied);
- }
- nextAction.setNextHeartbeatStartDate(nextHeartbeatStartDate);
- return nextAction;
- }
electionTimeout 定時器
至此,我們已經(jīng)知道了心跳實現(xiàn)的一些細(xì)節(jié),默認(rèn)情況下副本集節(jié)點會每2s向其他節(jié)點發(fā)出心跳(默認(rèn)的超時時間是10s)。
如果心跳成功,將會持續(xù)以2s的頻率繼續(xù)發(fā)送心跳,在心跳失敗的情況下,則會立即重試心跳(以更短的超時時間),一直到心跳恢復(fù)成功或者超過10s的周期。
那么,心跳失敗是如何觸發(fā)主備切換的呢,electionTimeout 又是如何發(fā)揮作用?
在前面的過程中,與electionTimeout參數(shù)相關(guān)兩個方法如下,它們也分別對應(yīng)了單獨的定時器:
ReplicationCoordinatorImpl::_scheduleNextLivenessUpdate_inlock 發(fā)起保活狀態(tài)檢查定時器
ReplicationCoordinatorImpl::_cancelAndRescheduleElectionTimeout_inlock 重新發(fā)起選舉超時定時器
第一個是 _scheduleNextLivenessUpdate_inlock這個函數(shù),它的作用在于?;顮顟B(tài)檢測,如下:
- void ReplicationCoordinatorImpl::_scheduleNextLivenessUpdate_inlock() {
- //僅僅支持3.2+
- if (!isV1ElectionProtocol()) {
- return;
- }
- // earliestDate 取所有節(jié)點中更新時間最早的(以盡可能早的發(fā)現(xiàn)問題)
- // electionTimeoutPeriod 默認(rèn)為 10s
- auto nextTimeout = earliestDate + _rsConfig.getElectionTimeoutPeriod();
- // 設(shè)置超時回調(diào)函數(shù)為 _handleLivenessTimeout
- auto cbh = _scheduleWorkAt(nextTimeout,
- stdx::bind(&ReplicationCoordinatorImpl::_handleLivenessTimeout,
- this,
- stdx::placeholders::_1));
- }
因此,在大約10s后,如果沒有什么意外,_handleLivenessTimeout將會被觸發(fā),如下:
- void ReplicationCoordinatorImpl::_handleLivenessTimeout(...) {
- ...
- for (auto&& slaveInfo : _slaveInfo) {
- ...
- //lastUpdate 不夠新(小于electionTimeout)
- if (now - slaveInfo.lastUpdate >= _rsConfig.getElectionTimeoutPeriod()) {
- ...
- //在?;钪芷诤笕匀晃锤鹿?jié)點,置為down狀態(tài)
- slaveInfo.down = true;
- //如果當(dāng)前節(jié)點是主,且檢測到某個備節(jié)點為down的狀態(tài),進(jìn)入memberdown流程
- if (_memberState.primary()) {
- //調(diào)用_topCoord的setMemberAsDown方法,記錄某個備節(jié)點不可達(dá),并獲得下一步的指示
- //當(dāng)大多數(shù)節(jié)點不可見時,這里會獲得讓自身降備的指示
- HeartbeatResponseAction action =
- _topCoord->setMemberAsDown(now, memberIndex, _getMyLastDurableOpTime_inlock());
- //執(zhí)行指示
- _handleHeartbeatResponseAction(action,
- makeStatusWith<ReplSetHeartbeatResponse>(),
- true);
- }
- }
- }
- //繼續(xù)調(diào)度下一個周期
- _scheduleNextLivenessUpdate_inlock();
- }
可以看到,這個定時器主要是用于實現(xiàn)主節(jié)點對其他節(jié)點的?;钐綔y邏輯:
當(dāng)主節(jié)點發(fā)現(xiàn)大多數(shù)節(jié)點不可達(dá)時(不滿足大多數(shù)原則),將會讓自己執(zhí)行降備
因此,在一個三節(jié)點的副本集中,其中兩個備節(jié)點掛掉后,主節(jié)點會自動降備。 這樣的設(shè)計主要是為了避免產(chǎn)生意外的數(shù)據(jù)不一致情況產(chǎn)生。

圖- 主自動降備
第二個是_cancelAndRescheduleElectionTimeout_inlock函數(shù),這里則是實現(xiàn)自動Failover的關(guān)鍵了,
它的邏輯中包含了一個選舉定時器,代碼如下:
- void ReplicationCoordinatorImpl::_cancelAndRescheduleElectionTimeout_inlock() {
- //如果上一個定時器已經(jīng)啟用了,則直接取消
- if (_handleElectionTimeoutCbh.isValid()) {
- LOG(4) << "Canceling election timeout callback at " << _handleElectionTimeoutWhen;
- _replExecutor.cancel(_handleElectionTimeoutCbh);
- _handleElectionTimeoutCbh = CallbackHandle();
- _handleElectionTimeoutWhen = Date_t();
- }
- //僅支持3.2后的V1版本
- if (!isV1ElectionProtocol()) {
- return;
- }
- //僅備節(jié)點可執(zhí)行
- if (!_memberState.secondary()) {
- return;
- }
- ...
- //是否可以選舉
- if (!_rsConfig.getMemberAt(_selfIndex).isElectable()) {
- return;
- }
- //檢測周期,由 electionTimeout + randomOffset
- //randomOffset是隨機(jī)偏移量,默認(rèn)為 0~0.15*ElectionTimeoutPeriod = 0~1.5s
- Milliseconds randomOffset = _getRandomizedElectionOffset();
- auto now = _replExecutor.now();
- auto when = now + _rsConfig.getElectionTimeoutPeriod() + randomOffset;
- LOG(4) << "Scheduling election timeout callback at " << when;
- _handleElectionTimeoutWhen = when;
- //觸發(fā)調(diào)度,時間為 now + ElectionTimeoutPeriod + randomOffset
- _handleElectionTimeoutCbh =
- _scheduleWorkAt(when,
- stdx::bind(&ReplicationCoordinatorImpl::_startElectSelfIfEligibleV1,
- this,
- StartElectionV1Reason::kElectionTimeout));
- }
上面代碼展示了這個選舉定時器的邏輯,在每一個檢測周期中,定時器都會嘗試執(zhí)行超時回調(diào),
而回調(diào)函數(shù)指向的是_startElectSelfIfEligibleV1,這里面就實現(xiàn)了主動發(fā)起選舉的功能,
如果心跳響應(yīng)成功,通過cancelAndRescheduleElectionTimeout調(diào)用將直接取消當(dāng)次的超時回調(diào)(即不會發(fā)起選舉)
如果心跳響應(yīng)遲遲不能成功,那么定時器將被觸發(fā),進(jìn)而導(dǎo)致備節(jié)點發(fā)起選舉并成為新的主節(jié)點!
同時,這個回調(diào)方法(產(chǎn)生選舉)被觸發(fā)必須要滿足以下條件:
- 當(dāng)前是備節(jié)點
- 當(dāng)前節(jié)點具備選舉權(quán)限
- 在檢測周期內(nèi)仍然沒有與主節(jié)點心跳成功
這其中的檢測周期略大于electionTimeout(10s),加入一個隨機(jī)偏移量后大約是10-11.5s內(nèi),猜測這樣的設(shè)計是為了錯開多個備節(jié)點主動選舉的時間,提升成功率。
最后,將整個自動選舉切換的邏輯梳理后,如下圖所示:

圖-超時自動選舉
業(yè)務(wù)影響評估
副本集發(fā)生主備切換的情況下,不會影響現(xiàn)有的讀操作,只會影響寫操作。 如果使用3.6及以上版本的驅(qū)動,可以通過開啟retryWrite來降低影響。
但是如果主節(jié)點是屬于強(qiáng)制掉電,那么整個 Failover 過程將會變長,很可能需要在Election定時器超時后才被副本集感知并恢復(fù),這個時間窗口會在12s以內(nèi)。
此外還需要考慮客戶端或mongos對于副本集角色的監(jiān)視和感知行為。但總之在問題恢復(fù)之前,對于原主節(jié)點的任何讀寫都會發(fā)生超時。
因此,對于極為重要的業(yè)務(wù),建議最好在業(yè)務(wù)層面做一些防護(hù)策略,比如設(shè)計重試機(jī)制。