1. 程式人生 > >MongoDB 定位 oplog 必須全表掃描嗎?

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

MongoDB oplog (類似於 MySQL binlog) 記錄資料庫的所有修改操作,除了用於主備同步;oplog 還能玩出很多花樣,比如

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

總的來說,MongoDB 可以通過 oplog 來跟生態對接,來實現資料的同步、遷移、恢復等能力。而在構建這些能力的時候,有一個通用的需求,就是工具或者應用需要有不斷拉取 oplog 的能力;這個過程通常是

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

那麼問題來了,由於 MongoDB oplog 本身沒有索引的,每次定位 oplog 的起點都需要進行全表掃描麼?

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 欄位,先找到拉取的起點,然後繼續遍歷。

oplogHack 優化

注:以下實現針對 WiredTiger 儲存引擎,需要 MongoDB 3.0+ 版本才能支援

如果 MongoDB 底層使用的是 WiredTiger 儲存引擎,在儲存 oplog 時,實際上做過優化。MongoDB 會將 ts 欄位作為 key,oplog 的內容作為 value,將key-value 儲存到 WiredTiger 引擎裡,WiredTiger 預設配置使用 btree 儲存,所以 oplog 的資料在 WT 裡實際上也是按 ts 欄位順序儲存的,既然是順序儲存,那就有二分查詢優化的空間。

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

大致意思是,如果你find的集合是oplog,查詢條件是針對 ts 欄位的 gtegteq ,那麼 MongoDB 欄位會進行優化,通過二分查詢快速定位到起點; 備節點同步拉取oplog時,實際上就帶了這個選項,這樣備節點每次重啟,都能根據上次同步的位點,快速找到同步起點,然後持續保持同步。

oplogHack 實現

由於諮詢問題的同學對內部實現感興趣,這裡簡單的把重點列出來,要深刻理解,還是得深入擼細節。

// 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();
}

 


本文作者:張友東

原文連結

本文為雲棲社群原創內容,未經