Erlang的map性能测试

map

Erlang在R17之后,支持了map,并且在R18用hash改进了实现。

使用场景

各种option的配置,比如mysql启动参数。监控树的ChildSpec(官方已用map)。还有一种,就是我个人觉得拿来做索引用,因为lists:keyfind太不靠谱了,k就是id,v就是对象(record)。其他场景,请大家发挥想象。

性能

测试项目地址

https://gitcafe.com/Roowe/erlang-map-test

主要对比了几种Erlang常用的kv做法。

测试结果(R18环境)

find表示可以找到的查找,unfind表示找不到,update表示找得到的更新,add表示找不到要添加。

100条规模

{{dict_add,100},[{min,0.46},{max,1.88},{arithmetic_mean,0.6275999999999998}]}
{{dict_find,100},[{min,0.26},{max,0.72},{arithmetic_mean,0.32360000000000005}]}
{{dict_unfind,100},[{min,0.24},{max,0.44},{arithmetic_mean,0.27279999999999993}]}
{{dict_update,100},[{min,0.48},{max,14.82},{arithmetic_mean,0.8868}]}

{{list_add,100},[{min,2.54},{max,3.56},{arithmetic_mean,2.8532000000000006}]}
{{list_find,100},[{min,0.18},{max,0.54},{arithmetic_mean,0.218}]}
{{list_unfind,100},[{min,0.22},{max,0.34},{arithmetic_mean,0.2512000000000001}]}
{{list_update,100},[{min,1.22},{max,4.98},{arithmetic_mean,1.6080000000000003}]}

{{map_add,100},[{min,0.12},{max,0.26},{arithmetic_mean,0.168}]}
{{map_find,100},[{min,0.1},{max,0.5},{arithmetic_mean,0.17919999999999994}]}
{{map_unfind,100},[{min,0.06},{max,0.12},{arithmetic_mean,0.08160000000000006}]}
{{map_update,100},[{min,0.14},{max,1.28},{arithmetic_mean,0.2156000000000001}]}

10000条规模

{{dict_add,10000},[{min,0.56},{max,1.68},{arithmetic_mean,0.76}]}
{{dict_find,10000},[{min,0.74},{max,1.22},{arithmetic_mean,0.8836000000000002}]}
{{dict_unfind,10000},[{min,0.24},{max,1.22},{arithmetic_mean,0.3724}]}
{{dict_update,10000},[{min,0.64},{max,6.8},{arithmetic_mean,0.9667999999999997}]}

{{list_add,10000},[{min,255.66},{max,651.9},{arithmetic_mean,471.7027999999999}]}
{{list_find,10000},[{min,13.38},{max,149.74},{arithmetic_mean,84.8036}]}
{{list_unfind,10000},[{min,24.68},{max,286.96},{arithmetic_mean,203.0548}]}
{{list_update,10000},[{min,118.78},{max,458.72},{arithmetic_mean,217.12160000000003}]}

{{map_add,10000},[{min,0.26},{max,0.66},{arithmetic_mean,0.31039999999999995}]}
{{map_find,10000},[{min,0.44},{max,1.72},{arithmetic_mean,0.5456000000000001}]}
{{map_unfind,10000},[{min,0.22},{max,0.6},{arithmetic_mean,0.2772}]}
{{map_update,10000},[{min,0.3},{max,0.58},{arithmetic_mean,0.368}]}

结论

总体上,小数据规模,几百左右,三种方法都不太有性能差距,上万的话就很明显了,list就不太中用了。在R17没有优化的map测试的时候,发现依旧比list快,看了底层实现,算法复杂度都是O(n),应该就是链表和数组的差距了,离散内存访问和连续内存访问的开销不一样。

理解ibrowse的send_req

ibrowse简介

ibrowse是一个用Erlang写的http client。具有异步,多进程,管道,代理等功能,具体可以点击项目主页看看。

用途

游戏项目里面,跟渠道对SDK,一般情况是http请求,iOS的内购也是http请求。所以,有了ibrowse这个项目,可以简化我们的代码,不用自己再造轮子写http的发包和收包。

send_req

用ibrowse这个项目,也就使用ibrowse:send_req这个函数而已。

首先在send_req函数,根据Host和Port从ibrowse_lb ETS获取lb pid,如果ibrowse_lb ETS没有的话,就去ibrowse进程get_lb_pid,启动ibrowse_lb进程,并且将其pid存储到ibrowse_lb。

接着try_routing_request,通过spawn_connection(ibrowse_lb进程)获取Conn_Pid,Conn_Pid是处理收发包的进程pid。ibrowse_lb进程会根据Max_sessions(多少个http client进程)和Max_pipeline_size(一个http client进程多少个请求串行)来决定Conn_Pid,如果超负荷,就返回{error, retry_later}。

拿到Conn_Pid之后,会调用ibrowse_http_client:send_req,实际上是一个gen_server:call,底层的网络处理是异步的,所以底层发完包之后直接{noreply, State},等收到包之后才gen_server:reply,也就是实现了pipeline的原因。同时,立刻noreply之后,也可以让后面消息及时等到处理。

send_req大概也就这样了。

在Ubuntu 14.04(服务器)安装Erlang

下载安装包

可以在官网下载,如果嫌国内速度慢,也可以去我百度盘,点击这里。如果想命令行在服务器下载的话,可以直接用wget或者curl。或者去可以去又拍云的镜像http://erlang.b0.upaiyun.com/download下载(来源https://github.com/upyun/kerl),将文件名补齐就可以了。

wget -c http://www.erlang.org/download/otp_src_17.4.tar.gz 

安装依赖

apt-get install g++ gcc make libncurses5-dev  libssl-dev

因为我们项目并没用Java作为NIF,所以不需要安装Java,有需要的就先安装Java;同时因为服务器只有命令行,不需要wx这个图形库。

编译安装

 ./configure --prefix=/opt/otp/ && make -j4 && make install

指定prefix,是为了删除方便。同时在~/.bashrc将/opt/otp/bin添加到PATH。

export PATH=/opt/otp/bin:$PATH

使用

source ~/.bashrc

这样erlang就安装好了。可以将以上几个步骤整合成一个脚本,这样就无人值守安装了。

Erlang数据类型实现

国庆,没事,也不去给交通添堵,在家就看了下erlang怎么用c实现我们常用的list和tuple等基础数据类型,看了一遍源码,感觉还是能看得懂大概,毕竟没有特别高深的算法,不过有些比较绕的,就要费点心思,比如binary部分,单数据结构就有三种。反正不做OTP底层开发,看得大概也就满足了,写业务做框架做优化都会有所依据。

不过,我不打算具体讲点啥,因为内容太多,今天是国庆第四天刚过,要写完,估计我剩下三天都得在电脑前也未必搞得定。主要是记录下,一些导读的文章。

1、数据类型的内存 http://www.erlang.org/doc/efficiency_guide/advanced.html

2、siyao同学一系列介绍数据类型实现的文章

Erlang数据类型的表示和实现(1)——数据类型回顾 http://www.cnblogs.com/zhengsyao/p/erlang_eterm_implementation_1.html

Erlang数据类型的表示和实现(2)——Eterm 和立即数 http://www.cnblogs.com/zhengsyao/p/erlang_eterm_implementation_2.html

Erlang数据类型的表示和实现(3)——列表 http://www.cnblogs.com/zhengsyao/p/erlang_eterm_implementation_3.html

Erlang数据类型的表示和实现(4)——boxed 对象 http://www.cnblogs.com/zhengsyao/p/erlang_eterm_implementation_4_boxed.html

Erlang数据类型的表示和实现(5)——binary http://www.cnblogs.com/zhengsyao/p/erlang_eterm_implementation_5_binary.html

3、Erlang/erts代码笔记 - 通用Hash表 http://blog.crackcell.com/posts/2012/08/13/erts_hash.html

4、Erlang原子(atom)的内部实现及应用 http://844604778.iteye.com/blog/1959554

5、Erlang中list和tuple的构建及转换的内部实现 http://blog.csdn.net/zhongruixian/article/details/9410193

主要还是看代码,导读仅仅就是给你开个头罢了。

题外话,去年,线上服务器出了atom爆掉的问题(但是后来说又不是,是其他问题),但是我们主程(算吧)看了litaocheng的开发杂记http://erlangdisplay.iteye.com/blog/374167 ,其中有一段,“* 避免使用list_to_atom/1,因为erlang中atom数量最大为1048576, 且不进行GC控制。因此如果持续性的调用list_to_atom/1 可能很容易达到系统上限,从而导致emulator terminate。请使用list_to_existing_atom/1。 ”,然后将list_to_atom改为catch list_to_existing_atom,崩了,再list_to_atom。现在看来,atom实现是基于index和hash,如果本身出现过的,index和hash是不会变,何来达到系统上限呢?详细可以看看qingliang的[erlang]list_to_atom与list_to_existing_atom http://www.qingliangcn.com/2014/04/erlang/ 或者自己去看https://github.com/erlang/otp/blob/maint/erts/emulator/beam/bif.c 实现。

最后,知其然知其所以然。

Erlcron

实际业务中,多多少少逃不开定时作业,比如开服头七天各种活动的排名,然后通过邮件给玩家发奖励,如果erlang有crontab的话,就很爽了。配置个时间,然后就执行下函数就ok了。不然对于每个需求,都要自己开个进程去send_after或者由一个进程去管理,如果做不好的话,还是挺不好用的。

实际上,已经有人实现了,就是erlcron,https://github.com/erlware/erlcron

实现其实很简单,就是ecrn_agent.erl里面利用timeout来推动一个作业下一次执行,又是一个timeout使用示例。

erlcron有个好处,就是通过erlcron:set_datetime(DateTime).来看看实际执行的效果,起到简单测试作用。erlcron内部维护个虚假的时间,这样今天是星期一,作业是星期五执行的,那么可以将这个值改动为带周五就可以。

具体使用,看看https://github.com/erlware/erlcron 的文档就知道了,本身挺简单,这里就不多举例了。

Update Fri Sep 19 21:32:06 2014

实际业务使用,觉得erlcron太不好使了,特别是我们现在有个业务,月前25天打排位赛,之后小组淘汰赛,要开30+进程,特别不靠谱,其实开一个进程,随着时间推移,一直timeout下去就可以了。

我简单实现了个可以用的版本(有单元测试),跟erlcron差不多格式,我取名叫gs_cron,gs表示game_server。使用具体看测试代码,暂时没啥时间提供文档,https://github.com/roowe/gs_cron

多节点erlang实现排行榜

排行榜估计是个游戏都会做,云风也聊过,点击这里 查看,温馨提示,如果不想看八卦,直接跳到最后去。云风讲得也比较简单,好理解,但是实际在erlang操作细节蛮多,特别是我不想单点,要多节点。

总体上,在erlang做排行榜的话,思路还是云风那样的。

最初,我是用了mnesia的order set来维护前k个(精确的),全dirty操作,检测是否小于最后一名,否则不给进,由于并发原因会超出预定长度,适当做尾巴清理。但是后来计算排名的时候,需要遍历整表,当时规模是2W,然后取最后一名就慢太多了。ETS是百万分之一秒,遍历这么多次,每秒能处理就很有限了。大概实现看https://gist.github.com/roowe/296b2ab99fc05320188c ,hdb和mnesia接口基本一致,我们做了简单包装,加点统计信息什么的。

后来,想了下优化方案,只能从减少计算排名的时候遍历入手,怎么样可以少点遍历呢?做主排序值的计数器,用update_counter效率应该挺高的。同值排多少名,加上比这个值往前还有多少人就可以算出rank了,这样开销也就是遍历所有的主排序值(我们这个值就是1000-4000范围),从2W降到这个,基本毫无压力了。这个思路也是云风blog提到的做法,我只是加了同个主排序值的相对排名,他们家的相对排名是0,共用前面排名+1了,对于玩家来讲确实没多少区别,特别是后面的。

插曲,dirty_update_counter运行过程中是对,重启服务器之后会有数据出入,简单说,就是计数器重启之后记得不对,这个有点麻烦,目测是ets从dets和log加载数据有点问题,换成read和write就没有问题,暂时就用这个方法绕开了。

最后,准备完工的时候,当时测试,都是开一个节点一个进程,所以不会报错,也很快了。然后上多进程,一口气开了1W进程,然后mnesia狂报警告了,降到3k也是一样(稍微好点),开始出现清理尾巴资源竞争过于厉害的问题,基本让服务器处于不可用的状态。

后来,经过一夜的考虑,决定加事务去处理了,开了事务,开3k进程,跑了2min才跑完,略坑,主要是并发过于厉害,锁的问题。但是单进程又很快可以跑完,1s多。

再之后,决定挂个gen_server挡住消息来排队,然后清理也从插入分离,处理完所有消息之后,触发timeout之后才进行尾巴清理。

总体讲,多个节点,分别跑个排行榜进程,事务处理写入排行榜,触发timeout清理尾巴,性能上满足几千人在线,甚至1W在线,有兴趣就参考https://gist.github.com/roowe/a1c0aeabda42f53db608

浅谈游戏配置数据在erlang使用

ETS

在之前的手游项目(多服)的时候,我们的配置数据是从MySQL加载到ETS,然后从ETS里面lookup,加载的时候还用到string_to_term这种比较实用的方法,将配置里面的string转为erlang term,这样配置一些复杂的东西的时候,可以简化配置,算是DSL吧。比如任务系统,达成条件配成了,"{lv, 40}",表示这个任务40级的时候就可以完成了,在MySQL或者xsl里面就是字符串,但是在erlang里面就是个tuple,使用的时候,来个匹配就可以。然后有其他需求,爱怎么加就怎么加,比如我们还有强化1次,{equip_upgrade, 1},程序处理匹配就可以了。

使用心得

好处:支持全遍历和选择遍历,写个match_spec就可以了,查询也很快。

不足:每张表都需要写个load data函数,不过这个问题不大,封装下,基本就几行代码功夫,最初没封,都是复制粘帖,累。分布式多节点热更新不方便,要去每个节点触发reload data,有点不方便。

example

load_base_data(DbTable, EtsTable, HandleFun) ->
    L = db_base:select_all(DbTable, "*", []),
    hetsutil:lock(EtsTable),
    try
        hetsutil:truncate(EtsTable),
        lists:foreach(fun(Info) ->
                              RecInfo = HandleFun(Info),
                              hetsutil:insert(EtsTable, RecInfo)
                      end, L)
    catch _:R ->
            ?WARNING_MSG("load ~p failed R:~w Stack: ~p~n",[DbTable, R, erlang:get_stacktrace()])
    after
        hetsutil:unlock(EtsTable)
    end,
    ok.
 
init_base_dungeon() ->
    hmisc:load_base_data(base_dungeon, ?ETS_BASE_DUNGEON, fun handle_base_dungeon_from_db/1).
 
handle_base_dungeon_from_db(DbBase) ->
    RecBase = list_to_tuple([base_dungeon | DbBase]),
    RecBase#base_dungeon{
      reward = lib_reward:convert_common_reward(util:to_term_list(RecBase#base_dungeon.reward))
     }.

代码就不解释,大概意思就那样,不能直接用的。

生成代码

一开始,我觉得这个方式会有性能问题,而且麻烦,当时一开始同事的做法是用php写生成erlang代码的(其实就是字符串),有啥字段写啥,拼起来,很累。后来,我测试了ETS和生成代码的方式,ETS在高并发(瞬间几百或上千)的情况下,会稍微有点性能差,实际上,一般不会遇到这么BT的情况,其他情况两者差不多(印象中4倍),生成代码,编译之后内部估计也是hash,所以性能差不多。

这东西,最初我是比较反感用php写死那些字段,新加一张表,又得这样来一次,万一有100个字段,不是写数据生成的php代码,就得花上点时间。不过当时项目,我是后期加入,没写多少业务代码,没怎么被这php坑。后来,想通了,我就用erlang写了一套,上层只需要声明函数,每个字段是否需要特殊处理(转为term,或者record等),索引返回id等基础功能。这样新加表,写点定义即可,比以前有100倍+的生产力提高。

再后来,我了解有些团队是用VB生成erlang代码的(比如dcy),我面试一个主程,他的做法和dcy家一样,就是简单的get和all_id两个函数,不支持term的表示。有些东西,还是看团队需求吧,像dcy他们家那样确实简单,都不要写配置定义。

使用心得

好处:上线部署不需要外部依赖(如外部的csv,MySQL等),代码即配置。热更方便。 坏处:遍历比较麻烦,特别是像SELECT * FROM base WHERE type=1这种取某一类数据,再或者SELECT * FROM base WHERE lv>=10 ANDALSO lv=< 45,这种区间检索。不过,我后来有个比较赞的办法,就是对lv和type这个字段加人肉索引,这样可以减少遍历。

example

使用代码就不贴,写得比较搓,贴下生成的效果代码。

数据源大概就像下面这样。先简单的解释下,这是抄某个游戏的签到,reward字段在Excel里面是个字符串,格式是{BaseId, Num}。如果把它当成字符串的话,erlang代码就像reward = "{BaseId, Num}"了,但实际上,我们不加双引号,就是成了erlang的term了。其实,这里并不是转为term,而是term的更高一层,record,这样发奖励,我不用再写一层转化。还有个配置,player_card是跟策划约定好去另外一张表找BaseId,因为数量相同,只是每个月BaseId不一样,即data_base_login_reward_player_card:get(hmisctime:get_month())。

这个生成的代码,dcy他们家那种是做不到的,目测他们也不想要这种坑爹的feature。

id  reward  vip_double
1   {3,5000}    1
2   {1,20}  0
3   {26000003,1}    2
4   {3,7500}    0
5   {player_card,2} 3
%% Warning:本文件由data_generate自动生成,请不要手动修改
-module(data_base_login_reward).
 
-export([get/1]).
 
-include("common.hrl").
 
-include("define_reward.hrl").
 
-include("db_base_login_reward.hrl").
 
get(1) ->
    #base_login_reward{id = 1,
               reward = #reward_item{base_id = 3, num = 5000},
               vip_double = 1};
get(2) ->
    #base_login_reward{id = 2,
               reward = #reward_item{base_id = 1, num = 20},
               vip_double = 0};
get(3) ->
    #base_login_reward{id = 3,
               reward = #reward_item{base_id = 26000003, num = 1},
               vip_double = 2};
get(4) ->
    #base_login_reward{id = 4,
               reward = #reward_item{base_id = 3, num = 7500},
               vip_double = 0};
get(5) ->
    #base_login_reward{id = 5,
               reward =
               #reward_item{base_id =
                        data_base_login_reward_player_card:get(hmisctime:get_month()),
                    num = 2},
               vip_double = 3};
get(6) ->
    #base_login_reward{id = 6,
               reward = #reward_item{base_id = 3, num = 10000},
               vip_double = 4};
get(7) ->
    #base_login_reward{id = 7,
               reward = #reward_item{base_id = 1, num = 40},
               vip_double = 0};
get(8) ->
    #base_login_reward{id = 8,
               reward = #reward_item{base_id = 26000003, num = 2},
               vip_double = 5};
get(9) ->
    #base_login_reward{id = 9,
               reward = #reward_item{base_id = 3, num = 15000},
               vip_double = 0};
get(10) ->
    #base_login_reward{id = 10,
               reward =
               #reward_item{base_id =
                        data_base_login_reward_player_card:get(hmisctime:get_month()),
                    num = 3},
               vip_double = 6};
get(11) ->
    #base_login_reward{id = 11,
               reward = #reward_item{base_id = 3, num = 15000},
               vip_double = 7};
get(12) ->
    #base_login_reward{id = 12,
               reward = #reward_item{base_id = 1, num = 60},
               vip_double = 0};
get(13) ->
    #base_login_reward{id = 13,
               reward = #reward_item{base_id = 26000003, num = 3},
               vip_double = 8};
get(14) ->
    #base_login_reward{id = 14,
               reward = #reward_item{base_id = 3, num = 22500},
               vip_double = 0};
get(15) ->
    #base_login_reward{id = 15,
               reward =
               #reward_item{base_id =
                        data_base_login_reward_player_card:get(hmisctime:get_month()),
                    num = 4},
               vip_double = 9};
get(16) ->
    #base_login_reward{id = 16,
               reward = #reward_item{base_id = 3, num = 20000},
               vip_double = 10};
get(17) ->
    #base_login_reward{id = 17,
               reward = #reward_item{base_id = 1, num = 80},
               vip_double = 0};
get(18) ->
    #base_login_reward{id = 18,
               reward = #reward_item{base_id = 26000003, num = 4},
               vip_double = 11};
get(19) ->
    #base_login_reward{id = 19,
               reward = #reward_item{base_id = 3, num = 30000},
               vip_double = 0};
get(20) ->
    #base_login_reward{id = 20,
               reward =
               #reward_item{base_id =
                        data_base_login_reward_player_card:get(hmisctime:get_month()),
                    num = 5},
               vip_double = 12};
get(21) ->
    #base_login_reward{id = 21,
               reward = #reward_item{base_id = 3, num = 40000},
               vip_double = 13};
get(22) ->
    #base_login_reward{id = 22,
               reward = #reward_item{base_id = 1, num = 100},
               vip_double = 0};
get(23) ->
    #base_login_reward{id = 23,
               reward = #reward_item{base_id = 26000003, num = 5},
               vip_double = 14};
get(24) ->
    #base_login_reward{id = 24,
               reward = #reward_item{base_id = 3, num = 40000},
               vip_double = 0};
get(25) ->
    #base_login_reward{id = 25,
               reward =
               #reward_item{base_id =
                        data_base_login_reward_player_card:get(hmisctime:get_month()),
                    num = 7},
               vip_double = 15};
get(26) ->
    #base_login_reward{id = 26,
               reward = #reward_item{base_id = 3, num = 50000},
               vip_double = 15};
get(27) ->
    #base_login_reward{id = 27,
               reward = #reward_item{base_id = 1, num = 120},
               vip_double = 0};
get(28) ->
    #base_login_reward{id = 28,
               reward = #reward_item{base_id = 26000003, num = 5},
               vip_double = 15};
get(29) ->
    #base_login_reward{id = 29,
               reward = #reward_item{base_id = 3, num = 50000},
               vip_double = 0};
get(30) ->
    #base_login_reward{id = 30,
               reward =
               #reward_item{base_id =
                        data_base_login_reward_player_card:get(hmisctime:get_month()),
                    num = 9},
               vip_double = 15};
get(31) ->
    #base_login_reward{id = 31,
               reward = #reward_item{base_id = 3, num = 50000},
               vip_double = 15};
get(Var1) ->
    ?WARNING_MSG("get not find ~p", [{Var1}]), [].

最后还是贴下,我们生成这个的上层代码,生成的代码看似比较碉堡,不过确实写不了多少代码。

base_login_reward() ->
    RewardFun = fun(RewardStr) ->
                        {NewBaseId, NewNum} =
                            case bitstring_to_term(RewardStr) of
                                {player_card, Num} ->
                                    {"data_base_login_reward_player_card:get(hmisctime:get_month())", Num};
                                {BaseId, Num} ->
                                    {BaseId, Num}
                            end,                        
                        lists:concat(["#reward_item{base_id = ", NewBaseId ,
                                      ", num = ", NewNum, "}"])
                end,
    Fields = trans_to_fun(record_info(fields, base_login_reward), [{reward, RewardFun}]),
    [base_login_reward, base_login_reward, ["define_reward.hrl",
                                            "db_base_login_reward.hrl"],
     [default_get_generate_conf(Fields, id)]].

关于人肉索引,其实是这样的。在上层代码,检索出满足的lv之后,拿到lv去换出ids,然后这些就是ids就是满足条件的。type更是简化后的,一个type对应一群ids。

ids_by_lv(1) -> [6, 5, 4, 3, 2, 1];
ids_by_lv(5) -> [12, 11, 10, 9, 8, 7];
ids_by_lv(11) -> [18, 17, 16, 15, 14, 13];
ids_by_lv(15) -> [24, 23, 22, 21, 20, 19];
ids_by_lv(20) -> [29, 28, 27, 26, 25];
ids_by_lv(25) -> [34, 33, 32, 31, 30];
ids_by_lv(Var1) ->
    ?WARNING_MSG("ids_by_lv not find ~p", [{Var1}]), [].
 
all_lv() -> [1, 5, 11, 15, 20, 25].

在erlang简单使用SQL语句

代码地址

erl_mysql.erl https://gist.github.com/roowe/8840321 和 erl_mysql_tests.erl https://gist.github.com/8840329.git 具体怎么用,可以看测试代码。

为什么要有这个东西

记得我第一份实习的时候,写SQL都是直接写,然后也有些变量名大小写不规范啥的,该`的也没有。到了现在这家公司的时候,SQL是不允许直接拼写,除非特殊情况。有些同学可能觉得封不封都无所谓,写快代码的时候,倒是无所谓。否则,最好简单封下,像ORM那种就算了。否则要整天计较拼字符串的脏活。封装起来多好看嘛。

实现

公司直接封装是十分ugly的,而且支持很弱,比如写((lv=1) OR (lv=2)) AND (vip=1 OR vip=2)这种需要优先级,就没有法子,也幸好没写过那么复杂,XDDD。如果用s表达式,或许就不存在这种问题。

后来知道Sqerl,它又是基于ErlyWeb's ErlSQL的,看起来很不错。不过,有些东西我不需要。所以我在它的基础上面去掉了safe的支持和子查询,safe其实只要有字符串,没办法safe的,所以何必呢。比如nickname之类,作者的原意或许是是支持自定义的WHERE,我觉得挺好,但是说是不safe,有点那啥,并引入不必要的复杂性。简单的单元测试覆盖,写这种测试太烦了,所以点到而止,出bug再看看了。

设计上,应该是支持SQL语法,日后有需要再扩展。

代码

erl_mysql.erl

单元测试 erl_mysql_tests.erl

emysql与erlang-mysql-driver的pool模型

erlang-mysql-driver的pool

erlang-mysql-driver本身说是有pool(start_link再connect,mysql_dispatcher维护的是进程pid和emysql不一样),但实际上等于没有。因为一般fetch的时候call_server({fetch, PoolId, Query}, Timeout),然后mysql_dispatcher只有一个,这样大家都往里面发消息,然后等,这样发消息的进程太多的话,就会出现timeout的问题。尽管mysql_dispatcher背后是多进程,这样是没有意义的。大家可以去mysql_conn的send_msg(Pid, Msg, From, Timeout)加上io:format("~p~n", [{Pid, Msg, From, self()}]),看看输出什么就明白了。

那如何在erlang-mysql-driver使用pool呢?正确做法是use mysql_conn stand-alone,然后配合poolboy(https://github.com/devinus/poolboy),详细做法可以翻下https://github.com/ChicagoBoss/boss_db 的db启动和fetch部分。这里不详述了,因为我现在不用这个做法了,毕竟我不需要像boss_db那样牛(蛋)逼(疼),封装了常见的数据库作为model,所以需要poolboy来配合。其实内部一坨屎,难以扩展。

emysql

这个项目是2009年,算是比较新的,自带真进程池。emysql的conn_mgr维护的时候db tcp连接socket,然后执行SQL的时候,先去emysql_conn_mgr取个可用的socket,否则根据参数选择排队还是放弃。排队那块类似poolboy的实现。取到可用socket,就spwan进程出去发包收包,等结果,真正的多进程。这是大致流程,具体细节就得看代码了。

最后

使用emysql就不用过多关心底层了,直接application:start和emysql:add_pool就可以开箱即用。

用erlang写的命令行工具

起因

看到RabbitMQ有个脚本rabbitmqctl,通过传入参数即可控制正在运行的服务,比如加入集群,修改帐户权限,查看集群状态等等。

erlang本身实现了rpc,所以通过起个进程与正在运行的erlang进程交互还是挺方便的。

看了下RabbitMQ传入参数的实现,确实有点局限性,因为是自己写的命令行参数解析,略蛋疼。

之前用python做迅雷离线下载的时候用过相关的解析args的库类,erlang可能也有,google了下,真有,叫getopt。地址 https://github.com/jcomellas/getopt 文档很详细,慢慢看吧。我好久没用getopt相关的东西都一看就明白了。

example code

将项目某个打印record信息的工具,稍微改下,作为说明例子用,test_getopt.erl。

这个文件很简单,就是通过传入参数,然后打印record相关信息。因为最近在弄mnesia结构升级的功能支持和测试,需要在代码写record的字段,所以需要有东西打印record,这样我好复制粘帖。

思路很简单,将init:get_plain_arguments()传到getopt:parse获取到{ok, {Opts, [Command | Args]}} ,然后根据这些信息做逻辑就可以了。

用erlang操作erlang,适当的话,可以提高开发效率。

对了,作者那份代码是不支持打印中文信息的,我提了patch,但是还没merge。我暂时fork出来了,有兴趣的同学可以去https://github.com/roowe/getopt 看看。

-module(test_getopt).
 
-export([start/0]).
 
-record(attri2,{
          attack = 0,
          hp = 0,          
          magic = 0,
          hit = 0,
          def2 = 0
         }).
-record(test_player,{
          id,
          type,
          sex2,
          lv,
          good_list2 = [],
          partner_list = [],
          attri2 = #attri2{}
         }).
-record(goods,{
          id,
          lv,
          attri
         }).
 
-record(partner,{
          id,
          lv,
          attri=#attri2{}
         }).
 
commands_desc() ->
    [{"field [<record_name>]", "record字段"},
     {"default [<record_name>]", "record默认值"}
     ].
opt_spec_list() ->   
    [
     {help, $h, "help", undefined, "显示帮助,然后退出"}
    ].
usage() ->
    getopt:usage(opt_spec_list(), "pr_record", "<command> [<args>]", commands_desc()),
    halt(1).
parse_arguments(CmdLine) ->
    case getopt:parse(opt_spec_list(), CmdLine) of
        {ok, {Opts, [Command | Args]}} ->
            {ok, {list_to_atom(Command), Opts, Args}};
        {ok, {_Opts, []}} ->
            no_command;
        Error ->
            io:format("Error ~p~n", [Error]),
            no_command
    end.
start() ->
    {Command, Opts, Args} =
        case parse_arguments(init:get_plain_arguments()) of
            {ok, Res}  -> 
                Res;
            no_command ->
                usage()
        end,
    %% The reason we don't use a try/catch here is that rpc:call turns
    %% thrown errors into normal return values
    case catch action(Command, Args, Opts) of
        ok ->
            io:format("...done.~n", []),
            halt(0);
        {ok, Info} ->
            io:format("...done (~p).~n", [Info]),
            halt(0);        
        Other ->
            print_error("~p", [Other]),
            halt(2)
    end.
print_error(Format, Args) -> 
    fmt_stderr("Error: " ++ Format, Args).
 
fmt_stderr(Format, Args) -> 
    io:format(standard_error, Format ++ "~n", Args).
 
 
action(field, Args, _) ->
    lists:foreach(fun(RecordNameStr) ->
                          RecordName = list_to_atom(RecordNameStr),
                          io:format("(~w) -> ~w;~n", [RecordName, get_record_fields(RecordName)])
                  end, Args),
    ok;
action(default, Args, _) ->
    lists:foreach(fun(RecordNameStr) ->
                          RecordName = list_to_atom(RecordNameStr),
                          %% Fields = get_record_fields(RecordName),
                          %% ValueList = lists:zip(Fields, lists:nthtail(1, tuple_to_list(get_record_default(RecordName)))),
                          io:format("(~w) -> ~w;~n", [RecordName, get_record_default(RecordName)])
                  end, Args),
    ok;
action(Command, Args, Opts) ->
    io:format("Command: ~p Args: ~p Opts: ~p~n", [Command, Args, Opts]),
    invalid_command.
 
-define(RECORD_FIELDS(TableName), record_info(fields, TableName)).
get_record_fields(goods) ->
    ?RECORD_FIELDS(goods);
get_record_fields(test_player) ->
    ?RECORD_FIELDS(test_player);
get_record_fields(partner) ->
    ?RECORD_FIELDS(partner);
get_record_fields(attri2) ->
    ?RECORD_FIELDS(attri2).
 
-define(RECORD_DEFAULT(RecordName), #RecordName{}).
get_record_default(goods) ->
    ?RECORD_DEFAULT(goods);
get_record_default(test_player) ->
    ?RECORD_DEFAULT(test_player);
get_record_default(partner) ->
    ?RECORD_DEFAULT(partner);
get_record_default(attri2) ->
    ?RECORD_DEFAULT(attri2).

启动脚本,如果你们ebin有专门的目录,将那个路径加到-pa进去,我这是演示,所以在/tmp了。

[20:18:51] roowe@Laptop-Y400 /tmp$ cat test_getopt
#!/bin/bash
erl -pa ~/open_source/getopt/ebin/ -noinput -hidden -name pr_record$$@127.0.0.1 -boot start_clean  -s test_getopt -extra "$@"

效果如下:

[20:15:19] roowe@Laptop-Y400 /tmp$ ./test_getopt field test_player goods
(test_player) -> [id,type,sex2,lv,good_list2,partner_list,attri2];
(goods) -> [id,lv,attri];
...done.
[20:15:50] roowe@Laptop-Y400 /tmp$ ./test_getopt default test_player goods
(test_player) -> {test_player,undefined,undefined,undefined,undefined,[],[],{attri2,0,0,0,0,0}};
(goods) -> {goods,undefined,undefined,undefined};
...done.
[20:16:01] roowe@Laptop-Y400 /tmp$ ./test_getopt --help
Usage: pr_record [-h]  []

  -h, --help               显示帮助,然后退出
  field []    record字段
  default []  record默认值