最后更新于 .

之前已经已经写过一篇《从开放平台建设者角度对应用开发者的一点架构建议(1)》,主要是介绍了最基本的openid、平台数据、应用内部数据的存储建议,这一次我们更深入一点。 对之前的文章,我们提到了三种数据:

  • openid-id
  • id-平台数据
  • id-应用数据

相信大部分个人开发者的第一反应是,上面每份数据建一张表,之间建立很多外键关系。这样的确会有很大的好处,很多数据查询操作都可以直接通过sql语句完成,比如:

  1. 通过openid查询id
  2. 通过id查询openid
  3. 通过用户名查询openid/id
  4. 通过应用数据查询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的两层结构,但是要为以后留有适当的扩展接口 听起来似乎有些抽象,但是实施起来却很简单:

  1. 分库分表,即使所有的都库表都放在同一个mysql实体机上
  2. 要封装出数据库访问层,上层不需要知道访问的是的memcache还是mysql
  3. 将很多太依赖mysql的查询,放到业务层(比如自增key,比如外键查询)

按照上面的原则,如下的问题就可以得到解决:

  1. 注册用户数超过100w,导致单表查询性能降低,以及单机存不下的问题
  2. 数据访问频率变高,导致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,或者考虑其他的优化方案,如前面提到的读写分离,异步写等等。

好啦,就这样~,希望文中的建议能给已经是个人应用开发者,或者即将成为个人应用开发者的朋友有所帮助~

Pingbacks

  1. 第四部 » 博客推荐14:Vimer的程序世界 on #

    [...] SNS应用开发架构建议(2)-如果你的用户量达到100w [...]

Pingbacks已打开。

Trackbacks

引用地址

评论

  1. 小宇释然

    小宇释然 on #

    “第三,将openid-id,id-openid,id-平台数据,id-应用数据,分库分表。 ”

    这条下面的说明,是不是sharding的意思了?

    Reply

    1. Dante

      Dante on #

      嗯,之前不知道还有这样一个名词。。
      看了介绍,貌似差不多。

      Reply

      1. 小宇释然

        小宇释然 on #

        很高兴认识你。
        请问你是做这方面开发的吗?

        Reply

        1. Dante

          Dante on #

          你好,其实我是做腾讯开放平台的,所以会直接接触很多第三方应用,也会评估/帮助改善他们的架构。

          Reply

          1. 小宇释然

            小宇释然 on #

            嗯,了解了。
            我想在腾讯开放平台做一个应用,最近在了解这些信息

            Reply

            1. Dante

              Dante on #

              哈哈,好~

              Reply

    2. 五笔字根表

      五笔字根表 on #

      看不太懂能说清楚点吗??

      Reply

  2. 树脂交流博客

    树脂交流博客 on #

    很好的文章,先拜读一下

    Reply

  3. fy

    fy on #

    首先很高兴看到你把短连接地址放到文章尾部了,确实方便:)

    你这个分表分库架构还遗留一个问题,分了那么多数据库,不太好管理吧?一下子需要启动和管理那么多库,运维估计头大哈,我这边倒是有个还没真正经历线上使用的“解决方案”,不如就贴到你这吧,我懒得写博客:

    ---------

    写太长了,放到我的博客上了:)

    http://www.faryang.net/blog/?p=36&f=vimer

    Reply

    1. Dante

      Dante on #

      文章我看了,有一点我不太理解,id生成的时候如果是根据负载随机插到某个dbshard,那么程序想要通过id查询openid的时候,怎么知道到哪个dbshard查呢?

      另外,其实无论是通过你文中的id-genertor还是路由表,都是为了通过一次计算就可以让程序获知数据具体所在的位置。这一点是相同的。

      而对于你说的db-tb个数太多不好维护的问题,其实腾讯大部分的分布式存储都用的这样的模型,这样做了之后,
      1.对cache策略会非常友好
      2.机器扩容非常简单,数据很容易double
      3.每张表的数据量很小,远小于100w,速度很快
      4.出问题的时候,只会影响某个某个号段,可以在前端针对这部分用户出维护公告,而对其他用户没有影响

      Reply

      1. fy

        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

        1. Dante

          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

          1. fy

            fy on #

            核心确实没什么根本性的变化

            30是大了,所以我觉得可能只需用到2^24,30是为了说明问题写的,实际情况是不需要这么大,如果是像腾讯qzone对app采取的放量导入用户,这么做可能比较合适,不用一开始就启动几十个db实例,后期也不必做大量db迁移什么的。

            “总数据量来正比扩容”

            注册量与访问量的问题,需要监控到每台db的负载情况,当某个db负载到一定值的时候,比如到了他能承受最大压力的50%时,把新的用户引入到新的db上。

            我可能没说清楚意思,其实还权重的功能在里头,可以认为,哪台机器的压力小,那么分给他的权重就高,这个权重根据监控得到的负载情况可以随时改的。

            所以这些机器各个db的数据量不总是一样的,可以做到对性能比较低下的机器给的权重比较低,那么他的压力自然不会高。

            Reply

    2. Dante

      Dante on #

      一下子启动那么多库表---其实只是程序上线的时候需要启动一次,其他时间就不用管了。。
      而且开发调试阶段,把模设置成1就可以不用关心分库表了。

      Reply

  4. Nekle

    Nekle on #

    呵呵,这几天刚到腾讯实习的。
    前辈写的还是很不错的,学到了很多,辛苦了,希望有更多的分享。

    Reply

  5. 奥西里斯

    奥西里斯 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

    1. Dante

      Dante on #

      哈哈,我前段时间正好也在用django来实践这种分库。

      对于save操作来说,django是提供了路由方法的,不用每次都用using。
      对于查询类操作,确实就要手工算出库名,然后using了。

      当我想要在所有表里查的时候,比如所有人排行榜这些的时候,我该怎么办?

      对于这一点的话,其实这是惯性思维,当海量用户的时候,排行榜这种东西一天生成一次都可以,没必要每次都去计算。

      Reply

  6. 奥西里斯

    奥西里斯 on #

    感谢博主哈。我正在试这个,然后发现很郁闷的是在settings里面怎么改DATABASES这个字典啊。

    1000个呢。settings里面好像不能用for循环往DATABASES这个字典里加值啊,就算是外部写个方法来调用都不行啊。

    Reply

    1. Dante

      Dante on #

      是可以的
      settings.py也不过是个python文件,也是一样执行的

      Reply

  7. 奥西里斯

    奥西里斯 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

    1. Dante

      Dante on #

      你有把数据库先建立起来吗?

      Reply

      1. 奥西里斯

        奥西里斯 on #

        嗯 我用脚本创建的数据库
        去stackoverflow那边丢了一下人,原来是我傻了,应该先copy一下。
        现在基本上可以了。
        (另外stackoverflow上的老外说= =分1000库,你是facebook么……)

        Reply

  8. 奥西里斯

    奥西里斯 on #

    诡异的是从settings里导入这个DATABASES,得到的结果的确是包含了1000个键值对的那个- -

    Reply

  9. 奥西里斯

    奥西里斯 on #

    啊 我傻了,字典调用之前应该copy一下的- -。。这个问题算是解决了:)

    Reply

  10. 奥西里斯

    奥西里斯 on #

    悲剧,我发现这种方法容易导致数据完整性的问题啊。跨数据库操作是没法使用事务的吧?

    Reply

    1. Dante

      Dante on #

      事务最好做在程序逻辑层,而不要依赖底层数据库来保证。
      否则以后换成cache怎么办呢?

      Reply

  11. 奥西里斯

    奥西里斯 on #

    哈哈哈 感谢博主,已经改成了1000库的这种架构而且都能跑通了,非常有感觉。博主真是天才!

    Reply

  12. 奥西里斯

    奥西里斯 on #

    现在还有个问题请教,我现在读数据库已经专门提取出来了,统统使用自定义的get_xxx(args),getlist_xxx(args)这样的方法,这已经算是分出一层了吧。但是那些数据库的写操作,有些功能的封装还是照原样放在models里面或是manager里面的……我现在想知道这个要怎么提取呢,或者说有没有必要提取呢?因为我get的时候上层实际上已经不知道是从cache还是从db里面取的了,所以我觉得貌似写的操作没必要再提出来了的样子?博主指教一下呢,呵呵。

    Reply

    1. Dante

      Dante on #

      对于写操作来说,其实我也是建议直接save就可以了。
      实现的很细呀,现在是打算做一款sns应用吗?加油!

      Reply

      1. 奥西里斯

        奥西里斯 on #

        其实是先做手机SNS应用,不过以后肯定会搬到PC端的,所以后台想一开始就做好。
        (功能其实做了一半了,所以改成博主说的这种结构也还是花了不少功夫- -)
        博主的这个方法真是让我茅塞顿开的感觉,以后再有问题还要朝博主请教,呵呵。

        Reply

  13. AA

    AA on #

    还有第3篇没?

    Reply

    1. Dante

      Dante on #

      暂时没有时间哈,等有时间了整理下:)

      Reply

发表评论