MongoDB 创建大量集合测试问题

问题背景

对使用 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 >, bool, bool, std::set<mongo::RecordId, std::less, std::allocator >*) ()
#11 0x0000000000b07410 in mongo::MultiIndexBlock::doneInserting(std::set<mongo::RecordId, std::less, std::allocator >*) ()
#12 0x0000000000b0797d in mongo::MultiIndexBlock::insertAllDocumentsInCollection(std::set<mongo::RecordId, std::less, std::allocator >*) ()


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 时,合理设计数据模型,避免踩不必要的坑。

作者简介

张友东,阿里巴巴技术专家,主要关注分布式存储、Nosql数据库等技术领域,先后参与TFS(淘宝分布式文件系统)Redis云数据库等项目,目前主要从事MongoDB云数据库的研发工作,致力于让开发者用上最好的MongoDB云服务。

发表评论