背景我们使用MySQL来存储FriendFeed的所有数据。随着用户群的增长,数据库也增长了很多。它现在存储了超过2.5亿条记录和大量其他数据,从评论和“喜欢”到朋友列表。随着数据的增长,我们反复解决了快速增长带来的可扩展性问题。我们的尝试具有代表性,比如使用只读的mysql从节点和memcache来提高读吞吐量,对数据库进行分片来提高写吞吐量。然而,随着业务的增长,添加新功能比扩展现有功能以满足更多流量变得更加困难。特别是,更改架构或向包含超过10-20百万行的数据库添加索引可能会使数据库锁定数小时。删除旧索引也需要这么多时间,但不删除它们会影响性能;因为数据库在每次INSERT时不断地读写这些无用的块,并将重要的块推出内存。避免这些问题需要一些复杂的措施(例如在从节点上设置新的索引,然后将从节点与主节点交换),但这些措施容易出错且难以实施,并且它们阻止了更改schema/只能用索引实现的新特性。由于数据库的严重碎片化,MySQL的关系特性(如连接)对我们来说毫无用处,因此我们决定放弃RDBMS。虽然有很多项目(比如CouchDB)解决了灵活的模式数据存储和运行时索引的问题。但它在大型站点中的使用还不够广泛,不足以说服人们使用它。从我们看到和运行的测试来看,这些项目要么不稳定,要么缺乏足够的测试(请参阅这篇关于CouchDB的过时文章)。MySQL很好,它不会破坏数据;复制很好,我们已经看到了它的局限性。我们喜欢使用MySQL进行存储,只是非关系存储。经过深思熟虑,我们决定在MySQL之上采用无模式存储系统,而不是使用全新的存储系统。本文试图描述该系统的高级细节。我们很好奇其他大型站点如何处理这些问题,并希望我们所做的一些设计能够帮助其他开发人员。概述我们在数据库中存储的是一组无模式的属性(例如JSON对象或Python字典)。存储的记录只需要一个名为id的16字节UUID属性。实体的其余部分对数据库不可见。我们可以简单的存储新的属性来改变schema(可以简单理解为数据表中只有两个字段:id,data;其中data存储的是实体的属性集)。我们通过保存在不同表中的索引检索数据。如果我们想在每个实体中检索三个属性,我们需要三个数据表-每个特定属性一个。如果我们不想再使用索引,我们需要在代码中停止写入索引对应的表,并可选择删除该表。如果要增加新的索引,只需要为该索引创建一个新的MySQL表,并启动一个进程异步向表中添加索引数据(不影响正在运行的服务)。最后,随着我们的表的增长,添加和删除索引变得简单。我们大大改进了添加索引数据(我们称之为“清理器”)的过程,以便可以在不影响站点的情况下快速添加。我们一天就可以保存和索引新的属性,而且我们不需要切换主从MySQL数据库,或者任何其他可怕的操作。详细信息MySQL使用表来保存我们的实体,表如下所示:使用added_id字段的原因是因为InnoDB是按照物理主键的顺序存储数据的,自增主键保证了新实例在磁盘上老实体之后顺序写入,有利于分区读写(与旧实体相比,新实体通常阅读更频繁,因为FriendFeed页面是按时间倒序排列的)。实体本身被序列化为python字典并使用zlib压缩存储。该索引存在于单独的表中。如果我们要创建索引,我们创建一个新表来存储我们要索引的数据分片的所有属性。例如,一个FriendFeed实体通过看上去是这样的:{"id":"71f0c4d2291844cca2df6f486e96e37c","user_id":"f48b0440ca0c4f66991c4d5f6a078eaf","feed_id":"f48b0440ca0c4f66991c4d5f6a078eaf","title":"WejustlaunchedanewbackendsystemforFriendFeed!","link":"http://friendfeed.com/e/71f0c4d2-2918-44cc-a2df-6f486e96e37c","published":1235697046,"updated":1235697046,}Weindextheattributeuser_idoftheentitysothatwecanrenderapagecontainingAllattributesofasubmitteduser.我们的索引表如下所示:CREATETABLEindex_user_id(user_idBINARY(16)NOTNULL,entity_idBINARY(16)NOTNULLUNIQUE,PRIMARYKEY(user_id,entity_id))ENGINE=InnoDB;我们的数据存储会自动为你维护索引,所以如果你想在我们存放上述结构实体的数据存储中打开一个实例,你可以写一段代码(python):user_id_index=friendfeed.datastore.Index(table="index_user_id",properties=["user_id"],shard_on="user_id")datastore=friendfeed.datastore.DataStore(mysql_shards=["127.0.0.1:3306","127.0.0.1:3307"],indexes=[user_id_index])new_entity={"id":binascii.a2b_hex("71f0c4d2291844cca2df6f4876e96e),"user_id":binascii.a2b_hex("f48b0440ca0c4f66991c4d5f6a078eaf"),"feed_id":binascii.a2b_hex("f48b0440ca0c4f66991c4d5f6a078eaf"),"title":u"WejustlaunchedannewbackendsystemforFriendFeed!","link":u"http://friendfeed.com/e/71f0c4d2-2918-44cc-a2df-6f486e96e37c","published":1235697046,"updated":1235697046,}datastore.put(new_entity)entity=datastore.get(binascii.a2b_hex("71f0c4d2291844cca2df6f486e96e37c"))entity=user_id_index.get_all(datastore,user_id=binascii.a2b_hex("f48b0440ca0c4f66991c4d5f6a078eaf"))上面的Index类在所有实体在index_user_id表中找到user_id,自动维护index_user_id表的索引。我们的数据库是分片的,参数shard_on用于确定索引存储在哪个分片上(在本例中,使用entity["user_id"]%num_shards)。可以使用索引实例(见上文user_id_index.get_all)查询一个索引,使用python写的数据存储代码合并表index_user_id和表实体。先查询所有数据库分片中的表index_user_id,得到entityID列,然后在entities中取数据。新建一个索引,比如在属性链接上,我们可以新建一个表:CREATETABLEindex_link(linkVARCHAR(735)NOTNULL,entity_idBINARY(16)NOTNULLUNIQUE,PRIMARYKEY(link,entity_id))ENGINE=InnoDBDEFAULTCHARSET=utf8;我们可以修改数据存储以包含我们的新索引:index_link",properties=["link"],shard_on="link")datastore=friendfeed.datastore.DataStore(mysql_shards=["127.0.0.1:3306","127.0.0.1:3307"],indexes=[user_id_index,link_index])我可以异步建立索引(尤其是实时传输服务):./rundatastorecleaner.py--index=index_link#p#Consistencyandatomicity由于使用分区数据库,可能会存储实体的索引在与实体不同的分区中,这导致了一致性问题。如果在写入所有索引表之前进程崩溃了怎么办?许多有抱负的FriendFeed工程师倾向于构建事务协议,但我们希望尽可能保持系统清洁。我们决定放宽限制:主实体表中存储的属性集是一个规范的、完整的索引,不会影响真实的实体值。因此,在向数据库写入实体时,我们采用以下步骤:使用InnoDB的ACID属性写入实体entities表。将索引写入所有分区中的索引表。我们必须记住,从索引表中获取的数据可能不准确(例如,如果写操作没有完成第2步,它可能会影响旧的属性值)。为了保证使用上面的限制能够返回正确的实体,我们使用索引表来决定读取哪些实体,但是不信任索引的完整性,使用查询条件来过滤这些实体:1.从根据查询条件索引从表中获取entity_id2.根据entity_id从entities表中读取实体3.根据实体的真实属性过滤掉不满足查询条件的实体(使用Python)确保索引的持久化和一致性,上面提到的“清理”“worker”进程必须继续运行,写入丢失的索引,清理无效的旧索引。它优先清理最近更新的实体,因此保持索引一致性实际上非常快(几秒钟)。性能我们优化了新系统的主要指标,并对结果感到满意。下面是上个月FriendFeed页面的加载延迟统计图(我们几天前启动了新的后端,您可以从延迟的显着下降中发现那一天)。特别是,系统的延迟现在很稳定(即使在午后高峰时段)。下面是过去24小时FriendFeed页面加载延迟的图表。与上周的一天相比:到目前为止,该系统非常易于使用。我们在部署后也更改了几次索引,我们也开始将这种模式应用到MySQL中的那些更大的表中,以便我们以后可以轻松地更改它们的结构。原文链接:http://www.oschina.net/translate/friendfeed-schemaless-mysql-new
