`
marlonyao
  • 浏览: 248795 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

Mongodb内嵌文档插入性能评测

阅读更多
Mongodb作为典型的文档数据库,支持内嵌文档和复杂的查询,这给数据库建模带来了更多的灵活性。在一个博客应用中,有博客(Blog)和评论(Comment),每篇博客可以有多条评论。在关系数据库建模中,通常博客和评论分别对应一张表,评论表有到博客表的外键。在MongoDB中,也可以像关系型数据库那样,将博客和评论分别放到不同的集合中,另外也可以选择将评论嵌入到博客文档中。对于后者,一个博客的数据结构可能像这样:

> db.blog.findOne()
{
	_id: 1,
	title: "No Free Lunch",
	author: "Alex",
	comments: [
		{ who: "John", comment: "I agree" },
		{ who: "Felix", comment: "You must be joking..." },
	]
}


这种方式的好处显而易见,它更加符合对象模型,也可以利用MongoDB的事务,因为MongoDB并不支持跨文档的事务。当然这样做也有坏处,最明显的缺陷在于MongoDB限制单个文档大小最大只能为16M。若每条评论大小以1K计,那么一个文档最多可容纳16K的评论,这个数字实在不小,中国最著名的博主韩寒,去掉被和谐的博客和评论,评论数最多的博客也只有不到6万条评论。而另一个可能的问题是性能,在文档中插入内嵌文档的性能如何,这是我主要关注的问题。

为了考虑文档大小对内嵌文档插入速度的影响,分别考虑每篇博客有1条、10条、100条、1000条、10000条评论的情况。每种情况均插入1万条评论,博客只有1条评论时同时插入1万个博客文档,有10条评论时同时插入1000个博客文档,有100条评论时同时插入100个博客文档,依此类推。另外为了对比内嵌文档插入和独立文档插入的速度,我也需要记录在独立集合中插入1万条评论的时间。MongoDB同时支持异步插入和同步插入,为了排除它的影响,两种情况我都作了测试。

测试代码如下,使用python,每条评论大小为1K。insert_embbded_comments是内嵌文档插入,参数n是每篇博客的评论数目,comment_len是每条评论的长度,这里总为1000,count为插入的博客数目,count*n总为10000,safe表示是否为同步插入,函数返回结果为执行时间。insert_comments和insert_embbded_comments基本一样,只是在独立集合中插入评论。

from pymongo import Connection
import sys, time

# 目的:测试Mongo中内嵌文档的插入速度
conn = Connection()
db = conn.bench

conn.drop_database('bench')
def insert_embbded_comments(n, comment_len, count=1, safe=False):
    comment_text = 'a'*comment_len
    start = time.time()
    for c in xrange(count):
        blog = {'_id': c, 'title': 'Mongodb Benchmark'}
        db.blog.insert(blog)
        for i in xrange(n):
            db.blog.update({'_id': c}, {'$push': { 'comments': {'comment': comment_text}}}, safe=safe)
    end = time.time()
    return end - start

def insert_comments(n, comment_len, count=1, safe=False):
    comment_text = 'a'*comment_len
    start = time.time()
    for c in xrange(count):
        for i in xrange(n):
            db.blog.comments.insert({'comment': comment_text}, safe=safe)
    end = time.time()
    return end - start

def bench(safe=False):
    total = 10000
    print '===== %sINSERT %s comments =====' % ('SAFE ' if safe else '', total)
    print '%12s %15s %15s %15s %15s %15s' % ('', '1(x10000)', '10(x1000)', '100(x100)', '1000(x10)', '10000(x1)')

    sys.stdout.write('%12s ' % 'Embeded')
    sys.stdout.flush()
    row_types = (1, 10, 100, 1000, 10000)
    for nrows in row_types:
        conn.drop_database('bench')
        count = total / nrows
        time = insert_embbded_comments(nrows, 1000, count=count, safe=safe)
        sys.stdout.write('%15s%s' % (time, '\n' if nrows==row_types[-1] else ' '))
        sys.stdout.flush()
    sys.stdout.write('%12s ' % 'Non-embeded')
    for nrows in row_types:
        count = total / nrows
        conn.drop_database('bench')
        time = insert_comments(nrows, 1000, count=count, safe=safe)
        sys.stdout.write('%15s%s' % (time, '\n' if nrows==row_types[-1] else ' '))
        sys.stdout.flush()

bench()
bench(safe=True)


在我的笔记本(Ubuntu10.04, MongoDB1.8)上运行結果:
===== INSERT 10000 comments =====
                   1(x10000)       10(x1000)       100(x100)       1000(x10)       10000(x1)
     Embeded   2.31141519547   1.42457890511   1.34223604202    4.3767850399   35.7308151722
 Non-embeded   1.29936504364   1.30167293549   1.30044412613   1.29023313522   1.29240202904
===== SAFE INSERT 10000 comments =====
                   1(x10000)       10(x1000)       100(x100)       1000(x10)       10000(x1)
     Embeded   5.45804405212   4.29802298546   4.95570802689   13.7657668591   107.089906216
 Non-embeded   3.68912506104   3.65784692764   3.77990913391   3.66531991959   3.70736408234


前部分是异步插入,后部分是同步插入。标有Embeded的那一行是插入内嵌评论文档的执行时间,第一列,即标有1(x10000)的列,每篇博客有1条评论,共插入1万篇博客。第二列,即标有10(x1000)的列,每篇博客有10条评论,共插入1000篇博客。第三列,每篇博客有100条评论,共插入100篇博客。最后一列,每篇博客有1万条评论,只插入1篇博客。标有Non-embeded的行是是插入独立评论文档的执行结果,都是插入10000条评论,这一行执行时间基本相同。

可以发现,异步插入时,当嵌入的评论数目比较少时(不多于100时),内嵌插入速度与在独立插入速度基本相同,多出来的时间可能是因为插入了额外的博客文档所致。嵌入评论数目为1时的执行时间几乎是嵌入评论数目为10时的两倍,这是因为前者比后者多插入了9000条博客文档。可是当嵌入评论数目达到1000之后,插入速度慢了2.3倍,嵌入评论数目为10000时,内嵌插入比独立插入慢了26.5倍。同步插入时情况基本相同,只是所有速度都慢了将近3倍。

在运行测试程序的同时运行mongostat可以观察到更多MongoDB的运行细节。下面是当嵌入评论数目为10000时执行内嵌插入时,mongostat的输出结果:
insert  query update delete getmore command flushes mapped  vsize    res faults locked % idx miss %     qr|qw   ar|aw  netIn netOut  conn       time 
     0      0     11      0       0      12       0   128m   242m    59m      0     86.9          0       0|0     0|1    12k     2k     2   20:36:23 
     0      0     10      0       0      11       0   128m   242m    56m      0      110          0       0|0     0|1    11k     2k     2   20:36:24 
     0      0      7      0       0       8       0   128m   242m    59m      0     80.9          0       0|0     0|1     8k     1k     2   20:36:25 
     0      0      7      0       0       8       0   128m   242m    59m      0      111          0       0|0     0|1     8k     1k     2   20:36:26 
     0      0     32      0       0      33       0   128m   242m    56m      0      104          0       0|0     0|1    37k     4k     2   20:36:27 
     0      0     54      0       0      55       1   128m   242m    56m      0     96.8          0       0|0     0|1    62k     6k     2   20:36:28 
     0      0     54      0       0      55       0   128m   243m    52m      0     97.3          0       0|0     0|1    62k     6k     2   20:36:29 
     0      0     53      0       0      54       0   128m   243m    60m      0     95.9          0       0|0     0|1    61k     6k     2   20:36:30 
     0      0     53      0       0      54       0   128m   243m    60m      0     96.9          0       0|0     0|1    61k     6k     2   20:36:31 
     0      0     53      0       0      54       0   128m   243m    60m      0     97.2          0       0|0     0|1    61k     6k     2   20:36:32

下面是执行独立插入时mongostat的输出结果:
insert  query update delete getmore command flushes mapped  vsize    res faults locked % idx miss %     qr|qw   ar|aw  netIn netOut  conn       time 
  2582      0      0      0       0    2584       0    32m   136m    22m      5     10.2          0       0|0     0|0     2m   215k     2   20:36:53 
  2746      0      0      0       0    2747       0    32m   136m    25m      1      7.5          0       0|0     0|0     3m   229k     2   20:36:54 
  2728      0      0      0       0    2729       0    32m   136m    28m      4      7.6          0       0|0     0|0     3m   227k     2   20:36:55 
  2713      0      0      0       0    2714       0    32m   136m    30m      2      7.5          0       0|0     0|0     3m   226k     2   20:36:56 
  2618      0      0      0       0    2620       0    32m   136m    23m      4     10.2          0       0|0     0|0     2m   218k     2   20:36:57 
  2756      0      0      0       0    2757       0    32m   136m    26m      2      7.6          0       0|0     0|0     3m   229k     2   20:36:58 
  2711      0      0      0       0    2712       0    32m   136m    28m      4      7.4          0       0|0     0|0     3m   226k     2   20:36:59 
  2417      0      0      0       0    2418       0    32m   136m    31m      1      6.6          0       0|0     0|0     2m   201k     1   20:37:00 


首先注意在内嵌插入时锁占用率(locked %栏)达到甚至100%,而独立插入时锁占用率只有大概10%。我注意到,在测试时异步内嵌插入时,当输出10000(x1)列结果后,大概要等1分钟时间才看到输出Non-embeded,这可能是因为Mongodb此时还在持有锁,drop_database操作需要等待插入操作执行完成。另外注意内嵌插入时faults栏一直为0,表示不存在换页,而独立插入时存在换页。在我们的例子中文档大小大约是10M(10000x1K),这么大的文档,不换页并不正常,这说明MongoDB更新文档时总是将整个文档加载到内存,即使只更新部分内容(例如只插入一条评论时),也不能只将部分文档加载到内存,而将其它部分换出到磁盘。从mapped和vsize列可以看出,内嵌插入时要比独立插入时需要更多内存,这是因为独立插入时可以将其它文档暂时换出到磁盘上,降低内存占用,这也是为什么独立插入时faults列不为0的原因。

结论:在MongoDB不宜在文档中内嵌许多子文档。对于像博客这样的应用,将大量评论嵌入到博客文档中会严重影响性能,这是因为MongoDB更新文档时总是将整个文档加载到内存,即使只更新部分内容也是如此。另外,这里并没有考虑随机插入的情况,插入内嵌评论时,如果博客文档原来预留空间不足,将导致原有文档删除,并在数据库文件末尾分配新的文档,这也会劣化内嵌文档的插入性能,还会在数据库中文件中留下大量空洞。
分享到:
评论
2 楼 yang_44 2012-03-11  
请教一下,在一些频繁更改状态的数据中,比如一个任务有执行状态,如果使用内嵌文档的方式记录状态值,每个任务也只保留一条状态文档,代表当前状态,是不是比直接update更新文档的键值快呢?
1 楼 violetluna 2012-02-03  
非常好! 

相关推荐

Global site tag (gtag.js) - Google Analytics