对使用 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分钟一次,跟 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 >, bool, bool, std::set, std::allocator >*) ()
#11 0x0000000000b07410 in mongo::MultiIndexBlock::doneInserting(std::set, std::allocator >*) ()
#12 0x0000000000b0797d in mongo::MultiIndexBlock::insertAllDocumentsInCollection(std::set, 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 操作。
这个问题主要跟文件系统机制相关,testdb 下创建了数万个集合,对应到 wiredtiger 的实现,会出现一个目录下数万个文件的情况(集合的每个索引也要对应一个文件),而从ext4文件系统层面上,在目录里创建文件,先要遍历整个目录下所有的文件项,文件越多效率越低。
上述问题通常的解决方法是『将扁平化的目录层次化』,对应到 mongodb,就是将数万个集合分散到多个 DB 里,具体方法如下。
MongoDB 使用 wiredtiger 引擎时,大量集合的场景(通常业务设计上是有问题的),可能会遇到很多未知的问题,毕竟这不属于常见的应用场景,官方在这方面的测试支持也会相对弱些,比如上述提到的2个问题,还有之前分享的一个集合太多无法同步的问题,建议大家使用 MongoDB 时,合理设计数据模型,避免踩不必要的坑。