MongoDB 定位 oplog 必須全表掃描嗎?

MongoDB oplog (相似於 MySQL binlog) 記錄數據庫的全部修改操做,除了用於主備同步;oplog 還能玩出不少花樣,好比git

  1. 全量備份 + 增量備份全部的 oplog,就能實現 MongoDB 恢復到任意時間點的功能
  2. 經過 oplog,除了實現到備節點的同步,也能夠額外再往單獨的集羣同步數據(甚至是異構的數據庫),實現容災、多活等場景,好比阿里雲開源的 MongoShake 就能實現基於 oplog 的增量同步。
  3. MongoDB 3.6+ 版本對 oplog 進行了抽象,提供了 Change Stream 的接口,實際上就是能不斷訂閱數據庫的修改,基於這些修改能夠觸發一些自定義的事件。
  4. ......

總的來講,MongoDB 能夠經過 oplog 來跟生態對接,來實現數據的同步、遷移、恢復等能力。而在構建這些能力的時候,有一個通用的需求,就是工具或者應用須要有不斷拉取 oplog 的能力;這個過程一般是github

  1. 根據上次拉取的位點構建一個 cursor
  2. 不斷迭代 cursor 獲取新的 oplog

那麼問題來了,因爲 MongoDB oplog 自己沒有索引的,每次定位 oplog 的起點都須要進行全表掃描麼?mongodb

oplog 的實現細節

{ "ts" : Timestamp(1563950955, 2), "t" : NumberLong(1), "h" : NumberLong("-5936505825938726695"), "v" : 2, "op" : "i", "ns" : "test.coll", "ui" : UUID("020b51b7-15c2-4525-9c35-cd50f4db100d"), "wall" : ISODate("2019-07-24T06:49:15.903Z"), "o" : { "_id" : ObjectId("5d37ff6b204906ac17e28740"), "x" : 0 } }
{ "ts" : Timestamp(1563950955, 3), "t" : NumberLong(1), "h" : NumberLong("-1206874032147642463"), "v" : 2, "op" : "i", "ns" : "test.coll", "ui" : UUID("020b51b7-15c2-4525-9c35-cd50f4db100d"), "wall" : ISODate("2019-07-24T06:49:15.903Z"), "o" : { "_id" : ObjectId("5d37ff6b204906ac17e28741"), "x" : 1 } }
{ "ts" : Timestamp(1563950955, 4), "t" : NumberLong(1), "h" : NumberLong("1059466947856398068"), "v" : 2, "op" : "i", "ns" : "test.coll", "ui" : UUID("020b51b7-15c2-4525-9c35-cd50f4db100d"), "wall" : ISODate("2019-07-24T06:49:15.913Z"), "o" : { "_id" : ObjectId("5d37ff6b204906ac17e28742"), "x" : 2 } }

上面是 MongoDB oplog 的示例,oplog MongoDB 也是一個集合,但與普通集合不同數據庫

  1. oplog 是一個 capped collection,但超過配置大小後,就會刪除最老插入的數據
  2. oplog 集合沒有 id 字段,ts 能夠做爲 oplog 的惟一標識; oplog 集合的數據自己是按 ts 順序組織的
  3. oplog 沒有任何索引字段,一般要找到某條 oplog 要走全表掃描

咱們在拉取 oplog 時,第一次從頭開始拉取,而後每次拉取使用完,會記錄最後一條 oplog 的ts字段;若是應用發生重啓,這時須要根據上次拉取的 ts 字段,先找到拉取的起點,而後繼續遍歷。app

oplogHack 優化

注:如下實現針對 WiredTiger 存儲引擎,須要 MongoDB 3.0+ 版本才能支持工具

若是 MongoDB 底層使用的是 WiredTiger 存儲引擎,在存儲 oplog 時,實際上作過優化。MongoDB 會將 ts 字段做爲 key,oplog 的內容做爲 value,將key-value 存儲到 WiredTiger 引擎裏,WiredTiger 默認配置使用 btree 存儲,因此 oplog 的數據在 WT 裏實際上也是按 ts 字段順序存儲的,既然是順序存儲,那就有二分查找優化的空間。優化

MongoDB find 命令提供了一個選項,專門用於優化 oplog 定位。ui

大體意思是,若是你find的集合是oplog,查找條件是針對 ts 字段的 gtegteq ,那麼 MongoDB 字段會進行優化,經過二分查找快速定位到起點; 備節點同步拉取oplog時,實際上就帶了這個選項,這樣備節點每次重啓,都能根據上次同步的位點,快速找到同步起點,而後持續保持同步。阿里雲

oplogHack 實現

因爲諮詢問題的同窗對內部實現感興趣,這裏簡單的把重點列出來,要深入理解,仍是得深刻擼細節。url

// src/monogo/db/query/get_executor.cpp
StatusWith<unique_ptr<PlanExecutor>> getExecutorFind(OperationContext* txn,
                                                     Collection* collection,
                                                     const NamespaceString& nss,
                                                     unique_ptr<CanonicalQuery> canonicalQuery,
                                                     PlanExecutor::YieldPolicy yieldPolicy) {
    // 構建 find 執行計劃時,若是發現有 oplogReplay 選項,則走優化路徑
    if (NULL != collection && canonicalQuery->getQueryRequest().isOplogReplay()) {
        return getOplogStartHack(txn, collection, std::move(canonicalQuery));
    }

   ...
   
    return getExecutor(
        txn, collection, std::move(canonicalQuery), PlanExecutor::YIELD_AUTO, options);
}
StatusWith<unique_ptr<PlanExecutor>> getOplogStartHack(OperationContext* txn,
                                                   Collection* collection,
                                                   unique_ptr<CanonicalQuery> cq) {

    // See if the RecordStore supports the oplogStartHack
    // 若是底層引擎支持(WT支持,mmapv1不支持),根據查詢的ts,找到 startLoc
    const BSONElement tsElem = extractOplogTsOptime(tsExpr);
    if (tsElem.type() == bsonTimestamp) {
        StatusWith<RecordId> goal = oploghack::keyForOptime(tsElem.timestamp());
        if (goal.isOK()) {
            // 最終調用 src/mongo/db/storage/wiredtiger/wiredtiger_record_store.cpp::oplogStartHack
            startLoc = collection->getRecordStore()->oplogStartHack(txn, goal.getValue());
        }
    }
    
     // Build our collection scan...
     // 構建全表掃描參數時,帶上 startLoc,真正執行是會快速定位到這個點
    CollectionScanParams params;
    params.collection = collection;
    params.start = *startLoc;
    params.direction = CollectionScanParams::FORWARD;
    params.tailable = cq->getQueryRequest().isTailable();
}

 

本文做者:張友東

原文連接

本文爲雲棲社區原創內容,未經容許不得轉載。 

相關文章
相關標籤/搜索