792
技術社區[雲棲]
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個疑問
- Hidden 上最新的 oplog 在 Primary 節點上是存在的,為什麼 Hidden 會一直處於 RECOVERING 狀態無法恢複?
- 同步拉取 oplog 時,會走 oplogHack 的路徑,即快速根據oplog上次同步的位點定位到指點位置,這裏會走一個二分查找,而不是COLLSCAN,然後從這個位點不斷的tail oplog。既然有了這個優化,為什麼會出現掃描所有的記錄?
接下裏將結合 MongoDB 同步的細節實現來分析下上述問題產生的原因。
備如何選擇同步源?
MongoDB 複製集使用 oplog 來做主備同步,主將操作日誌寫入 oplog 集合,備從 oplog 集合不斷拉取並重放,來保持主備間數據一致。MongoDB 裏的 oplog 特殊集合擁有如下特性:
- 每條 oplog 都包含時間戳,按插入順序遞增,如果底層使用的KV存儲引擎,這個時間戳將作為oplog在KV引擎裏存儲的key,所以可以理解oplog在底層存儲就是按時間戳順序存儲的,所以在底層能快速根據ts找位置。
- oplog 集合沒有索引,它一般的使用模式是,備根據自己已經同步的時間戳,來定位到一個位置,然後從這個位置不斷 tail query oplog。針對這種應用模式,對於 local.oplog.rs.find({ts: {$gte: lastFetechOplogTs}}) 這樣的請求,會有特殊的oplogStartHack 的優化,先根據gte的查詢條件在底層引擎快速找到起始位置,然後從該位置繼續COLLSCAN。
- oplog 是一個 capped collection,即固定大小集合(默認為磁盤大小5%),當集合滿了時,會將最老插入的數據刪除。
選擇同步源,條件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 }
。
- 當備拉取到一批 oplog 後,假設第一條和最後一條 oplog 的時間戳分別為 firstOplogTimestamp、lastOplogTimestamp,則備在重放之前,會先把 minvalid 更新為
{ ts: lastOplogTimestamp, begin: firstOplogTimestamp}
,加了begin字段後就說明,當前處於一個不一致的狀態,等一批 oplog 全部重放完,備將 oplog 寫到本地,然後更新 minvalid 為{ ts: lastOplogTimestamp}
,此時又達到一致的狀態。 -
節點在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)在同步源上是否存在,主要目的是:
- 重放時,如果重放過程異常結束,重新去同步時,必須要找包含上次異常退出時oplog範圍的節點來同步
- 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作為表掃描的參數。
- 如果底層引擎查找到了對應的點,oplogStartHack優化有效
-
如果底層引擎沒有沒有找到對應的點,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的全表掃描。
如何避免上述問題?
- 上述問題一般很難遇到,而且隻有oplog集合大的時候影響才會很惡劣。
- 終極方法還是從代碼上修複,我會向官方提一個PR,在上述的場景不產生全表掃描,而是返回找不到記錄,徹底解決這個問題。
最後更新:2017-10-24 17:35:22