由于平时经常接触Google App Engine,所以对NoSQL也算比较关注。在设计网站时,总会不由自主地考虑使用NoSQL是否合适,而在给我的网站添加社交功能时,我也不禁想到了一个问题:Twitter为何会采用那么麻烦的NoSQL?

最早是看到 《Digg和Twitter都在迁移数据库到Cassandra》这篇文章才知道Twitter准备采用NoSQL。
用过Twitter的都知道,Twitter有timeline(时间线)这个概念。简单来说(忽略Retweet、DM等情况),你发的Tweet(包括reply等,下同)会出现在你的公开时间线上,而你的时间线相当于你和所有你关注的人(following)的公开时间线的并集。也就是说,你和你关注的人(following)所发的Tweet会出现在你的时间线;与此同时,你发的Tweet也会出现在关注你的人(followers)的时间线。
所以要提升Twitter的性能,必定要对这几个时间线来动手,可是这样问题就来了。

首先看传统的关系型数据库可以怎样处理吧(下面只是简化处理而已)。
第一个表是用户,存储用户的id、name和其他资料。第二个表是用户关系,存储用户id和其关注者。第三个表是Tweet,存储用户id和他所发的Tweet信息。
这样发Tweet是很简单的,只要在表三中insert一条即可。而删除也是同样的。
在获取公开时间线时,只要将表一和三进行inner join就行了。而在获取某个用户的时间线时,将表一和二进行inner join,获得所有following;加上该用户自己后,再和表三inner join,就能获得他和所有following的公开时间线,也就是他的时间线了。
这里不难看出,如果一个人following了很多人,那么获取所有following的公开时间线时会有个很庞大的inner join,非常影响性能。值得庆幸的是一般人不会following太多人(少于100),因为那会导致信息爆炸。可是Twitter上还有不少机器人存在,这些家伙想following多少都是有可能的…

因此这种方案最大的弊端就是庞大的inner join了,为了减少它的次数,使用memcache是比较常规的方法。
可是缓存用户的时间线是非常低效且不可靠的,所以缓存公开时间线显得更为合理。但如果仍然采用相同的数据库设计,当一个用户的公开时间线缓存失效时,就不得不再次更新所有用户的公开时间线缓存以避免inner join了,所以数据库设计仍然得更改。
于是再增加表四:公开时间线,内容就是用户和其最近所发的一些Tweet(或其id)。
那么在发Tweet时,需要插入表三并更新表四,删除也一样。
而在获取公开时间线时,先从缓存中取,拿不到再取表四,并放入缓存。获取用户的时间线时,也是先获取所有following,再分别从缓存中取following的公开时间线,取不到的再从表四里获取,最后合并即可。
这个方案的性能无疑比前者要好些,因为时间线并不需要即时性,只不过编程复杂度要高一些。

可Twitter为什么仍然对其不满呢?于是我又去研究了一下 Cassandra
在我对其的粗略研究中(不到一小时),我发现这个数据库也是采用键值对的存储方式,数据通信是采用JSON格式。虽然是键值对,但它却还支持范围查询(所以我估计是采用树,而不是hash算法,或者同时使用2类索引)。另一个重要特性是分布式,可以随意增加数据库结点来提升整体的读写性能。
因此不难看出,Twitter是被后者所吸引了。目前Twitter已有超过100亿条Tweet和1亿多用户,而它之前一直在使用MySQL作为后台数据库。可是MySQL对于处理超大型的数据比较棘手,而且在分布式方面也不是那么得心应手。就算去掉非活跃用户,要求峰值时支持1000万并发也在情理之中,可我估计非分布式数据库大概只能支持几千到几万并发。而突破了单台数据库的物理瓶颈,性能就可以通过服务器数量来提升了,这大概就是Twitter所打的如意算盘吧。
写到这里我突然发现了一篇好文: 《NoSQL数据库笔谈》,想深入了解NoSQL的不妨看看。这里面也谈到了一点:Twitter是没有自己的硬件的,全部由NTTA提供和维护,所以增加服务器也不会影响运营成本。

只是这样数据库设计也不得不改动了,正好我在Cassandra Wiki里发现了一个 Twissandra,于是就拿它来说明了。
这个Twissandra使用了Timeline和Userline这2个ColumnFamily(相当于关系数据库的表),于是就很好解释了。
用户发Tweet时不但要插入Tweet,还要插入Userline,然后遍历follower,分别插入他们的Timeline。由于非关系型数据库没有join操作,所以这个遍历是在应用服务器上完成的,因此要多次访问数据库。有些明星型用户可能有上百万的follower,这样发一个Tweet就要访问百万次数据库,这也是我在Google App Engine上遇到的一个难题(要知道一个GAE应用最多只能并行30个数据库查询)。
当然,获取时也有遍历的问题,但由于前文所述,following一般不会太多,所以还不算什么大问题。

由此可见,Twissandra带来了巨大的性能问题:过多的数据库访问。当然,如果Twitter有1亿台服务器的话,还是可以支持上百个拥有百万follower的用户来并行发Tweet,只是这个代价确实太高了。
那么Twitter肯定不会做这种蠢事,必然会使用更为有效的设计方案,可这个方案是什么?
最显而易见的就是不在数据库里使用Timeline和Userline这种区分,只使用公开时间线。这样就和方案二差不多,只不过join变成了多次数据库访问,同时也减少了数据冗余。
这样对一个用户来说,它的响应时间应该会比关系型数据库要慢,但由于存在缓存,所以平均下来仍然是差不多的。而对于整个系统而言,由于可以扩充大量数据库服务器,所以对数据库的并发性能有大幅提升。
因此在低负载时,单一用户的性能感受应该是略微降低了;而在高负载时,则会有较大改观。而幸运的是Twitter几乎一直都是高负载状态,所以转向NoSQL也在情理之中。
只是对我个人而言,一个低负载的SNS网站采用NoSQL似乎是弊大于利,可谁叫Google不给我其他的选择呢?