MoSonic:对SubSonic的分布式存储、缓存改进尝试(3)
接上文。
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)
- ...
- ...
以后要修改数据结构,直接改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中时,我感到的不是冲突,而更多的是一种不谋而合的美妙。
下篇会继续讲更多一些细节。