浏览 2893 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
|
|
---|---|
作者 | 正文 |
发表时间:2011-01-14
MoSonic支持海量数据存储,在web 2.0常见场景中其透明缓存层亦可带来10倍以上的读取性能提高。 这里写blog记述一下。 改进参考/使用了: FriendFeed Schemaless Database Design Twitter: Cache Money Facebook:Thrift 其中Cache Money影响较大,故内部命名为MoSonic,Money-SubSonic。 ============= SubSonic是.net中一个相当流行ORM库,号称零代码。与其它ORM相比,SubSonic在易用性方面相当突出。 但这也不意味着SubSonic,或者说ActiveRecord风格的ORM仅能局限于中小网站的开发使用。 ActiveRecord实际上仅是Martin Fowler在《企业应用架构模式》一书中的一个小章节提出的,两三页纸而已。 但在Web 2.0时代,随着被Ruby On Rails,Django等流行框架采用,而大放异彩。 Twitter建站之初也是采用Ruby On Rails,使用的也是RoR内置的ActiveRecord风格ORM。 随着Twitter的发展,数据库性能这块也必然成为瓶颈。而Twitter对此的回答是Cache-Money,一个透明的ActiveRecord缓存层。 Cache-Money能够透明的支持两类缓存,Object Cache 跟 Vector Cache。 (当然Cache-Money的作用不止这两类缓存。) ============= 都是ActiveRecord,上层API风格一致,Twitter可以在RoR的ORM中做的实现,也一定可以在SubSonic上用c#做实现。 Object Cache比较好理解,它仅仅是基于对象主键缓存对象内容。Object.FetchById(XXX) 这类函数中做下手脚,先查询缓存,缓存不存在时,再去查询数据库。这也是所谓的Read-Through直读缓存。 而Object.Save()在数据库保存成功之后,也随即更新Cache。这也是所谓的Write-Through直写缓存。 Object的直读/写缓存实现非常简单,对SubSonic的FetchById添加Object Cache的话,仅需要十来行代码就可以使用单机内存做缓存。 单机内存非常有限,Web 2.0网站一般使用memecached做分布式缓存,解决单机缓存内存有限的问题;这基本是标配了。 就.Net而言,找个靠谱的memcached client的要比实现SubSonic的Object Cache更加重要。 我使用的是BeIt出品的memcached客户端;据说性能更好的有EnyimMemcached,但我没有详细测试过,不好评论。 使用了Memcached做缓存的话,对象的序列化问题就变得非常突出了。 .Net内置的BinaryFormatter性能非常差,必须另寻序列化方案。 考察过诸多序列化方案后,我最终选择的是Facebook开源出来的Thrift;它的设计完备,跨语言/平台支持能力非常好,性能不比Google开源出来的ProtoBuf差,也可以扩展为进程间RPC通讯方案。 为SubSonic添加对象Thrift序列化相对来说就比较麻烦些,但也不难,在SubSonic的代码生成模板中修改即可。 Thrift本身实际上也有提供c#的对象序列化的代码生成工具;但直接使用的话,意味着需要在Thrift对象/SubSonic对象间多增加一次转换;修改代码量虽然会少些,但不如直接修改SubSonic的代码生成模板,直接将Thrift序列化的方法跟SubSonic对象做彻底的整合效率来得高。 使用Thrift做序列化跟使用.net默认的BinaryFormater性能差别是巨大的。公司两台Web服务器,在使用BinaryFormater时CPU近乎100%,但改用Thrift做序列化后立刻下降至20%。 (团队倒不是在出现服务器性能问题后才做了Thrift序列化;提前一年就做了;只是因故做了次实际的性能测试。) ========== 既然修改了SubSonic的代码生成模板,我也顺便将其Object new()的API干掉了;强迫开发者必须使用FetchById风格API获得对象。 同时也增加了FetchByIds的新函数,支持同时获得多个对象,对应select * from tables where id in (..., ...)的查询,以及memcached multi_get的命令。 通过添加透明的Object cache在不增加额外的业务逻辑代码情况下,已经可以获得显著的性能改善,但它还不是本质性的改进。 Cache Money真正的神器是其Vector Cache,实际上,Twitter团队在开发Cache Money时,是优先考虑了Vector Cache的实现,然后再考虑Object Cache;因为他们认为Vector Cache会性能影响更大,事实也证明他们判断正确。 声明:ITeye文章版权属于作者,受法律保护。没有作者书面许可不得转载。
推荐链接
|
|
返回顶楼 | |
发表时间:2011-01-14
Cache Money真正牛X的地方是在Vector Cache。在生产环境中,它不仅相对Object Cache命中率较更高,带来的性能飞跃更是可观。
在MoSonic的性能测试中,得到了有10倍的性能提高。 Vector Cache性能恐怖,但它对表结构,查询类型,有相当的严格的要求;列举如下: 表必须以自增数字(int / long)id为主键 查询的where中必须是 = 等于条件,如where user_id=1 多个where条件的话,相互关系必须是And,如where user_id=1 and id_deleted=0 查询结果仅能是数据id,如 select id from users where ... 不可以是 select user_name from users where ... 也可以是 select count(*) from users where ... 查询结果支持分页 查询结果必须以id排倒序,也就是order by id desc 只有完全符合上面五个条件,Vector Cache才可以生效;幸运的是,在web 2.0网站中,这类结构/查询正好是最常见的。 以博客为例,博客文章列表显示,分类文章数量,评论显示等等,基本都符合上述的查询。 比方说,要获得等级为1的用户时,需要使用下面的两个查询: select id from users where level=1 select * from users where id in (....) 两个查询cache money都可以完全缓存,如果直接使用: select * from users where level=1 的话,cache money则会完全失效。 对于两种风格的查询孰优孰劣,可以参考JavaEye老大Robin之前写的:为什么ORM性能比iBATIS好? ============= 因为要求了查询结果必须是id,并且排倒序,Vector Cache实际上是可以做到实时自动更新,而不是自动过期。 考虑这样的调用: select count(id) from photos where album_id=1 order by id desc limit 1, 100 select id from photos where album_id=1 order by id desc limit 1, 100 insert into photos (album_id)values(1) select count(id) from photos where album_id=1 order by id desc limit 1, 100 select id from photos where album_id=1 order by id desc limit 1, 100 显示列表,插入数据,再次显示列表;这是相当典型调用。 第1/2步查询会有缓存(即便是没有缓存,查询之后,缓存也会自动被生成,也就是所谓的直读)。 第3步插入数据时,获得数据库自增的ID后,可以直接将此id追加到第1/2步查询缓存结果中。 第4/5步查询直接命中第3步写数据时更新的缓存;完全无需查询数据库。 在查询、应用场景符合的理想情况下,有了Vector Cache,数据库读可以变成恐怖的0读取。 数据库仅需要承担写压力,100%的读都有Memcache的自动缓存。 这才是Cache Money的Vector Cache带来读性能飞跃的原因。 所有的数据库查询都变成了memcache get;memcache单机时在读能力,并发负荷能力上都要比传统关系型数据库高一个数量级;而且其shared nothing的架构,又可以水平扩张。 在高并发,多机缓存的情况下,可以预料Cache Money带来的读性能提高远不止10倍。 ============== Twitter的工程师对Cache Money的实现相当巧妙,他们针对一个限制多多的场景做到了100%的读缓存;而这个“限制多多”又恰恰是web 2.0网站中的最典型场景。 我在MoSonic中实现Vector Cache时,完全照搬了Cache Money的实现算法;就是C#的代码量比ruby膨胀了几倍。 |
|
返回顶楼 | |
发表时间:2011-01-15
Cache Money虽然解决了数据的读取性能瓶颈;但开发大网站数据库面临的问题远不至读压力。
首先是容量。 上千万/亿的数据量并不罕见,单一物理数据库服务器即便单纯承担写压力也会是瓶颈。更何况Cache Money仅仅是在理想状况下才可以做到数据库0读。缓存服务器更新,新增查询,复杂查询等等都还会造成读压力。 比较常见的做法是采用分表,也就是所谓的Sharding,把数据按照一定的规则,分别存储至多台数据库服务器上去。 其次是变动。 业务需求是不可预测的;无论一开始数据库表结构定义得如何完备,总会有新需求出来,需要对表结构做调整才可以实现。 数据量过了百万之后,每次对生产服务器做alter table/create index等调整都是痛苦的经历。 针对容量与变动这两个问题,FriendFeed提出的schema-less database design给出了一个相当漂亮的解决方案。 强烈推荐阅读FriendFeed的原文。 FriendFeed的方案大致是这样: 只有一种表结构,只有两个列:id + blob/binary(max) id本身是UUID,这本身可以很容易做sharding blob可以反序列化为任意结构 查询通过另外建表实现,比方说users表的blob列反序列化出来的结构中包含一个age的int属性;要查询select * from users where age = 18; 那么就另外建表如user_age,仅包括两列id / age;先查询此表获得id,再查询原本的users表获得完整数据 索引表可以异步建立,而且,建立的时候它都是跟查询相关,可以根据查询条件做sharding;如上面所的age。 FriendFeed的方案相当聪明,数据本身结构及其简单,sharding很容易做。写/读压力一下子就分布出去。 blob列用于序列化(数据甚至是先zip过再存,CPU强劲,磁盘IO是瓶颈),所以结构可以随时变化;只需要保证序列化算法可以兼容不同版本即可。 而灵活的序列化,恰恰是Facebook Thrift所解决的! (还记得一开始使用Memcached做object cache时采用了Thrift做序列化么?) 先不考虑Sharding分布方案,在MoSonic中将各个类定义为类似下面的结构: id(int) properties(blob) user_name(varchar) age(int) ... ... 使用时可以直接这样:User.FetchById(XXX).properties.user_name。 因为一开始已经把Thrift序列化代码生成做到SubSonic模板时,这里要给数据多增加一层结构也并非难事;有点水到渠成的感觉。 以后要修改数据结构,直接改Thrift的定义文件,然后重新生成代码就成。properties列中存的数据可能跟最新的结构不一直,但Thrift并不要求严格的匹配(BinaryFormatter则不然),它会自动忽略那些不符合的列;而一但Object被重新存入,数据就又会被重新序列化完整。 ====================== FriendFeed的分布式方案要求表主建是uuid,而cache money却要求所有表必须要有自增的ID主健。 这其实不是冲突,把database_name + table_name + id看成一个uuid即可。 而FriendFeed的分布式索引,跟cache money中Vector Cache有异曲同工之妙。 都是根据查询条件做处理/sharding。 之前为MoSonic添加Vector Cache,已经需要判断查询的表名/查询条件;符合即查询缓存;这里套用FriendFeed的方案则变成,符合即查询分布式索引! 执行select id from users where age=18 limit order by id desc 0,10时 逻辑变成这样: 检查Vector Cache,存在便返回 检查分布式索引表规则,获得新的数据库连接字符串 执行查询 写入Vector Cache 插入数据时,之前仅是更新Vector Cache,现在则需多一步去插入索引表。 实际运行中,因为是先插入数据表,同步更新Vector Cache,后续的插叙已经会命中缓存;索引表的更新实质变成是备份,可以异步插入。 Thrift / Cache Money / Schema-less Database Design实际上是三个不同团队为了解决不同方面的技术问题而做出的方案,但糅合进MoSonic中时,我感到的不是冲突,而更多的是一种不谋而合的美妙。 |
|
返回顶楼 | |