之前已经已经写过一篇《从开放平台建设者角度对应用开发者的一点架构建议(1)》,主要是介绍了最基本的openid、平台数据、应用内部数据的存储建议,这一次我们更深入一点。
对之前的文章,我们提到了三种数据:
- openid-id
- id-平台数据
- id-应用数据
相信大部分个人开发者的第一反应是,上面每份数据建一张表,之间建立很多外键关系。这样的确会有很大的好处,很多数据查询操作都可以直接通过sql语句完成,比如:
- 通过openid查询id
- 通过id查询openid
- 通过用户名查询openid/id
- 通过应用数据查询openid/id
上面的架构都很好的,并且开发成本非常低,但是这一切的前提是你的应用的用户量有多少。
100w是个坎,100w之前没有任何问题,100w之后,这种架构就是垃圾
很多人会说,对于一个小应用,考虑那么大量用户干嘛?你这是过度设计了吧。
有这种思想的人不少,没错,当年facebook过100w用户的时候,已经是一家有很多职员的公司了。那你会不会觉得,当我们的小应用成长为100w用户的时候,我们已经有了足够的资金,足够的职员,可以考虑重构了?
然而事实是,zynga新推出的游戏《帝国与同盟》在facebook上上线一周,日活跃就达到3000w,更别说注册用户量。而就国内的情况来说,在朋友网上面的任何一款应用,只要不是太差,1~2个月,注册用户基本就可以轻松达到100w。
是不是有些震撼?SNS应用的病毒式传播能力远超过我们的想象,而这所带来的结果,除了利益之外,在技术上也对架构提出了更高的要求。
请别误会我的意思,我并不是说一个个人开发的应用,一开始就要考虑 读写分离,异步写,将二层架构(webserver+db)变成三层架构(webserver+cache+db)甚至四层架构(webserver+logicserver+cache+db),我的意思是,大系统开始要小做,但是不代表不给以后留下扩展的接口。
OK,到此为止,我所要表达的观点基本已经出来了:
对于一个小团队来说,我们可以继续保持webserver+db的两层结构,但是要为以后留有适当的扩展接口
听起来似乎有些抽象,但是实施起来却很简单:
- 分库分表,即使所有的都库表都放在同一个mysql实体机上
- 要封装出数据库访问层,上层不需要知道访问的是的memcache还是mysql
- 将很多太依赖mysql的查询,放到业务层(比如自增key,比如外键查询)
按照上面的原则,如下的问题就可以得到解决:
- 注册用户数超过100w,导致单表查询性能降低,以及单机存不下的问题
- 数据访问频率变高,导致mysql需要迁移到memcache的问题
OK,那么根据这个原则,我们从新来设计一下我们的底层数据库结构。
首先,冗余出一份id-openid的数据,来支持id到openid的查询。
原来我们只有openid-id这样一个映射表,是因为mysql也可以直接通过id查询openid,然而一旦分库表之后,就不得不再冗余一份id-openid的映射表,但也确实是没有办法的事情(当然,如果你技术够牛,也可以自己实现一个双key cache)。
第二,将所有自增key的地方,替换为在某个地方存放当前id的最大值。
既然要抛弃自增生成id的方法,那我们就需要一个地方来存储当前的最大id的值。这里用一个表来记录就可以了,因为毕竟每秒新注册的用户还是很少的
第三,将openid-id,id-openid,id-平台数据,id-应用数据,分库分表。
最简单的方法就是取模,id=1234,如果10库10表的话,千位和百位对应库,十位和个位对应表,那就是db_2和tb_4。当然,在某些框架,如django上不方便分表,那么也可以只分库,比如分100库,落到db_34。我们这里采用只分库,不分表的方式,我们分1000库,这样等到你有10亿用户的时候,每个表也才100w条记录,一定是够用了。
具体怎么分呢?实际上,我们有更简单的方法,如下:
库名 | 包含的表 |
db_app_single | id_alloc |
db_app_openmod_0 ~ db_app_openmod_999 | openid2id |
db_app_idmod_0 ~ db_app_idmod_999 | id-openid id-平台数据 id-应用数据 |
OK,这样我们整个架构对于抗大量用户的能力就大大加强了,而且扩展性也比原来的架构要好很多。以后一旦访问量的瓶颈达到之后,我们就可以把db的直接访问变成访问cache,或者考虑其他的优化方案,如前面提到的读写分离,异步写等等。
好啦,就这样~,希望文中的建议能给已经是个人应用开发者,或者即将成为个人应用开发者的朋友有所帮助~
小宇释然 on #
“第三,将openid-id,id-openid,id-平台数据,id-应用数据,分库分表。 ”
这条下面的说明,是不是sharding的意思了?
Reply
Dante on #
嗯,之前不知道还有这样一个名词。。
看了介绍,貌似差不多。
Reply
小宇释然 on #
很高兴认识你。
请问你是做这方面开发的吗?
Reply
Dante on #
你好,其实我是做腾讯开放平台的,所以会直接接触很多第三方应用,也会评估/帮助改善他们的架构。
Reply
小宇释然 on #
嗯,了解了。
我想在腾讯开放平台做一个应用,最近在了解这些信息
Reply
Dante on #
哈哈,好~
Reply
五笔字根表 on #
看不太懂能说清楚点吗??
Reply
树脂交流博客 on #
很好的文章,先拜读一下
Reply
fy on #
首先很高兴看到你把短连接地址放到文章尾部了,确实方便:)
你这个分表分库架构还遗留一个问题,分了那么多数据库,不太好管理吧?一下子需要启动和管理那么多库,运维估计头大哈,我这边倒是有个还没真正经历线上使用的“解决方案”,不如就贴到你这吧,我懒得写博客:
---------
写太长了,放到我的博客上了:)
http://www.faryang.net/blog/?p=36&f=vimer
Reply
Dante on #
文章我看了,有一点我不太理解,id生成的时候如果是根据负载随机插到某个dbshard,那么程序想要通过id查询openid的时候,怎么知道到哪个dbshard查呢?
另外,其实无论是通过你文中的id-genertor还是路由表,都是为了通过一次计算就可以让程序获知数据具体所在的位置。这一点是相同的。
而对于你说的db-tb个数太多不好维护的问题,其实腾讯大部分的分布式存储都用的这样的模型,这样做了之后,
1.对cache策略会非常友好
2.机器扩容非常简单,数据很容易double
3.每张表的数据量很小,远小于100w,速度很快
4.出问题的时候,只会影响某个某个号段,可以在前端针对这部分用户出维护公告,而对其他用户没有影响
Reply
fy on #
"想要通过id查询openid的时候,怎么知道到哪个dbshard查呢?"
路由表的问题是还要去查落在哪个dbshard上,id-generator 根据id值已经知道他是落在哪个dbshard上了:)
前提是,预先设定每个dbshard只能容纳一定范围内的id,比如某用户id是 2^30+1那么,他就落在dbshard2上。另一用户的id是 5*2^30+8 那么他就落在dbshard6上。
不用查路由表,只是简单的做个区间(2^30)平均分配;因为一个dbshard通常能承受的数据行数不会超过2^30;具体实现时我只用2^24,感觉单库支撑1000w左右的数据量应该差不多;
画个图吧:
----------------------------------------------------------
|dbshard1 | dbshard2 | dbshard3 | ....
|1~2^30 | 2^30~2*2^30 | 2*2^30~3*2^30 | ....
|1,2,.... | 2^30, 2^30+1, ......
openid - id
A 1 (第一个用户可能落在shard1上)
B 2^30+1 (第二个用户可能落在shard2上)
C 2^30+2 (第三个用户可能落在shard2上,当然可能得到的是2,那么就落在shard1上,因为这个是随机的)
D ...
每个dbshard的下一个要插入的id都是按顺序产生的,这样id就不会有冲突(即全局id):
shard1: 1,2,3,....
shard2: 2^30+1, 2^30+2, .....
Reply
Dante on #
|1~2^30 | 2^30~2*2^30 | 2*2^30~3*2^30 | …
明白了,其实这种方法和文中的方法核心是一样的,即id/ (2^30) % n。
但因为总数据量很难确定(一个产品上线,很难评估会有100w人注册还是1亿人注册),而且根据总数据量来正比扩容是不合理的(访问量不是与注册量成正比),所以一般不会用2^30这么大的除数。
Reply
fy on #
核心确实没什么根本性的变化
30是大了,所以我觉得可能只需用到2^24,30是为了说明问题写的,实际情况是不需要这么大,如果是像腾讯qzone对app采取的放量导入用户,这么做可能比较合适,不用一开始就启动几十个db实例,后期也不必做大量db迁移什么的。
“总数据量来正比扩容”
注册量与访问量的问题,需要监控到每台db的负载情况,当某个db负载到一定值的时候,比如到了他能承受最大压力的50%时,把新的用户引入到新的db上。
我可能没说清楚意思,其实还权重的功能在里头,可以认为,哪台机器的压力小,那么分给他的权重就高,这个权重根据监控得到的负载情况可以随时改的。
所以这些机器各个db的数据量不总是一样的,可以做到对性能比较低下的机器给的权重比较低,那么他的压力自然不会高。
Reply
Dante on #
一下子启动那么多库表---其实只是程序上线的时候需要启动一次,其他时间就不用管了。。
而且开发调试阶段,把模设置成1就可以不用关心分库表了。
Reply
Nekle on #
呵呵,这几天刚到腾讯实习的。
前辈写的还是很不错的,学到了很多,辛苦了,希望有更多的分享。
Reply
奥西里斯 on #
看了这篇文章收益颇多……我是django的新手,我想问下,django里面分1000个库怎么写啊。
我查了下django的官方文档,多数据库处理用一个using就OK了的样子- -
因为我比较弱,所以我就只是这样想了想,希望博主指点指点哈。
我没有用外键,但是基本上所有的数据表都有一个player_id,我就用这个player_id来计算出应该using哪个库,然后重写下models.Model中默认的save,参数using=None改为using=取模计算出的库。
至于要使用manager的时候,同样重写models.Manager,get_query_set里加一句objects.using(取模计算出的库)。
貌似是可行的……但是现在有个问题……
当我想要在所有表里查的时候,比如所有人排行榜这些的时候,我该怎么办?
Reply
Dante on #
哈哈,我前段时间正好也在用django来实践这种分库。
对于save操作来说,django是提供了路由方法的,不用每次都用using。
对于查询类操作,确实就要手工算出库名,然后using了。
当我想要在所有表里查的时候,比如所有人排行榜这些的时候,我该怎么办?
对于这一点的话,其实这是惯性思维,当海量用户的时候,排行榜这种东西一天生成一次都可以,没必要每次都去计算。
Reply
奥西里斯 on #
感谢博主哈。我正在试这个,然后发现很郁闷的是在settings里面怎么改DATABASES这个字典啊。
1000个呢。settings里面好像不能用for循环往DATABASES这个字典里加值啊,就算是外部写个方法来调用都不行啊。
Reply
Dante on #
是可以的
settings.py也不过是个python文件,也是一样执行的
Reply
奥西里斯 on #
但是我试了下,无论如何都不行啊……
DB_MOD = 1000
arc = DATABASES['default']
for i in xrange(0, DB_MOD):
arc.__setitem__('NAME', arc_idmod_' + str(i))
DATABASES.__setitem__('arc_idmod_' + str(i), arc)
--------------
这样syncdb --database=arc_idmod_0的时候根本就不建表啊- -
Creating tables ...
Installing custom SQL ...
Installing indexes ...
No fixtures found.
实际上是什么表都没有建起来
在外面写方法以后调用也是一样的效果
(我是想用脚本运行syncdb0~999的)
Reply
Dante on #
你有把数据库先建立起来吗?
Reply
奥西里斯 on #
嗯 我用脚本创建的数据库
去stackoverflow那边丢了一下人,原来是我傻了,应该先copy一下。
现在基本上可以了。
(另外stackoverflow上的老外说= =分1000库,你是facebook么……)
Reply
奥西里斯 on #
诡异的是从settings里导入这个DATABASES,得到的结果的确是包含了1000个键值对的那个- -
Reply
奥西里斯 on #
啊 我傻了,字典调用之前应该copy一下的- -。。这个问题算是解决了:)
Reply
奥西里斯 on #
悲剧,我发现这种方法容易导致数据完整性的问题啊。跨数据库操作是没法使用事务的吧?
Reply
Dante on #
事务最好做在程序逻辑层,而不要依赖底层数据库来保证。
否则以后换成cache怎么办呢?
Reply
奥西里斯 on #
哈哈哈 感谢博主,已经改成了1000库的这种架构而且都能跑通了,非常有感觉。博主真是天才!
Reply
奥西里斯 on #
现在还有个问题请教,我现在读数据库已经专门提取出来了,统统使用自定义的get_xxx(args),getlist_xxx(args)这样的方法,这已经算是分出一层了吧。但是那些数据库的写操作,有些功能的封装还是照原样放在models里面或是manager里面的……我现在想知道这个要怎么提取呢,或者说有没有必要提取呢?因为我get的时候上层实际上已经不知道是从cache还是从db里面取的了,所以我觉得貌似写的操作没必要再提出来了的样子?博主指教一下呢,呵呵。
Reply
Dante on #
对于写操作来说,其实我也是建议直接save就可以了。
实现的很细呀,现在是打算做一款sns应用吗?加油!
Reply
奥西里斯 on #
其实是先做手机SNS应用,不过以后肯定会搬到PC端的,所以后台想一开始就做好。
(功能其实做了一半了,所以改成博主说的这种结构也还是花了不少功夫- -)
博主的这个方法真是让我茅塞顿开的感觉,以后再有问题还要朝博主请教,呵呵。
Reply
AA on #
还有第3篇没?
Reply
Dante on #
暂时没有时间哈,等有时间了整理下:)
Reply