在上一次的测试中我们比较了MongoDB与Tokyo Tyrant的Table Database两种存储方式的性能。不过由于条件限制,我只能在自己的MBP上测试,而这至少会带来两个问题。首先,真实环境下客户端和服务器是通过内网连接的,它的性能比本地回环要慢不少,一些和网络传输性能有关的问题可能会体现不出。其次,由于无法进行并发测试(并发测试的客户端资源占用较高,放在同一台机器上准确性较差),这又和生产环境有很大区别了。因此,我前两天向同事借了台性能测试用的机器,希望可以得到更可靠的结果。
测试环境与数据
这次我使用了新的环境进行性能测试:
- OS:CentOS release 5.3 (Final)
- RAM:4GB
- CPU:Intel(R) Xeon(R) CPU E5405 @ 2.00GHz (64 bit, 4 cores * 2)
- 其他:SCSI硬盘,ext3文件系统
客户端与服务器端配置相同,两台机器使用百兆网相连,测试时服务器保持空闲。
测试数据的结构与之前相同(包括索引),数据分布同样保持一致,每个线程插入的数量较少,但每次测试的总数均不低于上次(110万条)。
为了测试并发插入的性能,我稍微改写了测试脚本。首先,我为它增加init参数,它的作用是初始化存储结构(清除数据,建立索引),拿MongoDB的测试脚本为例:
# ruby mdb-insert.rb init
index for CreateTime created.
index for CategoryID created.
index for UserID created.
index for Tags created.
initialize completed.
此外,脚本还支持“分类范围”的指定,例如:
# ruby mdb-insert.rb 101 200
这表示我们将插入CategoryID为101至200的新闻,由于每个分类中均匀分布10至100条记录,因此只要范围的上下限保持10的倍数,这样平均每个分类有55条新闻,于是新闻ID也可以此计算出来。例如分类101至200的新闻,其ID便为5501至11000。您可以通过阅读代码了解这些细节。
为了并行测试,每次我就将同时执行多个脚本进行插入,每个脚本提供不同的参数。我使用类似这样的shell脚本来启动并行任务:
#!/bin/bash
# mdb-insert.sh
echo "=== initialize ==="
ruby mdb-insert.rb init
for((i=1;i<=5;i++))
do
let begin=$((4000 * ($i - 1) + 1))
let end=$((4000 * $i))
echo === start task $i ===
ruby mdb-insert.rb $begin $end > logs/mdb-insert-$begin-$end.log &
done
这段脚本将启动5个任务,每个任务将插入4000个分类,即22万条记录。5个任务总共插入110万条记录,与前次持平。接下来的测试中,我在增加任务数量的同时也会适当降低每个任务插入的记录数目。最多我将启用100个任务,每个任务插入1000个分类,即所有任务共计插入550万条记录,是之前的5倍。
在所有的测试中,无论多少个任务都是同时启动,且几乎同时结束(与总耗时相比很小)。因此,在接下来的数据中我不会列出每个任务的开始及结束时间,如果您关心这部分数据,可以在文章结尾处可以获得本次测试生成的所有记录文件。
Tokyo Tyrant性能测试
第1次测试启动5个任务,每个任务4000个分类,共计插入110万条记录。结果如下(第1行粗体表示“万条记录”,每个数字的单位为“秒”,下同):
与之前的现象类似,当数据库中的数据越多时,插入速度也会随之减慢:在每个任务插入前1万条记录时,耗时大约为5、6秒。但是到了中后期,每插入1万条数据则需要等待15至20,甚至30秒的时间。值得注意的是,虽然使用了较好的服务器,但是并行插入110万条记录的时间却比上次要来的多(这次耗时340秒,而之前是)。原因可能是客户端与服务器端的分离导致网络速度的下降。另一种可能我猜测是,根据Tokyo Cabinet的文档中提到文件读写锁的使用,因此每次只能插入一条记录,且插入(或更新)时无法读取数据,因而在并发环境下Tokyo Tyrant(它其实是Tokyo Cabinet上层的TCP服务器)的表现并不会有所提高。不过据读过Tokyo Cabinet代码的同事说,Tokyo Cabint在使用Table Database的时候锁粒度不会那么大,因此关于这点还需要寻找进一步的资料。
第2次测试启动10个任务,每个任务还是4000个分类,因此共计插入220万条记录。结果如下:
第2次测试的总数据量为第1次的2倍,而耗时却是第1次的3.5倍,这应该还是由于数据量增大而导致的插入性能降低。但是,我们目前还不能排除这和并发连接数有关的可能——虽然我们有理由相信这个性能问题只和数据量相关(稍后再说)。为了查明并发程度和性能是否有关系,我们再进行第3次测试。
第3次测试启动20个任务,每个任务2000个分类,因此共计仍旧是220万条记录。结果如下:
这个结果实在是非常能够说明问题:第3次的耗时与上一次几乎完全相同,这意味着加大并发量并不会影响TT的性能。这是因为TT在服务器端维护了一个线程池(现在的测试,也是默认情况下为8个线程),因此请求再多,同时进行的任务也是有限的,而累积的任务会排在队列中。这也是使用队列和固定数量工作线程的好处:即使压力再大,服务器端的吞吐量也能够一直保持较高水准,而客户端请求的响应时间随着请求数量增大而线性增长。如果没有队列,随着压力增大,服务器端吞吐量会剧烈下降(一般至少也是线性的),而客户端请求的响应时间增长更快,直至超时。
为此,我们也没有必要继续测试更多的并发数量了,因为即便是再多并发,TT的吞吐量(即插入记录的数目)也只是和数据库中记录数量相关。根据测试,我们可以总结出:
- 插入110万条记录的平均吞吐量为:大约3225条/秒
- 插入220万条记录的平均吞吐量为:大约1833条/秒
- 当数据库包含100万条数据时吞吐量为:1700~1800条/秒
- 当数据库包含200万条数据时吞吐量为:150~160条/秒
似乎随着数据量的增加,TT的吞吐量下降得也非常明显。这其中可能有索引的因素在里面,因此如果您想要得到适合自己的结果,最好可以亲自进行试验。
MongoDB性能测试
与TT相同,第1次测试启动5个任务,每个任务4000个分类,共计插入110万条记录。结果如下:
在5个并发任务的情况下,MongoDB的表现较TT要出彩不少。首先,随着数据库中已有记录的增加,插入速度并没有降低的迹象,可以说十分平稳。此外,在同样的客户端和服务器环境下,MongoDB插入110万条数据的耗时比TT的一半还要略少一些。而且,虽然网速带来的一定负面影响,但可能是由于服务器配置的提高,再加上并发写入的性能优势,此次测试比上次单机环境下的表现也要好上许多。
那么在数据量和并发继续增大的情况下MongoDB表现如何呢?于是第2次测试启动10个任务,每个任务还是4000个分类,共计插入220万条记录。结果如下:
第2次的数据量为第1次的2倍,而耗时则大约为2.08倍,两者基本保持一致,这说明了MongoDB在数据量和并发量增加的情况下,吞吐量几乎没有改变。
第3次测试依旧与TT相同,启用20个任务,每个任务2000个分类,共计插入220万条记录。结果如下:
在总数据量不变的情况下,我们又将并发量提高了一倍,但是MongoDB依旧表现出的稳定的吞吐量,总耗时与第2次测试几乎完全相同。
这样看来,似乎在MongoDB在内部也有个类似于TT的队列机制以保证一定的吞吐量。为了验证这个看法,我再次加大了数据量和并发量。在第4次测试中我启用了50个任务,每个任务还是2000个分类,这样插入的记录数据则达到了550万。结果如下:
在50个并发任务下,MongoDB终于略显疲态。第4次测试的数据量为第3次的2.5倍,但耗时却接近3.5倍。为了验证这是由于数据量增加,还是并发量提高引起的问题,我又进行了第最后一次测试。
第5个测试中启用了100个任务,但每个任务只写入1000个分类,则总条目数还是和第4次相同,为550万。结果如下:
请注意,由于每个任务插入1000个分类,即5.5万条记录,因此上表中最后一栏为5.5而不是6。随着并发量的增大,MongoDB的耗时继续增加了。由于这个表现和之前的预测不同,我又把第4次和第5次测试各执行了一遍,结果并没有太大区别,基本可以排除“环境异常”这样的可能。
经过5次测试的结果,我们可以得出MongoDB的性能指标:
- 5个并发,插入110万条记录的平均吞吐量:大约6600条/秒
- 10个并发,插入220万条记录的平均吞吐量:大约6300条/秒
- 20个并发,插入220万条记录的平均吞吐量:大约6300条/秒
- 50个并发,插入550万条记录的平均吞吐量:大约4500条/秒
- 100个并发,插入550万条记录的平均吞吐量:大约4100条/秒
总体来说,虽然在数据量和并发量增大的情况下MongoDB的吞吐量有所下降,但是MongoDB的表现比TT还是要抢眼不少,而且在绝对数值上还是相当出彩的。
其他信息
其他还有一些信息可能值得参考。
在进行测试时,我发现TT进程的CPU占有率比较低,除了少量时候出现80~150%以外(top命令的结果,8核服务器),绝大部分时间CPU占有率只有20~40%。MongoDB的CPU占有率比TT要高出不少,基本上保持在80~110%之间(数据量和并发量在这里影响不大)。但是总体来说CPU的占有率都很低,毕竟现在的测试属于IO密集型操作。
不过在内存占有率方面,两者的区别很大。在测试过程中(服务器内存4G),TT进程的内存占有率一直保持在20%一下,而MongoDB会缓步增加,最后保持在80%上下。
此外,在插入了550万条数据之后,MongoDB的数据文件增长到了6G。这个数字比上次测试的2G/110万稍有缓解,但还是比TT要大上许多。
最后再来谈谈为什么仅仅测试了“并发插入”而没有“更新”和“删除”:其实这也是通过使用场景而设计出来的测试用例。因为对于此类存储来说,大量并发插入的场景比较多,例如日志记录,用户行为记录,或是SNS应用中常见的News Feed等等。这些场景的特点就是需要数据的不断增长,而大量密集的更新则很少会出现──删除就更不用说了。至于并行的更新,我接下来也会进行测试的,只是可能会模拟一些实际的场景,例如在更新的同时进行不断地读取和插入吧。
所有测试代码,及完整测试结果:http://github.com/JeffreyZhao/mdb-tt-benchmark