mnesia数据库优化

我一直都很想使用mnesia数据库,但mnesia的数据库一直很头疼的就是内存的占用量,我到 现在还没有办法,另外一个担心就是效率,我不知道执行到底需要占用多长时间,正好,从 另外的一个mysql上有一个不大的测试数据,可以让我检查下。

先说明下数据结构,这个数据结构很简单,是一个短信的结构,变成erlang的语法如下:

-record(sms, {
          id,
          time,
          date,
          code,
          content,
          status,
          status2
         }).

mysql中这个数据总共是55272条,我想取出其中一天(比如2014-10-31)的内容,然后根据 总量,status,status2分组,并按照一天的每一个小时进行统计,最后出现下面的三组结果:

  1. 指定一天的每个小时中所发送的短信量
  2. 指定一天的每个小时中发送成功的短信量(status=1)
  3. 指定一天中每个小时中发送失败的短信量(status2=1)

需要注意的是,源数据中,status和status2并非在0,1之间,而是可能有其他状态。我选 定的这一天总共有2327的量。

mysql中的time是按照“YYYY-MM-DD hh-mm-ss”的格式存储的,因此,如果取某一天的话,需 要使用left函数,转换成erlang就是{datetime,{{YYYY,MM,DD},{hh,mm,ss}}}。

先说下测试的环境,16G内存,i8四核,其实这个条件无论对于mysql和mnesia都是完全完美 的。

我先作了一个用于输出的测试代码:

test() ->
    io:format("use z=~p~n", [timer:tc(?MODULE, test, [z])]),
    ok.

后续的其他方法可以通过test(a),test(b),test(c)….等等来输出

test(z) ->
    Test={date, {2014, 
                 10, 
                 30}},
    F = fun() ->
                All = qlc:e(qlc:q([D||D<-mnesia:table(sms),
                              D#sms.orderdate=:= Test])),
                [All, All, All]
        end,
    {atomic, [All_sms, Sms_Succ, Sp_Succ]} = mnesia:transaction(F),
    ok;

这个是最简单的一个,只输出某一天的量,不做过滤,使用qlc的方式,据说select要比qlc 更高效些,因此也做了各使用select的等同代码:

test(y) ->
    Test={date, {2014, 
                 10, 
                 30}},
    F = fun() ->
                A = #sms{orderdate=Test, _='_'},
                All = mnesia:select(sms, [{A, [], ['$_']}], read),
                [All, All, All]
        end,
    {atomic, [All_sms, Sms_Succ, Sp_Succ]} = mnesia:transaction(F),
    ok;

简单运行一下测试,select果然要比qlc高效些, qlc关键在于表现不稳定,在我的测试中, 在8ms和26ms之间晃荡,而select一般都在8~11ms之间,以10ms为多数。

接下来是真实的测试内容了,首先第一个方案是将选择出来的sms按照一定的顺序排列,然 后根据小时筛选统计:

test(a) ->
    Test={date, {2014, 
                 10, 
                 30}},

    F = fun() ->
                A = qlc:q([D||D<-mnesia:table(sms),
                              D#sms.orderdate=:= Test]),
                B = qlc:keysort(2, A, [{order, descending}]),
                Qc = qlc:cursor(B),
                All = qlc:next_answers(Qc, all_remaining),
                qlc:delete_cursor(Qc),
                All_succ = [D||D<-All,  D#sms.status=:=1],
                ALL_SP_succ = [D||D<-All,  D#sms.sp_status=:=1],
                F1 = fun(T1) ->
                             case T1 of
                                 [] ->
                                     [];
                                 [Head|Still] -> 
                                     order_sort_hour(Still, Head#sms.ordertime, [])
                             end
                     end,
  
                All_sms = F1(All),
                Sms_succ = F1(All_succ),
                Sp_List= F1(ALL_SP_succ),
                [All_sms, Sms_succ, Sp_List]
        end,
    {atomic, [All_sms, Sms_Succ, Sp_Succ]} = mnesia:transaction(F),
    ok.
order_sort_hour([], _, Result) ->
    lists:reverse(Result);
order_sort_hour([Order|All], {datetime, {{Y, M, D}, {_HH, _MM, _SS}}}, []=Result) ->
    Time2 = Order#sms.ordertime,
    {datetime, {{Y, M, D}, {HH2, _, _}}} = Time2,
    order_sort_hour(All, Time2, [[HH2, 1]|Result]);
order_sort_hour([Order|All], {datetime, {{Y, M, D}, {HH, _MM, _SS}}}= Time, [[HH, Num]|Rest]=Result) ->
    Time2 = Order#sms.ordertime,
    {datetime, {{Y, M, D}, {HH2, _, _}}} = Time2,
    case HH2 of
        HH ->
            order_sort_hour(All, Time, [[HH, Num+1]|Rest]);
        _ ->
            order_sort_hour(All, Time2, [[HH2, 1]|Result])
    end.

这种方式测试下来,平均竟然需要50ms左右!!

改进方法,我首先觉得这个这个让qlc将结果排序不靠谱,觉得会占用时间,乱许处理会比 较快些,于是,有了下面的改进:

test(b) ->
    Test={date, {2014, 
                 10, 
                 30}},

    F = fun() ->
                A = qlc:q([D||D<-mnesia:table(sms),
                              D#sms.orderdate=:= Test]),
                Qc = qlc:cursor(A),
                All = qlc:next_answers(Qc, all_remaining),
                qlc:delete_cursor(Qc),
                All_succ = [D||D<-All,  D#sms.status=:=1],
                ALL_SP_succ = [D||D<-All,  D#sms.sp_status=:=1],
                T = [{Num, 0} || Num<-lists:seq(0, 23)],
                All_sms = sort2(All, T),
                Sms_succ = sort2(All_succ, T),
                Sp_List= sort2(ALL_SP_succ, T),
                [All_sms, Sms_succ, Sp_List]
        end,
    {atomic, [All_sms, Sms_Succ, Sp_Succ]} = mnesia:transaction(F),
    ok.

sort2([], Result) ->
    [[H, N]||{H,N}<-Result];
sort2([Order|All], Result) ->
    Time2 = Order#sms.ordertime,
    {datetime, {{Y, M, D}, {HH, _, _}}} = Time2,
    HH2 = HH+1,
    R2 = lists:nth(HH2, Result),
    {HH, Num} = R2,
    sort2(All, lists:keyreplace(HH, 1, Result, {HH, Num+1})).

测试一下,时间大幅缩短到20ms左右,果然排序比较坑阿,当然,时间的另外一个排序的排 进也会对时间有很大的影响。

于是,有了第三个版本:

test(c) ->
    Test={date, {2014, 
                 10, 
                 30}},
    F = fun() ->
                All = qlc:e(qlc:q([D||D<-mnesia:table(sms),
                              D#sms.orderdate=:= Test])),
                All_succ = [D||D<-All,  D#sms.status=:=1],
                ALL_SP_succ = [D||D<-All,  D#sms.sp_status=:=1],
                T = [{Num, 0} || Num<-lists:seq(0, 23)],
                All_sms = sort2(All, T),
                Sms_succ = sort2(All_succ, T),
                Sp_List= sort2(ALL_SP_succ, T),
                [All_sms, Sms_succ, Sp_List]
        end,
    {atomic, [All_sms, Sms_Succ, Sp_Succ]} = mnesia:transaction(F),
    ok;

乍一看,c的版本和b的版本没太大区别,但c的版本普遍比b的版本要快平均7ms,也就是 13ms左右,看来,使用数据库的游标还是有代价的。

最起始的时候,我们说select要比qlc高效,于是,就有了d的版本:

test(d) ->
    Test={date, {2014, 
                 10, 
                 30}},
    F = fun() ->
                A = #sms{orderdate=Test, _='_'},
                All = mnesia:select(sms, [{A, [], ['$_']}]),
                All_succ = [D||D<-All,  D#sms.status=:=1],
                ALL_SP_succ = [D||D<-All,  D#sms.sp_status=:=1],
                T = [{Num, 0} || Num<-lists:seq(0, 23)],
                All_sms = sort2(All, T),
                Sms_succ = sort2(All_succ, T),
                Sp_List= sort2(ALL_SP_succ, T),
                [All_sms, Sms_succ, Sp_List]
        end,
    {atomic, [All_sms, Sms_Succ, Sp_Succ]} = mnesia:transaction(F),
    ok;

但结果,有些出乎我的意料,d的版本在大部分时间都比c的版本要长,虽然不多,就不到 1ms,但终究是长了,总之,没有意料中的快。但是否是最快的,这个就不敢保证了,反正 拼着不折腾不舒服斯基的小强精神,我做了下面的版本:

test(e) ->
    Test={date, {2014, 
                 10, 
                 30}},
    F = fun() ->
                All = qlc:e(qlc:q([D||D<-mnesia:table(sms),
                              D#sms.orderdate=:= Test])),
                All_succ = [D||D<-All,  D#sms.status=:=1],
                ALL_SP_succ = [D||D<-All,  D#sms.sp_status=:=1],
                T = [{Num, 0} || Num<-lists:seq(0, 23)],
                sort3(All, T, T, T)
        end,
    {atomic, [All_sms, Sms_Succ, Sp_Succ]} = mnesia:transaction(F),
    ok.
sort3([], Result, Result2, Result3) ->
    [[[H, N]||{H,N}<-Result], [[H, N]||{H,N}<-Result2], [[H, N]||{H,N}<-Result3]];
sort3([Order|All], Result, Result2, Result3) ->
    Time2 = Order#sms.ordertime,
    {datetime, {{Y, M, D}, {HH, _, _}}} = Time2,
    HH2 = HH+1,
    R2 = lists:nth(HH2, Result),
    {HH, Num} = R2,
    S1 = case Order#sms.status of
             1 ->
                 1;
             _ ->
                 0
         end,
    S2 = case Order#sms.sp_status of
             1 ->
                 1;
             _ ->
                 0
         end,
    sort3(All, lists:keyreplace(HH, 1, Result, {HH, Num+1}), lists:keyreplace(HH, 1, Result2, {HH, Num+S1}), lists:keyreplace(HH, 1, Result3, {HH, Num+S2})).

e的版本将三个的整理放到一起进行,直接说结果吧,比d,c要慢,比b要快,因此,目前最 快就是c和d了,但c和d使用的记录都是整条记录,那么如果记录减少会不会影响呢。于是我 再次改进了下:

test(f) ->
    Test={date, {2014, 
                 10, 
                 30}},
    F = fun() ->
                A = #sms{orderdate=Test, ordertime='$1', status='$2', sp_status='$3', _='_'},
                All = mnesia:select(sms, [{A, [], ['$$']}], read),
                All_succ = [[{D, S1, S2}]||[{D, S1, S2}]<-All, S1=:=1],
                ALL_SP_succ = [[{D, S1, S2}]||[{D, S1, S2}]<-All, S2=:=1],
                T = [{Num, 0} || Num<-lists:seq(0, 23)],
                All_sms = sort4(All, T),
                Sms_succ = sort4(All_succ, T),
                Sp_List= sort4(ALL_SP_succ, T),
                [All_sms, Sms_succ, Sp_List]
        end,
    {atomic, [All_sms, Sms_Succ, Sp_Succ]} = mnesia:transaction(F),
    ok.
sort4([], Result) ->
    [[H, N]||{H,N}<-Result];
sort4([[Time2, _, _]|All], Result) ->
    {datetime, {{Y, M, D}, {HH, _, _}}} = Time2,
    HH2 = HH+1,
    R2 = lists:nth(HH2, Result),
    {HH, Num} = R2,
    sort4(All, lists:keyreplace(HH, 1, Result, {HH, Num+1})).

然后,奇迹来了,这个操作在大部分时候,仅比y的操作慢不到1ms,也就是说,大部分时候, 3个操作的过滤使用了不到1ms的时间。

是否还有更快的操作,应该还有吧,但我已经没有太大兴趣测试了。

mochiweb与cowboy使用json

服务器原来的socket实现机制更改为ranch了,这样可以专注维护自己的事情了。其实,之 所以采用ranch是因为我使用bigwig来在网页中对状态进行监控,而bigwig使用的是cowboy 和ranch,cowboy也使用的是ranch,这样,代码中就存在了两个http的框架,mochiweb和 cowboy,而看了ranch的结构,感觉很不错,就将socket服务器也转换为基于ranch架构了。 mochiweb单独一个模块来进行http服务感觉重复,因此就决定将mochiweb从模块中去除,转 为cowboy。

一切基本都很顺利,但服务器和网页的数据交互总是有问题,数据交互使用的是json格式, mochiweb带有自己的将erlang数据转换为json的方法,cowboy使用的jsx,代码分别为

mochijson2:encode([{success, true}, {root, []}]),
jsx:term_to_json([{success, true},{root, []}]),

看上去很一致吧,我也以为不会有什么大问题,然后碰到了下面的格式

Data=[{"ID", 1},
      {<<"name">>, <<"zhangsan">>
}].

mochiweb侧输出的是:

“{\”ID\”:1,\”name\”:\”zhangsan\”}”

而jsx侧输出的是:

“[{\”_type\”:\”tuple\”,\”data\”:[\”ID\”,1]},{\”_type\”:\”tuple\”,\”data\”:[\”name\”,\”zhangsan\”]}]”

如果想要和mochiweb输出表现一致的话,Data需要使用下面的方式:

% 方法1(推荐)
Data=[{<<"ID">>, 1},
      {<<"name">>, <<"zhangsan">>
}].
%方法2
Data2=[{'ID', 1},
      {'name', <<"zhangsan">>
}].

很明显不是我想要的数据,通过调查,发现jsx有以下的限制:

作为json索引的字段名称,不能是字符串list,如果为atom或者binary,则和mochiweb的输出 一致,也基本上和理解上一致,如果为字符串list,则jsx认为这个是一个数组,这可能是 因为erlang并没有真正意义上的字符串,字符串在erlang中是用list表示的吧。

另外一种链接到erlang节点的方法

在使用rebar完全清除老的内容,并且生成新的程序,然后启动之后,发现 attach 怎么也 没办法连上了,即使ping也没办法连通。提示类似下面的错误:

** System NOT running to use fully qualified hostnames **
** Hostname test_server is illegal **
** ERROR: Shell process terminated! (^G to start new job) **

最后找到了原因,是因为启动的时候,我用的节点的名称是test_server,是个短名,但参 数的格式写成了-name test_server,造成在连接的时候必须使用长名的格式,而目标的却 是个短名格式。虽然kill掉之后,修改完再启动就可以了。但查找资料过程中,在 stackoverflow上找到了另外一种连接的方式,可以连上节点:

erl -sname test -setcookie *COOKIE* -remsh n1@hostanme

-remsh是短名的格式,这样可以连接上节点,并且执行节点中的操作,只是节点代码中的 io:format之类的输出,并不会在新的节点上显示。

mnesia使用中文创建数据库并支持REST操作

这个不是什么高深的内容,记录下来,给自己提个醒。

起源于我想给我的所有数据库添加一个统一的操作接口,当然,使用英
文会更方便,更批量化,代码更少等等,我在弄的时候突发奇想,使用中
文吧,这样网页中看着连想都不用想,而且还可以给别人看,连教育部都
不支持中文了,杂家也随下大流。

其实erlang的record完全支持中文的声明,比如下面的例子:

      -record('公告',
        {'ID', %ID
         '标题' = "", % 标题
         '内容' = "", % 描述
         '显示' = true% 禁用
        }).
      

只所以可以成功是因为单引号的字符串,在erlang中就是作为一个atom
类型的,如果你想给里面加个空格什么的,也没问题。当然,使用的时候也
要加上单引号。

其实,erlang的record使用中文的话,也就上面的这些。但如果在shell
中使用,就比较烦人了,在命令行下使用下面的语句:

     mnesia:transaction(fun() -> mnesia:match_object('公告') end)).
     

会出现:illegal atom 的错误,或许修改环境变量的编码可以修改,我
没有测试。

接下来是http的REST接口,一般都使用的是表格显示,我喜欢使用JSON数
据格式,这个就更简单些了。还是上面的record:

继续阅读mnesia使用中文创建数据库并支持REST操作

我为什么要用erlang来做服务器端

刚和朋友交流,就服务器端的开发语言选择,做了些讨论。这里记录一
下概要。

按照一般的理解,我用C/C++做服务器端会更符合我的习惯,因为我对
C/C++要比erlang要熟悉,要更顺手,而且,也用C/C++弄过服务器,设计
过基于C/C++的服务器架构。但我个人还是更愿意选择erlang

首先是设计方便,大约在07年还是08年的时候,那个时候在konami弄的
移植用的软件架构消停的时候,有一段空闲时间,就开始考虑快速的服务
器开发架构这块,当时是想采用lua或者其他的比如python的,因为对lua
更了解些,就写了个使用lua脚本的架构,但随着想要的越来越多,发现我
的架构并不是我所需要的,我需要的是一个足够的灵活,足够的高效,修
改需要可以实时反映出来,而且,还要像搭积木一样,可以随意组合,这
些内容都没办法满足。然后,碰到了erlang,忘了是在哪个网站,反正是
在看技术相关的文章的时候看到的,我立刻就被erlang所宣称的特性吸引
了,这正是我想要的一个底层平台,我所要的就是一个这样支持我瞎折腾
的平台。

其次,学习并没有想象中的难,大约用了1周的时间,学习完erlang的
内容(Programming Erlang,当时还没有中文),然后开始设计一个基本的
模型,然后再设计,设计,直到我稍微满意。从顺手程度而言,要比lisp
顺手的多,我当初使用C/C++设计的时候,是有考虑过lisp的,我用的是
emacs,所以多多少少要会些lisp,但一看到lisp中别人写的库,我就感到
崩溃,没有匹配高亮我真的不知道那些高手如何将那么多的括号匹配上的,
因此我也一直没好意思对别人说我也会lisp的,扯远了些,总之,相比
lisp,erlang更适合我的学习。

在几十次的试验之后,包括基于yaws,mochiweb的页游,最后总算有了
自己的想法,熟悉了使用erlang思考问题的方式,开始设计一个一个的模
块结构。

因此,erlang就是被我用来瞎折腾的,用来验证我的设计思路以及想法
的,我也因此不断的思考如何用erlang来快速设计想要的内容,而erlang
方面,第三方库也越来越多,相比使用C/C++,验证这些内容,都要进行大
量的编码。下面列出部分使用erlang做的试验:

页游: 使用yaws和mochiweb都设计过,yaws的好处就是可以把erlang
当成php用,mochiweb呢,就一个web框架,想做什么随你,问题是我对网
页前端的设计不熟,所以就随便试试,主要关注在大量用户的情况下,
浏览器在进行http连接服务器的时候,服务器如何区分各个独立的用户以
及这些用户的行为。

股票数据抓取: 这个就无聊了,当时就想看看新浪股票的数据能不能
用erlang来分析,同时想看下如果发送很频繁的时候数据是不是实时变化
的,总之,结果忘了。

电话号码抓取: 前些年我用android手机的时候,拨长途使用IP拨号便
宜,但试了几个软件都不理想,我只想要一个IP拨号功能就行了,顶多加
上归属地,但软件总有一些其他的,比如广告,比如个头太大,比如总是
更新,烦啊,就在股票数据抓取的基础上,写了个电话号码的抓取,还专
门写了个10多K的ip拨号的android软件,专门定制了android手机系统,
不过,最后这套内容不知道在哪个硬盘中,总之没有找到了。当时抓取是
从几个网站上同时抓取的,有些有时间限制,有些会出现不同的干扰信息,
如果用C/C++来做这些处理的话,每次都要中断下,使用erlang就方便多了,
出现错误的时候,直接改,改到正确为止,不同的网站有不同的限制,因
此,我设计了好几种模式,如果发现了时间限制,则切换代理,如果一个
网站可以快速抓取的话,在自己分配的数据完成之后,再自动对所有任务
分配,总之,不死不休。

网络负载测试: 测试erlang在不同连接数之下的内存占用量,当时,
有一个现成的C++服务器以及mysql数据库,也头一次看到了erlang的NIF,
就想试试NIF内容,然后就用了几个小时做了个小设计,erlang做网络连接
的前段,原来的C++代码通过NIF做数据处理的后端,同时连接数据库。我
通过一台笔记本,连接到erlang服务器,总之,笔记本当初在启动到12K的
连接的时候,再也开动不了新的了。反观服务器,超轻松,反正玩的目的
达到了,也不追求极限,够用即可。

网游服务器: 这个设计过好多个版本,有些是测试看是否可以设计的,
有些是做教学用途的,有些纯粹是看着别人的好玩,模仿用途的,
总之,杂七杂八一大堆,当然,还有一个因素是这样就不用去学mysql了。

网络爬虫: 写了个优酷的网络爬虫,结果爬了优酷的几百万个网页之
后,虚拟机内存小,爬不动了,开了就说内存不足gameover了,虽然可以
改成磁盘也可以,不过,反正这些数据对我没有太大用途,爬了也没用,
只是验证下使用erlang设计更方便还是python更方便, 而且,对于这种大
网站,有些页面可能会出现一些不一致的内容,不过,在后来的bilibili
的网站上,发现了更多的不一致,觉得还是优酷的规范些,但一旦出现了
一些问题,总可以不用关闭直接修改,然后就解决问题了,从这一点上而
言,我觉得比python要好些,而且,对于抓取的内容格式什么的进行后期
处理,不满意也可以随时修改。

webshell: 偶然想法,通过web来管理所有的基于erlang的内容,结果
发现已经有人弄了,拿过来一用,总之,在不同的方式上有些问题,比如
本地什么的就没有问题,通过nginx中转,端口映射什么的就没办法,修修
改改,结果直接在服务器上使用了。

smtp发邮件: 服务器出现了异常怎么也得发封邮件吧,结果试试,没
问题,可以做到。

bitcon服务器: 这个是突然的一个想法,觉得erlang社区现在发展的
也还不错,结果果然有这方面的内容,结果,可怜的树莓Pi编译了一个晚
上才将bitcon的代码编译完,算了,不折腾树莓Pi了。

以上只是部分使用erlang折腾的而且可以公开讨论的内容, 其他一些相
关的就没办法说了。总之,erlang就是供我瞎折腾的。