论坛首页 编程语言技术论坛

MoSonic:对SubSonic的分布式存储、缓存改进方案尝试

浏览 2894 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2011-01-14  
在公司内部项目实现过程中团队对SubSonic增加了分布式存储,透明对象缓存,透明查询缓存的支持;内部使用了两三年,并且在持续改进中。

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会性能影响更大,事实也证明他们判断正确。
   发表时间: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膨胀了几倍。
0 请登录后投票
   发表时间: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中时,我感到的不是冲突,而更多的是一种不谋而合的美妙。
0 请登录后投票
论坛首页 编程语言技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics