植臻

谦虚、热情、简单、极致

全球同服游戏的数据库层怎么设计

| Comments


我们要做一个牛逼的产品!

老大最近说公司要做一款百万级DAU的产品,考虑服务器端承对数据库的读写压力,需要一个数据库的优化方案。有一同事说准备用mnesia分布式,然后问我们会不会有性能问题,仔细思考了一下,感觉这位同学并没有找准解决方向。

数据库集群解决什么问题

并行数据库系统的目标是充分发挥并行计算机的优势,利用系统中的各个处理机结点并行完成数据库任务,提高数据库系统的整体性能。

分布式数据库解决什么问题

分布式数据库系统主要目的在于实现场地自治和数据的全局透明共享,而不要求利用网络中的各个结点来提高系统处理性能。


mnesia的分布式是提供了一个分布式数据库的解决方案,然而他并不适用于解决百万级用户量数据库读写的性能问题。所以说,那位同学没有找到解决问题的方向。

简单了解一下mnesia的分布式

  • 适用范围:
    较低负载情况下,需要全局透明的数据,比如全局排行榜之类的数据。
  • 优势:
    erlang原生支持,使用方便,开发速度较快,问题容易排查。
  • 问题:
    mnesia分布式是一个全联通网络,节点间通信成本与节点关系是:
    n(n-1)/2,一旦数据量大,节点多,IO通信压力巨大。

实例:

-module(db_sync).

-export([create_schema/0, create_table/0, i/0]).
-export([add_account/3, del_account/1,
        read_account/1]).

-record(account, {id = 0, name = "", phone =
        13800000000}).

create_schema() ->
net_kernel:connect('two@MacAir'),
io:format("Self:~w, Connect
        Nodes:~w",[node(),
        nodes()]),
mnesia:create_schema([node()|nodes()]).

create_table() ->
    mnesia:create_table(account,
            [{disc_copies, [node()|nodes()]},
            {attributes,
            record_info(fields,
                account)}]
            ).

i() ->
mnesia:system_info().

add_account(ID, Name, Phone) ->
mnesia:transaction(fun() ->
        mnesia:write(#account{id
            = ID, name
            = Name,
            phone =
            Phone})
        end).

del_account(ID) ->
    mnesia:transaction(fun() ->
            mnesia:delete({account,
                ID})
            end).

read_account(ID) ->
    mnesia:transaction(fun()
            ->
            mnesia:read({account,
                ID})
            end).

xterm1中

erl -pa ebin -sname two -mnesia dir "two"

xterm2中

erl -pa
ebin -sname one -mnesia dir "one"
db_sync:create_schema().

xterm1,xterm2中分别:

mnesia:start().

任意节点创建表:

db_sync:create_table().

one节点插入数据:

db_sync:add_account(2, "zhizhen", 18588748984).

two节点查找数据:

db_sync:read_account(2).

这就是mnesia的分布式全联通节点,它已经在底层把数据同步了。在应用层面,就比较简单了。所以说,它解决的,是一个数据节点共享的问题。mnesia分布式甚至对节点间底层通信带宽要求很高,分布式节点最好处于同一机房内。

那么百万级DAU的数据库怎么设计呢?

答案是水平分片(sharding) + 垂直分片,我们今天重点讲水平分片。

  • 适用范围: 百万级甚至千万级大数据情况通用解决方案 表的查询方式单一简单,最好是有唯一主键查询 不做联表事务查询
  • 问题: 如果有事务的话,涉及到分布式事务,是非常复杂的

简单的水平切分,hash

我们一般将大表的唯一键值作为hash的key,比如我们如果准备拆分一张3千万数据的表,做完hash之后,分插入3个分片(sharding)中。

simple_hash(Item) ->
    case Item rem 3 of
        0 ->
            %insert data into user_table (ip:127.0.0.1)
        1 ->
            %insert data into user_table (ip:127.0.0.2)
        2 ->
            %insert data into user_table (ip:127.0.0.3)
    end.

这时候,随着业务的增长,如果数据涨到5千万了,慢慢地发现3个sharding已经不能满足我们的需求了,这个时候,如果打算再增加两个sharding,我们需要怎么做呢?
这个时候我们需要根据新的hash规则把数据重新导入到5个sharding中,几乎5千万行数据都要移动一遍。假设mysql美秒钟的插入速度快达2000/s,即使这样的速度,也要让服务暂停8个小时左右。这个时候DBA肯定会跟你急的,因为他需要通宵导数据。
那有没有一种更好的办法,降低增加分片的成本呢?

一致性hash

借用David Wheeler一句名言:

All problems in computer science can be solved by another level of indirection.

是的,任何计算机相关的问题,都可以通过增加一层来解决。一致性hash就是实现了这个虚拟层。erlang一个一致性hash的开源实现:hash_ring
有了hash_ring 之后,增加2个sharding就比较简单了,下面部分是伪代码:

-module(hash).
-compile(export_all).

-define(PRINT(I, P), io:format(I, P)).

start() ->
    Nodes = hash_ring:list_to_nodes([
            '127.0.0.1', 
            '127.0.0.2',
            '127.0.0.3'
        ]),
    Ring0 = hash_ring:make(Nodes),
    erlang:put(ring, Ring0).

get_nodes() ->
    Ring = erlang:get(ring),
    hash_ring:get_nodes(Ring).

add_node(Name) ->
    Ring0 = erlang:get(ring),
    Ring1 = hash_ring:add_node(hash_ring_node:make(Name), Ring0),
    erlang:put(ring, Ring1),
    %%新添加node时,对数据进行移动
    Fun = fun(I) ->
            OldServer = hash_ring:collect_nodes(I, 1, Ring0),
            NewServer = hash_ring:collect_nodes(I, 1, Ring1),
            if OldServer =/= NewServer ->
                    %todo delete data from old server
                    %todo insert data into new server
                    todo;
                true ->
                    todo
            end
    end,
    lists:foreach(Fun, lists:seq(1, 5000)).

insert(Item) ->
    Ring0 = erlang:get(ring),
    [Node] = hash_ring:collect_nodes(Item, 1, Ring0),
    ?PRINT("insert ~p into node ~p ~n", [Item, Node]).

simple_hash(Item) ->
    case Item rem 3 of
        0 -> 
            %insert data into user_table (user table 0 ip:127.0.0.1)
            todo;
        1 -> 
            %insert data into user_table (user table 1 ip:127.0.0.2)
            todo;
        2 ->
            %insert data into user_table (user table 2 ip:127.0.0.3)
            todo
    end.

这样的话,增加2个sharding之后,只需要移动2千万条数据到新的sharding上即可。

参考文章

以此为起点

| Comments


There are some birds that you can’t lock,their every feather on their body is sparkling with freedom —《The Shawshank Redemption》


来自《创游记》。

我与街机

| Comments


The farther backward you can look, the farther forward you will see.
——Winston Churchill


不知道该从哪写起,真的忘了从哪开始,但是记得清楚,自那之后,一切都变了
某一天,拿着零花钱去买冰棒,要是当初,把钱买了冰棒。。。。。。

Erlang-mysql-driver

| Comments

历史

erlang-mysql-driver 是Yariv Sadan 从Yxa这个数据库引擎的ejabberd这个分支里fork出来的一个项目,他(Yariv Sadan)把它做成了一个独立项目,并给他起了一个高大上的名字。之后便挂在Google Code 上。

在Yariv Sadan去Facebook工作之前,他给加上了高级的prepared statements 和transactions 机制。并且修复了Yxa 版本之前落后的连接池问题。

PlistBuddy修改Xcode工程版本号

| Comments

上一篇博客整得那么蛋疼,其实是想修改xcode工程的版本号,也就是plist文件里的这行:

<key>CFBundleShortVersionString</key>
<string>1.0</string>

后来发现mac下直接有现成工具可用:

/usr/libexec/PlistBuddy -c 'Set :CFBundleShortVersionString 1.0.4' Info.plist

Mac Sed 替换搜索到文本的下一行

| Comments

mac下用sed通过正则表达式实现文件中文本替换与linux还不一样,今天遇到一个很奇怪的需求,需要替换搜索到文本的下一行… 举个例子,比如有一个test文件,内容是:

version
1.0.1
XXX
version
1.0.2
XXX
version
1.0.3

一个人要经历3次成长

| Comments

  • 第一次在,发现自己不是世界中心的时候

  • 第二次在,发现即使再怎么努力,终究还是有些事令人无能为力的时候

  • 第三次在,明知道有些事无能为力,但还是开始尽力争取的时候

阅读skynet

| Comments

一直在关注云风大神的skynet,大神已经写了21篇关于skynet设计以及 优化的博客了。
云风关于skynet的介绍说了,skynet主要还是参照了erlang的 服务器异步编程思想,鉴于做过erlang开发的缘故,我比较能理解他博客里面 关于设计思想方面的说明。
不过c根基薄弱,加上也比较懒惰,一直没认真读代码,不过skynet主要部分 代码并不多,代码跟设计一样飘逸,是深入学习c的好教材。