副本集是如何實現自動Failover的

簡介

最近一個 MongoDB 集羣環境中的某節點異常下電了,致使業務出現了中斷,隨即又恢復了正常。
經過ELK 告警也監測到了業務報錯日誌。ios

運維部對於節點下電的緣由進行了排查,發現僅僅是資源分配上的一個失誤致使。 在解決了問題以後,你們也對此次中斷的也提出了一些問題:mongodb

」當前的 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
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
備節點2的日誌dom

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
能夠看到,備節點1在 16:51:11 時主動發起了選舉,併成爲了新的主節點,隨即備節點2在 16:51:12 獲知了最新的主節點信息,所以能夠確認此時主備切換已經完成。
同時在日誌中出現的,還有對於原主節點(172.30.129.78:30071)大量心跳失敗的信息。分佈式

那麼,備節點具體是怎麼感知到主節點已經 Down 掉的,主備節點之間的心跳是如何運做的,這對數據的同步複製又有什麼影響?
下面,咱們挖掘一下 副本集的故障轉移(Failover) 機制函數

副本集 如何實現 Failover

以下是一個PSS(一主兩備)架構的副本集,主節點除了與兩個備節點執行數據複製以外,三個節點之間還會經過心跳感知彼此的存活。
圖片描述性能

一旦主節點發生故障之後,備節點將在某個週期內檢測到主節點處於不可達的狀態,此後將由其中一個備節點事先發起選舉並最終成爲新的主節點。 這個檢測週期 由electionTimeoutMillis 參數肯定,默認是10s。
圖片描述優化

接下來,咱們經過一些源碼看看該機制是如何實現的:

<>

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實例

下面這個圖,描述了各個方法之間的調用關係

圖片描述

圖-主要關係

心跳的實現

首先,在副本集組建完成以後,節點會經過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& 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(...) {

...

const Milliseconds alreadyElapsed = now - hbStats.getLastHeartbeatStartDate();
Date_t nextHeartbeatStartDate;

// 計算下一次 心跳啓動時間
// numFailuresSinceLastStart 對應連續失敗的次數(2次之內)
if (hbStats.getNumFailuresSinceLastStart() &lt;= kMaxHeartbeatRetries &amp;&amp;
    alreadyElapsed = _rsConfig.getElectionTimeoutPeriod()) {
        ...
        //在保活週期後仍然未更新節點,置爲down狀態
        slaveInfo.down = true;

        //若是當前節點是主,且檢測到某個備節點爲down的狀態,進入memberdown流程
        if (_memberState.primary()) {

            //調用_topCoord的setMemberAsDown方法,記錄某個備節點不可達,並得到下一步的指示
           //當大多數節點不可見時,這裏會得到讓自身降備的指示
            HeartbeatResponseAction action =
                _topCoord-&gt;setMemberAsDown(now, memberIndex, _getMyLastDurableOpTime_inlock());
            //執行指示
            _handleHeartbeatResponseAction(action,
                                           makeStatusWith(),
                                           true);
        }
    }
}
//繼續調度下一個週期
_scheduleNextLivenessUpdate_inlock();

}
能夠看到,這個定時器主要是用於實現主節點對其餘節點的保活探測邏輯:

當主節點發現大多數節點不可達時(不知足大多數原則),將會讓本身執行降備

所以,在一個三節點的副本集中,其中兩個備節點掛掉後,主節點會自動降備。 這樣的設計主要是爲了不產生意外的數據不一致狀況產生。
圖片描述
圖- 主自動降備

第二個是_cancelAndRescheduleElectionTimeout_inlock函數,這裏則是實現自動Failover的關鍵了,
它的邏輯中包含了一個選舉定時器,代碼以下:

void ReplicationCoordinatorImpl::_cancelAndRescheduleElectionTimeout_inlock() {

//若是上一個定時器已經啓用了,則直接取消
if (_handleElectionTimeoutCbh.isValid()) {
    LOG(4) &lt;&lt; &quot;Canceling election timeout callback at &quot; &lt;&lt; _handleElectionTimeoutWhen;
    _replExecutor.cancel(_handleElectionTimeoutCbh);
    _handleElectionTimeoutCbh = CallbackHandle();
    _handleElectionTimeoutWhen = Date_t();
}

//僅支持3.2後的V1版本
if (!isV1ElectionProtocol()) {
    return;
}
//僅備節點可執行
if (!_memberState.secondary()) {
    return;
}
...
//是否能夠選舉
if (!_rsConfig.getMemberAt(_selfIndex).isElectable()) {
    return;
}

//檢測週期,由 electionTimeout + randomOffset
//randomOffset是隨機偏移量,默認爲 0~0.15*ElectionTimeoutPeriod = 0~1.5s
Milliseconds randomOffset = _getRandomizedElectionOffset();
auto now = _replExecutor.now();
auto when = now + _rsConfig.getElectionTimeoutPeriod() + randomOffset;

LOG(4) &lt;&lt; &quot;Scheduling election timeout callback at &quot; &lt;&lt; when;
_handleElectionTimeoutWhen = when;

//觸發調度,時間爲 now + ElectionTimeoutPeriod + randomOffset
_handleElectionTimeoutCbh =
    _scheduleWorkAt(when,
                    stdx::bind(&amp;ReplicationCoordinatorImpl::_startElectSelfIfEligibleV1,
                               this,
                               StartElectionV1Reason::kElectionTimeout));

}

上面代碼展現了這個選舉定時器的邏輯,在每個檢測週期中,定時器都會嘗試執行超時回調,
而回調函數指向的是_startElectSelfIfEligibleV1,這裏面就實現了主動發起選舉的功能,
若是心跳響應成功,經過cancelAndRescheduleElectionTimeout調用將直接取消當次的超時回調(即不會發起選舉)
若是心跳響應遲遲不能成功,那麼定時器將被觸發,進而致使備節點發起選舉併成爲新的主節點!

同時,這個回調方法(產生選舉)被觸發必需要知足如下條件:

  1. 當前是備節點
  2. 當前節點具有選舉權限
  3. 在檢測週期內仍然沒有與主節點心跳成功

這其中的檢測週期略大於electionTimeout(10s),加入一個隨機偏移量後大約是10-11.5s內,猜想這樣的設計是爲了錯開多個備節點主動選舉的時間,提高成功率。
最後,將整個自動選舉切換的邏輯梳理後,以下圖所示:
圖片描述

圖-超時自動選舉

業務影響評估

副本集發生主備切換的狀況下,不會影響現有的讀操做,只會影響寫操做。 若是使用3.6及以上版本的驅動,能夠經過開啓retryWrite來下降影響。
可是若是主節點是屬於強制掉電,那麼整個 Failover 過程將會變長,極可能須要在Election定時器超時後才被副本集感知並恢復,這個時間窗口會在12s之內。
此外還須要考慮客戶端或mongos對於副本集角色的監視和感知行爲。但總之在問題恢復以前,對於原主節點的任何讀寫都會發生超時。
所以,對於極爲重要的業務,建議最好在業務層面作一些防禦策略,好比設計重試機制。

參考連接

https://docs.mongodb.com/manu...

https://www.percona.com/blog/...

https://www.percona.com/blog/...

本文來自MongoDB中文社區(mongoing-mongoing)
圖片描述關於zale唐卓章,華爲技術專家,多年互聯網研發/架設經驗,關注NOSQL 中間件高可用及彈性擴展,在分佈式系統架構性能優化方面有豐富的實踐經驗,目前從事物聯網平臺研發工做,致力於打造大容量高可用的物聯網服務。

相關文章
相關標籤/搜索