閱讀792 返回首頁    go 技術社區[雲棲]


MongoDB Priamry 為何持續出現 oplog 全表掃描?

線上某 MongoDB 複製集實例(包含 Primary、Secondary、Hidden 3個節點 ),Primary 節點突然 IOPS 很高,調查後發現,其中 Hidden 處於 RECOVERING 狀態,同時 Priamry 上持續有一全表掃描 oplog 的操作,正是這個 oplog 的 COLLSCAN 導致IO很高。

2017-10-23T17:48:01.845+0800 I COMMAND  [conn8766752] query local.oplog.rs query: { ts: { $gte: Timestamp 1505624058000|95, $lte: Timestamp 1505624058000|95 } } planSummary: COLLSCAN cursorid:20808023597 ntoreturn:0 ntoskip:0 keysExamined:0 docsExamined:44669401 keyUpdates:0 writeConflicts:0 numYields:353599 nreturned:0 reslen:20 locks:{ Global: { acquireCount: { r: 707200 } }, Database: { acquireount: { r: 353600 }, acquireWaitCount: { r: 15 }, timeAcquiringMicros: { r: 3667 } }, oplog: { acquireCount: { r: 353600 } } } 935646ms

上述問題,初步一看有2個疑問

  1. Hidden 上最新的 oplog 在 Primary 節點上是存在的,為什麼 Hidden 會一直處於 RECOVERING 狀態無法恢複?
  2. 同步拉取 oplog 時,會走 oplogHack 的路徑,即快速根據oplog上次同步的位點定位到指點位置,這裏會走一個二分查找,而不是COLLSCAN,然後從這個位點不斷的tail oplog。既然有了這個優化,為什麼會出現掃描所有的記錄?

接下裏將結合 MongoDB 同步的細節實現來分析下上述問題產生的原因。

備如何選擇同步源?

MongoDB 複製集使用 oplog 來做主備同步,主將操作日誌寫入 oplog 集合,備從 oplog 集合不斷拉取並重放,來保持主備間數據一致。MongoDB 裏的 oplog 特殊集合擁有如下特性:

  1. 每條 oplog 都包含時間戳,按插入順序遞增,如果底層使用的KV存儲引擎,這個時間戳將作為oplog在KV引擎裏存儲的key,所以可以理解oplog在底層存儲就是按時間戳順序存儲的,所以在底層能快速根據ts找位置。
  2. oplog 集合沒有索引,它一般的使用模式是,備根據自己已經同步的時間戳,來定位到一個位置,然後從這個位置不斷 tail query oplog。針對這種應用模式,對於 local.oplog.rs.find({ts: {$gte: lastFetechOplogTs}}) 這樣的請求,會有特殊的oplogStartHack 的優化,先根據gte的查詢條件在底層引擎快速找到起始位置,然後從該位置繼續COLLSCAN。
  3. oplog 是一個 capped collection,即固定大小集合(默認為磁盤大小5%),當集合滿了時,會將最老插入的數據刪除。

2

選擇同步源,條件1:備上最新的oplog時間戳 >= 同步源上最舊的oplog時間戳

備在選擇同步源時,會根據 oplog 作為依據,如果自己最新的oplog,比同步源上最老的 oplog 還有舊,比如 secondaryNewest < PrimaryOldest ,則不能選擇 Primary 作為同步源,因為oplog不能銜接上。如上圖,Secondary1 可以選擇 Primary 作為同步源,Secondary2 不能選擇 Primary作為同步源,但可以選擇 Secondary1 作為同步源。

如果所有節點都不滿足上述條件,即認為找不到同步源,則節點會一直處於 RECOVERING 狀態,並會打印 too stale to catch up -- entering maintenance mode 之類的日誌,此時這個節點就隻能重新全量同步了(向該節點發送 resync 命令即可)。

選擇同步源,條件2:如果minvalid處於不一致狀態,則minvalid裏的時間戳在同步源上必須存在

local.replset.minvalid 是 MongoDB 裏的一個特殊集合,用於存儲節點同步的一致時間點,在備重放oplog、回滾數據的時候都會用到,正常情況下,這個集合裏包含一個ts字段,跟最新的oplog時間戳一致,即 { ts: lastOplogTimestamp }

  1. 當備拉取到一批 oplog 後,假設第一條和最後一條 oplog 的時間戳分別為 firstOplogTimestamp、lastOplogTimestamp,則備在重放之前,會先把 minvalid 更新為 { ts: lastOplogTimestamp, begin: firstOplogTimestamp},加了begin字段後就說明,當前處於一個不一致的狀態,等一批 oplog 全部重放完,備將 oplog 寫到本地,然後更新 minvalid 為{ ts: lastOplogTimestamp},此時又達到一致的狀態。
  2. 節點在ROLLBACK時,會將 minvalid 先更新為{ ts: lastOplogTimestampInSyncSource, begin: rollbackCommonPoint},標記為不一致的狀態,直到繼續同步後才會恢複為一致的狀態。比如

    主節點  A B C F G H
    備節點1 A B C F G 
    備節點2 A B C D E
    
    備節點就需要回滾到 CommonPoint C,如果根據主來回滾,則minvalid會被更新為 { ts: H, begin:C}` 
    

在選擇同步源時,如果 minvalid 裏包含 begin 字段,則說明它上次處於一個不一致的狀態,它必須先確認 ts 字段對應的時間戳(命名為 requiredOptime)在同步源上是否存在,主要目的是:

  1. 重放時,如果重放過程異常結束,重新去同步時,必須要找包含上次異常退出時oplog範圍的節點來同步
  2. ROLLBACK後選擇同步源,必須選擇包含ROLLBACK時參考節點對應的oplog範圍的節點來同步;如上例,備節點2回滾時,它的參考節點包含了H,則在接下來選擇同步源上,同步源一定要包含H才行。

為了確認 requireOptime 是否存在,備會發一個 ts: {$gte: requiredOptime, $lte: requiredOptime} 的請求來確認,這個請求會走到 oplogStartHack的路徑,先走一次二分查找,如果能找到(絕大部分情況),皆大歡喜,如果找不到,就會引發一次 oplog 集合的全表掃描,如果oplog集合很大,這個開銷非常大,而且會衝掉內存中的cache數據。

oplogStartHack 的本質

通過上麵的分析發現,如果 requiredOptime 在同步源上不存在,會引發同步源上的一次oplog全表掃描,這個主要跟oplog hack的實現機製相關。

對於oplog的查找操作,如果其包含一個 ts: {$gte: beginTimestamp} 的條件,則 MongoDB 會走 oplogStartHack 的優化,先從引擎層獲取到第一個滿足查詢條件的RecordId,然後把RecordId作為表掃描的參數。

  1. 如果底層引擎查找到了對應的點,oplogStartHack優化有效
  2. 如果底層引擎沒有沒有找到對應的點,RecordId會被設置為空值,對接下來的全表掃描不會有任何幫助。(注:個人認為,這裏作為一個優化,應該將RecordId設置為Max,讓接下裏的全表掃描不發生。)

     if (查詢滿足oplogStartHack的條件) { 
        startLoc = collection->getRecordStore()->oplogStartHack(txn, goal.getValue());  // 1. 將起始值傳到底層引擎,通過二分查找找到起始值對應的RecordId
     }
    
    // Build our collection scan...
    CollectionScanParams params;
    params.collection = collection;
    params.start = *startLoc;                               // 2. 將起始RecordId作為表掃描的參數
    params.direction = CollectionScanParams::FORWARD;
    params.tailable = cq->getParsed().isTailable();
    

總結

結合上述分析,當一致時間點對應的oplog在同步源上找不到時,會在同步源上觸發一次oplog的全表掃描。當主備之間頻繁的切換(比如線上的這個實例因為寫入負載調大,主備角色切換過很多次),會導致多次ROLLBACK發生,最後出現備上minvalid裏的一致時間點在同步源上找不到,引發了oplog的全表掃描。

如何避免上述問題?

  1. 上述問題一般很難遇到,而且隻有oplog集合大的時候影響才會很惡劣。
  2. 終極方法還是從代碼上修複,我會向官方提一個PR,在上述的場景不產生全表掃描,而是返回找不到記錄,徹底解決這個問題。

最後更新:2017-10-24 17:35:22

  上一篇:go  Elasticsearch 創始人 Shay Banon:讓數據自己說話
  下一篇:go  10月24日雲棲精選夜讀:2017杭州·雲棲大會完美收官 虛擬化平台精彩回顧