一致性和原子性
由于我们给数据库做过分片,并且实体的索引可以被存储在和它本身不同的多个分片之上,这样一致性就成了问题。要是进程在完成对所有索引表的写入前就崩溃了,那该如何是好?
构建一套事务协议就成了大多数FriendFeed工程师眼中的有力方案,但我们希望尽可能地保持系统结构简洁。于是,我们决定放宽约束,使得:
主entities表中存储的属性包是规范的。
索引反映的不一定是实际的实体值。
这样一来,要将一个实体写入数据库,我们要进行以下步骤:
1.用InnoDB的ACID属性来保证,将实体写入entities表。
2.把索引值写入所有分片上的索引表中。
当从索引表中读入数据时,我们很清楚索引值不一定是准确的(也就是说,假如第2步的写入操作没有完成,索引所反映的可能还是老的属性数据)。根据前面提到的约束,为保证不返回无效实体,虽然使用索引表来决定哪些实体该被读入,但是我们会重新把查询的过滤条件应用到实体上,而不是完全信赖索引的完整性:
1.基于查询从所有的索引表中读入entity_id。
2.由取得的实体ID值从entities表中取得实体值。
3.根据实际的属性值,(在Python中)过滤掉所有不符合查询条件的实体。
为保证索引不会永久丢失,以及不一致性问题最终得到解决,我在前面提到的“清扫程序”进程会持续不断的扫描实体表,写入丢失的索引,并清除旧索引和无效索引。清扫程序会优先清扫最近更新的实体,使得索引中的不一致性问题在实际情况下很快得到修复(在几秒之内)。
性能
我们在新系统中对主要的索引做了不少优化,其结果令我们颇为满意。下图显示的是过去一个月内FriendFeed页面浏览的等待时间(我们在几天前启动了新的后端系统,从图中的分水岭处很容易就能看出具体是哪天)。
目前,系统的页面等待时间超乎想象地稳定,甚至在每天正午的流量高峰期也不例外。下图显示的是过去24小时内FriendFeed的页面浏览等待时间。
大家可以对比一周前的数据。
到目前为止,系统的运转一切正常。自从新系统部署以后,我们已经对索引做过了好多次变更,并且开始对我们最大的MySQL表进行转换,来使用这套新的方案。这样一来,我们在今后就可以更加从容地更改这些表的结构了。
参考资料
1.FriendFeed:http://friendfeed.com
2.CouchDB:http://couchdb.apache.org
3.CouchDB性能初探:http://tinyurl.com/CouchDBPerf
4.Pickle:http://docs.python.org/library/pickle.html