2015年年终总结

工作

工作上,依旧老样子,并无太多需要挑战,做的是休闲游戏,也没太多业务需要折腾。超级飞侠这个也算告一段落了,游戏后面被掌趣接手,年后会开始有所动作。也算忙碌了一年,是开始收获的时候吧。

总体上,过去一年,工作很顺利,没有遇到特别棘手的问题。希望来年,继续保持吧,同时希望更有激情。

新的一年,我们将会做ARPG的游戏,每和别人说起这个,大家都说,必死。想想,死就死吧,反正高收益的事情也意味着高风险吧。

感情

不知不觉已经到了结婚生娃的年纪了,每次回家,我爸就问我,什么时候打算和女朋友结婚,什么时候带他们去见女方的家长。算是间接逼婚吧。所以,2016年,会尽量去解决这个问题吧。

锻炼

过去的一年,我习惯了跑步了。我不敢说爱上,毕竟只是为了健康而已。如果整天可以坐着,不会造成有任何不适,或许,我会选择不跑步吧。

过去的一年,跑了2个马拉松距离的比赛,若干个线上半马。总体上已经超额完成了,所以,我也觉得我的身体棒棒的。

过去的一年,训练并不是很足和规范,所以跑那两次全马的目标是安全跑完,并不在乎成绩,虽然安全跑完,但后面脚踝有点受伤了。今年的目标,会加大训练量吧,周跑量要上去,之前都是跑跑停停,每次跑比赛都抱佛脚。

最后,2016年,希望可以完成两次马拉松就好了,一次全马,一次半马。去年目标是一场,稳中求进吧,毕竟再多几场也没啥意思。钱的开销倒还好,机票和注销几千块一次,主要是时间和精力不能全部放到跑步上去嘛。

其他

最近迷上了摄影,给女朋友买的微单,然后自己就拿来练手了,还打算在新的一年,入手sony旗舰的微单A7RM2,这样一来,想想自己挣的钱还真好少。开始买了书,半路出家,学那些理论基础,虽然离摄影师肯定很远很远,但是短期目标,熟悉各种理论,和玩熟悉自己手上的装备,应该还是可以的。

程序上,打算学习下iOS编程,这个也拖了好久了,希望业余的时间可以做点有趣的东西来玩玩吧。还有,新的一年里,打算多写点Blog,争取每周一更新吧,并且纯技术性的会放到http://tech.roowe.net/里去,备份在https://gitcafe.com/Roowe/Blog/,这样一来,这些东西就不会丢了,但是碎碎念的东西就放到http://www.iroowe.com/,丢了就无所谓咯。

新的一年祝各位事事顺利。

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),应该就是链表和数组的差距了,离散内存访问和连续内存访问的开销不一样。

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

前生

一年前,我写了篇浅谈游戏配置数据在erlang使用,一年后,我在同事的提示下,我又简化实现,甚至可以无限扩展了,原先拼字符串会有种种局限性。

原理

本质上,配置数据转代码而已,然后可以将数据用erl_syntax的接口表示成代码的AST,接着用format接口转化成具体的代码。这中间使用到了erlang的AST,理论上,你想生成什么样的erlang代码都可以,毕竟erl_syntax是一个erlang完备的语法树。

example

这里只是实验性代码,具体完备功能就按照各位具体项目需求去各自实现吧,有疑问可以发email找我探讨下。

-module(erl_syntax_try).
-compile(export_all).
test() ->
    RecordName = erl_syntax:atom(base_dungeon),
    erl_syntax:record_expr(RecordName, [
                                        erl_syntax:record_field(erl_syntax:atom(key0), erl_syntax:abstract(1)),
                                        erl_syntax:record_field(erl_syntax:atom(key1), erl_syntax:abstract(atom)),
                                        erl_syntax:record_field(erl_syntax:atom(key2), erl_syntax:abstract("xxx")),
                                        erl_syntax:record_field(erl_syntax:atom(key3), erl_syntax:abstract(<<"hello中文"/utf8>>)),
                                        erl_syntax:record_field(erl_syntax:atom(key3), 
                                                                erl_syntax:binary([erl_syntax:binary_field(
                                                                                     erl_syntax:string("hello中文"),
                                                                                     [erl_syntax:atom(utf8)]
                                                                                    )])
                                                               )
                                      ]).
 
write() ->
    file:write_file("/tmp/1.erl", [unicode:characters_to_binary(erl_prettypr:format(test()))]).
 
format() ->
    io:format("~w~n", [erl_prettypr:format(test())]).

生成的代码长这样

#base_dungeon{key0 = 1, key1 = atom, key2 = "xxx",
                  key3 =
                      <<104, 101, 108, 108, 111, 228, 184, 173, 230, 150,
                        135>>,
                        key3 = <<"hello中文"/utf8>>}

最后,加个函数什么就可以了。

我们项目的生成的代码例子如下

%% version : 002a4bbf4625aa6234ae8c8cdc170417
%% Warning:本文件由data_generate自动生成,请不要手动修改
 
-module(data_base_treasure_award).
 
-export([get/1]).
 
-include("db_base_treasure_award.hrl").
 
get(1) ->
    #base_treasure_award{id = 1, item_id = 20001,
			 award_count = 1};
get(2) ->
    #base_treasure_award{id = 2, item_id = 20002,
			 award_count = 1};
get(3) ->
    #base_treasure_award{id = 3, item_id = 20003,
			 award_count = 1};
get(4) ->
    #base_treasure_award{id = 4, item_id = 20004,
			 award_count = 1};
get(5) ->
    #base_treasure_award{id = 5, item_id = 0,
			 award_count = 1};
get(6) ->
    #base_treasure_award{id = 6, item_id = 3,
			 award_count = 1};
get(7) ->
    #base_treasure_award{id = 7, item_id = 1,
			 award_count = 1};
get(8) ->
    #base_treasure_award{id = 8, item_id = 1,
			 award_count = 2};
get(9) ->
    #base_treasure_award{id = 9, item_id = 1,
			 award_count = 3};
get(10) ->
    #base_treasure_award{id = 10, item_id = 2,
			 award_count = 100};
get(11) ->
    #base_treasure_award{id = 11, item_id = 2,
			 award_count = 500};
get(12) ->
    #base_treasure_award{id = 12, item_id = 2,
			 award_count = 1000};
get(13) ->
    #base_treasure_award{id = 13, item_id = 25009,
			 award_count = 1};
get(Val) -> lager:warning("get not find ~p", [Val]), [].

最后

这仅仅是个data2code的一个思路,欢迎交流。

在ubuntu 14.04 server使用pure-ftpd

install

 apt-get install pure-ftpd

用户

groupadd ftpgroup
useradd -g ftpgroup -d /dev/null -s /etc ftpuser
mkdir /home/ftpusers
mkdir /home/ftpusers/ftp
pure-pw useradd ftp -u ftpuser -d /home/ftpusers/ftp
pure-pw mkdb ## 执行过pure-pw useradd等用户操作之后要进行更新数据库
ln -s /etc/pure-ftpd/pureftpd.passwd /etc/pureftpd.passwd
ln -s /etc/pure-ftpd/pureftpd.pdb /etc/pureftpd.pdb
ln -s /etc/pure-ftpd/conf/PureDB /etc/pure-ftpd/auth/PureDB
chown -hR ftpuser:ftpgroup /home/ftpusers/

新增readonly账户

useradd -g ftpgroup -d /dev/null -s /etc ftpuser_readonly
pure-pw useradd ftp_guest -u ftpuser_readonly -d /home/ftpusers/ftp
pure-pw mkdb

参考

  1. https://help.ubuntu.com/community/PureFTP
  2. http://download.pureftpd.org/pub/pure-ftpd/doc/README.Virtual-Users
  3. http://marc.info/?l=pureftpd-list&m=128345058000347

在ubuntu 14.04 server使用KVM

KVM本身可以很复杂,也可以很简单。如果要求没有那么多,像我这样,能顺利安装虚拟机,之后启动用,对性能无要求,然后就可以看看下面了。

现在主流的CPU都支持了虚拟化了,记得在BIOS设置那里打开CPU虚拟化,不然虚拟机会慢半拍的,卡顿。

下面无特殊说明的话,全部命令使用root账户运行。

安装需要的包

apt-get install qemu-kvm libvirt-bin bridge-utils virtinst

编辑/etc/libvirt/qemu.conf,然后将user和group设为root,重启libvirt服务。不然virt-install执行会报权限有问题。

配置网桥

我想把虚拟机暴露到局域网供其他机子访问,而默认的是NAT方式,所以要改下。

vim  /etc/network/interfaces

内容大致如下

auto lo eth0 br0
iface lo inet loopback
iface br0 inet dhcp
  bridge_ports eth0
iface eth0 inet manual

然后重启机子或者

ifdown eth0 && sudo ifup br0 && sudo ifup eth0

创建虚拟机

下面创建一个虚拟机名字是redmine-host,img路径是/root/kvm_image/redmine-host.img,iso路径是/root/Downloads/ubuntu-14.04.1-server-amd64.iso。

virt-install -n redmine-host --vcpus 2 -r 1024 --disk path=/root/kvm_image/redmine-host.img,bus=virtio,size=100 -c /root/Downloads/ubuntu-14.04.1-server-amd64.iso --network bridge=br0,model=virtio --graphics vnc,listen=0.0.0.0,port=5901 --noautoconsole -v

然后,找个VNC客户端,打开HOST_IP:5901这个vnc服务来进行安装系统。后面就按照普通安装linux流程就可以了。

virsh

virsh可以看文档,但是常用的并不多。

virsh edit Domain ## 修改配置,内存,cpu核数什么的,记住要先shutdown
virsh list --all

参考

理解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