以前的项目大都把数据存放在关系型数据库中,关系型数据库的优势在于使用普及,资料丰富,且有大量辅助类库来简化开发。当然它们的问题比较明显的,一是在数据量上升的情况下伸缩性比较差,且进行结构调整的代价比较高。因此现在有个所谓NoSQL的“运动”也逐渐普遍起来了,它便是借助一些非关系型存储方式来开发项目(个人认为其实将它解释为Not Only SQL更为合适)。因此在新项目里,我也想尝试一下使用之前一直只是“听说”的存储方式。
在和同事的交流过程中,我了解到他们的项目正在尝试使用Tokyo Tyrant(后称TT)进行存储,并且据说效果不错,因此我一开始也打算尝试使用TT进行主要存储,为此也花了一定时间为其编写.NET平台下的驱动程序。不过在驱动程序的开发过程中,我逐渐意识到TT的功能有着严重的限制,似乎并不适合作为接下来项目的主要存储方式。因此,我又将眼光转向了之前关注过的MongoDB上。MongoDB也是NoSQL的代表之一,是一个面向文档的,架构灵活(Schema-less)的存储方式,在仔细阅读相关资料之后,我发现它的功能与TT相比可谓天上地下,非常适合许搭建各类项目(关于这点以后有机会再谈)。
不过,既然选择NoSQL的原因是性能,那么他们的性能表现究竟如何呢?TT的性能表现在业界非常出名,而MongoDB的使用便相对较少了(当然,官网列举了不少案例,最近著名的开源网站SourceForge也打算使用MongoDB重新设计他们的网站)。为此,我决定亲手比较一下两者的性能表现。
测试环境及数据
由于缺乏合适的环境,因此我不得不在我的MBP上进行这次性能比较,参数如下:
- OS:Mac OS X v10.6.2(Snow Leopard)
- RAM:4GB / 1067MHz / DDR 3
- CPU:2.53GHz Intel Core 2 Duo
- 64-bit Kernel and Extensions:Yes
- TT在64 bit环境下编译,MongoDB使用64 bit版本。
在测试执行时,我尽量关闭多余的应用程序,避免其它因素造成影响。同样,由于条件限制,我只得把客户端和服务器跑在同一台机器上。测试代码使用Ruby编写,这是由于两者都有官方提供的驱动程序。此外,由于我对于两者的优化都不太熟悉,因此它们都使用了默认的配置。
关于测试数据,我将存入110万条“新闻”数据,包含以下字段:
- ID:标识,32位整数,带索引
- Title:标题,字符串
- CategoryID:分类ID,32位整数,带索引
- CreateTime:日期,保存为32位整数,带索引
- UserID:用户ID,32位整数,带索引
- Tags:标签集合,字符串数组,带索引
- Source:来源,字符串
- SourceUrl:来源URL,字符串
- Status:状态,32位整数
为了相对接近真实的环境的数据分布特征(便于以后进行查询操作比较),我设计了这样的测试数据(具体可阅读代码):
- 2万个分类,分别拥有10个至100条新闻,总共110万条记录。
- 1万个用户,根据分类id取模得到其归属。
- 创建时间从2010年1月1日起递增,每条记录增加1秒。
- 每条记录拥有2到6个Tag,除了其中一个之外,都从一个1.7万个Tag库中获取。
- 每条记录的字符串字段都不长(20-30字符)
由于TT只支持字符串的值(但可以建立数字索引,会将字符串当作数字来识别),因此事实上所有的值都会转化为字符串进行保存。此外,由于存在“根据Tag查找新闻”的业务,因此对于Tags字段也建立了索引,其中MongoDB直接支持对字符串数组的索引(索引其中每个元素),而在存入TT时则转化为空格分隔的字符串,并为其建立倒排索引(Token Inverted Index)以便进行全文查找。
在存储方式上,所有数据将放入MongoDB的同一个集合内,而TT则选用Table Database存储结构。在使用TT时,另一种做法是完全使用键值对来保存一条记录的各个字段,不过这样做会大大限制TT的功能,或是会为系统编写带来额外的复杂度,便不考虑Hash / B+ Tree / Fixed-length等存储方式了。
插入操作性能比较
具体代码在tt-insert.rb及mdb-insert.rb文件中。两段代码均使用一个连接,使用循环每次插入一条:由于如果每次都建立连接,会在TCP/IP连接的打开关闭上消耗大部分时间,由于实际项目中基本都会使用连接池等机制来复用连接,因此这方面便不多做考虑;再者,由于实际插入时几乎不可能批量操作,因此这里并不使用两者提供的批量插入功能。脚本每插入10万条记录便打印出所耗时间,结果如下:
从结果上看,MongoDB大约有10%的领先,不过总体来说两者的差距不大。值得一提的是,TT在数据量越大的情况下,每插入10万条记录的耗时越长(也从同事的使用过程中确认了这一点)。因此,一开始两者插入速度几乎没有差距,但是慢慢地TT便落后于MongoDB了。目前推测这可能是TT的Table Database存储结构特有的问题,不知道随着数据量的增长TT表现如何,因为对于一个大型系统来说,100多万条记录实际上只是一个很小的数目。
有同事推测,TT插入性能低是因为建立了Tags字段的倒排索引,于是我也测试了不在Tags字段上建索引的情况。令人惊讶的是,同样去除Tags字段的索引之后,MongoDB的性能提升超过20%,而TT的性能提升却微乎其微。
值得一提的是,放入相同的记录之后,TT的文件为400多M,但MongoDB则为整整2G。因此推测这是MongoDB进行空间预分配的结果,邮件列表上的这个讨论也证实了这一点。此外,上面每次测试都是从零开始的,经测试如果在插入前为MongoDB预分配文件空间,则性能还会有些许提高。
通过主键获取记录性能比较
具体代码在tt-get-by-key.rb及mdb-get-by-key.rb文件中。两段代码同样均使用一个连接,使用循环进行100万次Get操作,每次都随机获取一个110万以内的数字,并作为ID进行Get操作。同样,每10万次Get操作后打印出所耗时间,结果如下:
在Get操作方面,MongoDB有大约20%的领先。不过从实际情况分析,这方面的差距也不是太大,因为在项目中根据主键获取记录的操作8成以上都是落在缓存上的,因此在这方面对存储的要求并不高。
通过主键更新记录性能比较
具体代码在tt-update-by-key.rb及mdb-update-by-key.rb文件中。两段代码各自使用同一个连接,使用循环进行大量的更新操作。每次随机获取一个110万以内的数字,并作为ID更新其CreateTime、Title、Source、SourceUrl、Status五个字段。结果如下:
首先是MongoDB的三次测试结果,每次更新100万条数据。从数据上看,三次查询的性能越来越高,这个现象在重启服务器之后可以重现(三次测试之间并没有重启服务器)。
请注意,TT的三次测试均只更新了10万条记录(因为100万条记录耗时太长)。和MongoDB类似,一开始查询性能较差,但是会慢慢提高,这个现象在重启服务器后也能重现,于是猜测是缓存的结果。即便如此,TT的更新时的最高速度只有大约没秒1500次,而MongoDB却超过了每秒5000次。这是因为TT在功能上有硬伤:它无法像SQL的UPDATE语句那样更新某条记录的部分字段,因此必须全部取出,修改后再整体写入。而MongoDB支持高效的直接修改——这也成为MongoDB放在首页上的“招牌功能”。
因此,TT对于“更新某个分类下所有新闻的状态”这样的操作会很不适应,而它的这个限制导致我们很难实现如“乐观并发控制”这样的手段。此外,在实际应用中客户端与服务器不会使用本地连接,因此在生产环境中TT和MongoDB在这方面的差距可能会更明显一些。
总结
到目前为止,我们测试了TT和MongoDB在CRU三种基本操作下的性能,似乎MongoDB全面胜过Tokyo Tyrant。不过需要强调的是,这个测试还很不全面:它没有使用合适的环境,也没有对两者进行适当的优化。此外,两者在并发情况下,或是同时读写下的表现如何也是值得进一步考察的,我也会设计更多的测试用例。因此,这里的数据仅供参考,如果有合适的环境我也会重新进行测试。