MongoDB建立大量集合測試問題

張友東發表於2016-07-29

問題背景

對使用 wiredtiger 引擎的 mongod 進行如下測試,不斷的『建立集合、建立索引,插入一條記錄』,然後統計這3個動作的耗時。

var db = db.getSiblingDB("testdb");
for (var i = 0; i < 100000; i++) {
    var start = (new Date()).getTime();
    var collName = "test" + i;
    var doc = {name: "name" +i, seq: i};
    db.createCollection(collName);        // 建立集合
    db[collName].createIndex({name: 1});  // 建立索引
    db[collName].insert(doc);             // 插入一條記錄
    var end = (new Date()).getTime();     // 統計耗時
    print("cost: " + (end - start));
}

隨著集合數越來越多,測試過程中發現2個問題

  1. 偶爾會出現耗時很長的請求(1s、2s、3s..不斷上升),統計了下頻率,大約1分鐘左右出現一次。
  2. 平均耗時不斷增加,從最開始平均10ms 不到,一直到20ms、30ms、40ms…

測試問題1

因為耗時很長的請求頻率大概1分鐘一次,跟 wiredtiger 預設的60scheckpoint 很接近,懷疑問題跟 checkpoint 有關,從執行慢日誌看,耗時長是因為 createIndex 的原因。

通過當時的 pstack 發現,建立索引的執行緒正在等鎖,只有 checkpoint 執行緒在幹活

Thread 4 (Thread 0x7f80c3c72700 (LWP 70891)):
#0  0x00007f80c2ddc054 in __lll_lock_wait () from /lib64/libpthread.so.0
#1  0x00007f80c2dd7388 in _L_lock_854 () from /lib64/libpthread.so.0
#2  0x00007f80c2dd7257 in pthread_mutex_lock () from /lib64/libpthread.so.0
#3  0x00000000019f3f95 in __wt_curfile_open ()
#4  0x0000000001a580a5 in __session_open_cursor_int ()
#5  0x0000000001a09e13 in __wt_curtable_open ()
#6  0x0000000001a57f29 in __session_open_cursor_int ()
#7  0x0000000001a584b9 in __session_open_cursor ()
#8  0x000000000108cfe9 in mongo::WiredTigerIndex::BulkBuilder::openBulkCursor(mongo::WiredTigerIndex*) ()
#9  0x000000000108841e in mongo::WiredTigerIndexStandard::getBulkBuilder(mongo::OperationContext*, bool) ()
#10 0x0000000000cb09e9 in mongo::IndexAccessMethod::commitBulk(mongo::OperationContext*, std::unique_ptr<mongo::IndexAccessMethod::BulkBuilder, std::default_delete<mongo::IndexAccessMethod::BulkBuilder> >, bool, bool, std::set<mongo::RecordId, std::less<mongo::RecordId>, std::allocator<mongo::RecordId> >*) ()
#11 0x0000000000b07410 in mongo::MultiIndexBlock::doneInserting(std::set<mongo::RecordId, std::less<mongo::RecordId>, std::allocator<mongo::RecordId> >*) ()
#12 0x0000000000b0797d in mongo::MultiIndexBlock::insertAllDocumentsInCollection(std::set<mongo::RecordId, std::less<mongo::RecordId>, std::allocator<mongo::RecordId> >*) ()


Thread 68 (Thread 0x7f80b9336700 (LWP 37085)):
#0  0x00000000019db9e0 in __config_next ()
#1  0x00000000019dc106 in __config_getraw.isra.0 ()
#2  0x00000000019dc5a6 in __wt_config_getones ()
#3  0x0000000001a2437d in __wt_meta_ckptlist_get ()
#4  0x0000000001a65218 in __checkpoint_worker.isra.10 ()
#5  0x0000000001a64888 in __checkpoint_apply ()
#6  0x0000000001a6657a in __txn_checkpoint ()
#7  0x0000000001a66e17 in __wt_txn_checkpoint ()
#8  0x0000000001a57854 in __session_checkpoint ()
#9  0x00000000019e4f8f in __ckpt_server ()
#10 0x00007f80c2dd5851 in start_thread () from /lib64/libpthread.so.0
#11 0x0000003403ee767d in clone () from /lib64/libc.so.6

為什麼建索引會跟 checkpoint 有衝突?分析索引程式碼發現,前臺建索引時,mongod 會使用 wiredtiger 的 bulk cursor,而openBulkCursor是要競爭 checkpoint 鎖的(個人理解是避免在 bulk insert 過程中出現 checkpoint),所以 createIndex 會阻塞等待 checkpoint 完成。

// src/cursor/cur_file.c:__wt_curfile_open
 /* Bulk handles require exclusive access. */
    if (bulk)
        LF_SET(WT_BTREE_BULK | WT_DHANDLE_EXCLUSIVE);

    /* Get the handle and lock it while the cursor is using it. */
    if (WT_PREFIX_MATCH(uri, "file:")) {
        /*
         * If we are opening exclusive, get the handle while holding
         * the checkpoint lock.  This prevents a bulk cursor open
         * failing with EBUSY due to a database-wide checkpoint.
         */
        if (LF_ISSET(WT_DHANDLE_EXCLUSIVE))
            WT_WITH_CHECKPOINT_LOCK(session, ret,
                ret = __wt_session_get_btree_ckpt(
                session, uri, cfg, flags));

另外從目前的實現看,後臺建索引時並不是 bulk cursor,而是使用普通的 cursor 逐條插入,故不會去競爭 checkpoint 的鎖,上述測試程式碼在createIndex 時加上{background: true}選項時問題解決。

建議使用者在建立索引時,儘量選擇後臺建索引的方式,可能效能上不如前臺方式,但後臺建索引對業務的影響是最小的(前臺建索引還會獲取 db 的寫鎖,導致 db 上的讀寫都被阻塞),最好的方式是 DDL 和 DML 分離,在業務程式碼中不要出現建索引、建集合的邏輯,預先建立好,業務只做CRUD 操作。

測試問題2

這個問題主要跟檔案系統機制相關,testdb 下建立了數萬個集合,對應到 wiredtiger 的實現,會出現一個目錄下數萬個檔案的情況(集合的每個索引也要對應一個檔案),而從ext4檔案系統層面上,在目錄裡建立檔案,先要遍歷整個目錄下所有的檔案項,檔案越多效率越低。

上述問題通常的解決方法是『將扁平化的目錄層次化』,對應到 mongodb,就是將數萬個集合分散到多個 DB 裡,具體方法如下。

  1. 配置 storage.directoryPerDB 選項為 true
  2. 業務上將集合分散到多個 DB 裡(如100個,平均每個目錄下就只有幾百個檔案)

總結

MongoDB 使用 wiredtiger 引擎時,大量集合的場景(通常業務設計上是有問題的),可能會遇到很多未知的問題,畢竟這不屬於常見的應用場景,官方在這方面的測試支援也會相對弱些,比如上述提到的2個問題,還有之前分享的一個集合太多無法同步的問題,建議大家使用 MongoDB 時,合理設計資料模型,避免踩不必要的坑。


相關文章