http://wqtn22.iteye.com/blog/1820587
1. 进程标志设置:
消息和binary内存:erlang:process_flag(min_bin_vheap_size, 1024*1024),减少大量消息到达或处理过程中产生大量binary时的gc次数
堆内存:erlang:process_flag(min_heap_size, 1024*1024),减少处理过程中产生大量term,尤其是list时的gc次数
进程优先级:erlang:process_flag(priority, high),防止特殊进程被其它常见进程强制执行reductions
进程调度器绑定:erlang:process_flag(scheduler, 1),当进程使用了port时,还需要port绑定支持,防止进程在不同调度器间迁移引起性能损失,如cache、跨numa node拷贝等,当进程使用了port时,主要是套接字,若进程与port不在一个scheduler上,可能会引发严重的epoll fd锁竞争及跨numa node拷贝,导致性能严重下降
2. 虚拟机参数:
+S X:X :启用调度器数量,多个调度器使用多线程,有大量锁争用
-smp disable :取消smp,仅使用单线程,16个-smp_disabled虚拟机性能高于+S 16:16
+sbt db :将scheduler绑定到具体的cpu核心上,再配合erlang进程和port绑定,可以显著提升性能,但是如果绑定错误,反而会有反效果
3. 消息队列:
消息队列长度对性能的影响主要体现在以下两个方面:进程binary堆的gc和进程内消息匹配,前者可以通过放大堆内存来减少gc影响,后者需要谨慎处理。
若进程在处理消息时是通过消息匹配方式取得消息,同时又允许其它进程无限制投递消息到本进程,此时会引发灾难,匹配方式取得消息会引发遍历进程消息队列,如果此时仍然有其它进程投递消息,会导致进程消息队列暴涨,遍历过程也将增大代价,引发恶性循环。已知模式有:在gen_server中使用file:write(raw模式)或gen_tcp:send等,这些操作都是erlang虚拟机内部通过port driver实现的,均有内部receive匹配接收,对于这些操作,最好的办法是将其改写为nif,直接走进程堆进行操作,次之为将file:write或gen_tcp:send改写为两阶段,第一阶段为port_command,第二阶段由gen_server接收返回结果,这种异步化可能有些正确性问题,对于gen_tcp:send影响不大,因为网络请求本身要么同步化要么异步化,都需要内部的确认机制;对于file:write影响较大,file:write的错误通常为目录不存在或磁盘空间不足,确保这两个错误不造成影响即可,同时如果进程的其它部分需要使用file的其它操作,必须首先清空之前file:write产生的所有file的port消息,否则有可能产生消息序列紊乱的问题。
对于套接字的接口调用,可以参考rabbitmq的两阶段套接字发送方法,而对于文件接口调用,可以参考riak的bitcask引擎将文件读写封装为nif的方法
4. 内存及ets表:
ets表可以用于进程间交换大数据,或充当缓存,以及复杂匹配代理等,其性能颇高,并发读写可达千万级qps,并有两个并发选项,在建立表时设置,分别是{write_concurrency, true} | {read_concurrency, true},以允许ets的并发读写
使用ets表可以绕过进程消息机制,从而在一定程度上提高性能,并将编程模式从面向消息模式变为面向共享内存模式
5. CPU密集型操作:
erlang执行流程的问题:
1. 其指令都是由其虚拟机执行的,一条指令可能需要cpu执行3-4条指令,一些大规模的匹配或遍历操作会严重影响性能;
2. 其bif调用执行过程类似于操作系统的系统调用,需要对传入参数进行转换,在大量小操作时损失性能较为严重
3. 其port driver流程较为繁冗复杂,需要经历大量的回调等,一般的小功能操作,不要通过port driver实现
建议:
字符串匹配不要通过list进行,最好通过binary;单字节匹配,尤其是语法解析,如xmerl、mochijson2、lexx等,尽管使用binary,但是它们是一个字节一个字节匹配的,性能会退化到list的水平,应该尽量将其nif化;
对于一些小操作,反而应该去bif化、去nif化、去port driver化,因为进入erlang内部函数的执行代价也不小;
已知的性能瓶颈:re、xmerl、mochijson2、lexx、erlang:now、calendar:local_time_to_universal_time_dst等
6. 数据结构:
减少遍历,尽量使用API提供的操作
由于各种类型的变量实际可以当做c的指针,因此erlang语言级的操作并不会有太大代价
lists:reverse为c代码实现,性能较高,依赖于该接口实现的lists API性能都不差,避免list遍历,[||]和foreach性能是foldl的2倍,不在非必要的时候遍历list
dict:find为微秒级操作,内部通过动态hash实现,数据结构先有若干槽位,后根据数据规模变大而逐步增加槽位,fold遍历性能低下
gb_trees:lookup为微秒级操作,内部通过一个大的元组实现,iterator+next遍历性能低下,比list的foldl还要低2个数量级
其它常用结构:queue,set,graph等
7. 计时器:
erlang的计时器timer是通过一个唯一的timer进程实现的,该进程是一个gen_server,用户通过timer:send_after和timer:apply_after在指定时间间隔后收到指定消息或执行某个函数,每个用户的计时器都是一条记录,保存在timer的ets表timer_tab中,timer的时序驱动通过gen_server的超时机制实现。若同时使用timer的用户过多,则tiemr将响应不过来,成为瓶颈。
更好的方法是使用erlang的原生计时器erlang:send_after和erlang:start_timer,它们把计时器附着在进程自己身上。
8. 尾调用和尾递归:
尾调用和尾递归是erlang函数式语言最强大的优化,尽量保持函数尾部有尾调用或尾递归
9. 文件预读,批量写,缓存:
这些方式都是局部性的体现:
预读:读空间局部性,文件提供了read_ahead选项
批量写:写空间局部性
对于文件写或套接字发送,存在若干级别的批量写:
1. erlang进程级:进程内部通过list缓存数据
2. erlang虚拟机:不管是efile还是inet的driver,都提供了批量写的选项delayed_write|delay_send,
它们对大量的异步写性能提升很有效
3. 操作系统级:操作系统内部有文件写缓冲及套接字写缓冲
4. 硬件级:cache等
缓存:读写时间局部性,读写空间局部性,主要通过操作系统系统,erlang虚拟机没有内部的缓存
10.套接字标志设置:
延迟发送:{delay_send, true},聚合若干小消息为一个大消息,性能提升显著
发送高低水位:{high_watermark, 128 * 1024} | {low_watermark, 64 * 1024},辅助delay_send使用,delay_send的聚合缓冲区大小为high_watermark,数据缓存到high_watermark后,将阻塞port_command,使用send发送数据,直到缓冲区大小降低到low_watermark后,解除阻塞,通常这些值越大越好,但erlang虚拟机允许设置的最大值不超过128K
发送缓冲大小:{sndbuf, 16 * 1024},操作系统对套接字的发送缓冲大小,在延迟发送时有效,越大越好,但有极值
接收缓冲大小:{recbuf, 16 * 1024},操作系统对套接字的接收缓冲大小
11. 序列化/反序列化:
通常情况下,为了简化实现,一般将erlang的term序列化为binary,传递到目的地后,在将binary反序列化为term,这通常涉及到两个操作:
term_to_binary及binary_to_term,这两个操作性能消耗极为严重,应至多只做一次,减少甚至消除它们是最正确的,例如直接构造binary进行跨虚拟机数据交换;
但对比与其它的序列化和反序列化方式,如利用protobuf等,term_to_binary和binary_to_term的性能是高于这些方式的,毕竟是erlang原生格式,对于力求简单的应用,其序列化和反序列化方式推荐term_to_binary和binary_to_term
12. 并发化
在一些场景下,如web请求、数据库请求、分布式文件系统等,单个接入接口已经不能满足性能需求,需要有多个接入接口,多个数据通道,等等,这要求所有请求处理过程必须是无状态的,或者状态更改同步进入一个公共存储,而公共存储也必须是支持并发处理的,如并发数据库、类hdfs、类dynamo存储等,若一致性要求较高,最好选用并发数据库,如mysql等,若在此基础上还要求高可用,最好选择同步多结点存储,
mnesia、zk都是这方面的典型;若不需要较高的一致性,类hdfs、类dynamo这类no sql存储即可满足
13. hipe
将erlang汇编翻译成机器码,减少一条erlang指令对应的cpu指令数
http://blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=429659&id=5205993
本人主要从事游戏后端开发,所以本文只从游戏开发角度分析Erlang使用中应注意的问题和优化点。
- 单节点还是多节点
Erlang节点之间的通信是透明的,节点内部和外部之间的调用一致。基于这样的特性,很多人会选用多节点,把各子系统(登陆节点,玩家节点,地图节点,全局节点等)分配到不同的节点中,以支持更多的在线玩家。这样做的出发点是好点是好的,但会引起一列表的问题:登陆、转场逻辑复杂,节点间的消息广播频繁,玩家数据同步、一致问题,内存消耗,运维复杂化等。相比之下,单节点就简单多了,不用考虑节点通信,玩家数据保证一致,运维方便,一机多服。在开启SMP的情间下,单节点的性能已经很好。对页游的业务,同时在线达到5000人已经非常少见,即使达到也是首服当天才会出现,单节点完全可以应付这样的情况,所以没必要用多节点,增加系统复杂性。
2. 消息广播
消息广播是游戏中的性能消耗大头,主要包括地图的行走、PK广播,世界聊天广播。世界聊天广播可以通过CD等策划手段限制,行走和PK包的广播实时性高,只能通过技术手段解决。地图中的广播包,只需发给视野内的玩家就可以,不用全地图广播。视野内的玩家可以通过九宫格划分,以 X,Y为主键,映射到对应的玩家数据。以九宫格方式查找玩家非常高效,我第一个游戏,地图中的玩家起初是保存在一个列表中,每次广播时都要遍历列表,找出同屏玩家,消息广播非常低效,特别是在PK时,CPU占用高。用九宫格优化后,一切问题都解决了。还有一个优化广播问题的方法是数据包缓存。
3. 缓存-数据库,网络
缓存是用空间换时间,它是性能优化中常用的方法。数据库缓存,开服时把玩家的必要数据加载到内存中,可以减少玩家的登陆延时,应对玩家并发登陆,刷新也很有效。同时玩家数据没必要实现存库,对于坐标,经验,金钱等变化频繁的值,如果实时存在,会很容易压跨数据库或对存库进程造成消息阻塞。玩家改变的数据可以缓在内存中,定时存库,或下线时再存库。网络中的消息包也可以在应用层给缓存起来,达到一定长度或延时一定时间后再发出去。虽然虚拟机和TCP层会做缓存,最好还是在应用层做一次缓存。
4. 进程-每玩家应该有几个进程
其实每玩家一个进程已经足够,代码简单,方便维护,性能开销小。没必要为每个玩家开启了网络,物品、任务等进程,多个进程不但造成进程间通信开销,还不好维护。
5. 善用进程字典
Erlang中是不建议用进程字典的,但进程字典是数据存取最快的方式,对于游戏这种高性能要求的应用,进程字典是不二的选择。使用进程字典时要切记在对应的进程中操作,最好按功能把put,get操作封装到模块接口中,避免误用。
6. 代码规范
a. 代码应该简单,逻辑清晰,把功能细分到函数中。函数一般不多于30行,每个模块不多于1000行。
b. 写尾递归函数一定要有清淅的退出条件,不要在函数中改变退出条件。一个退出条件不明确的尾递归,是造成消息阻塞,内存耗尽的主要原因之一。
- %% 一个明确的尾递归函数:
- loop([H |T]) ->
- do_something,
- loop(T);
- loop([]) ->
- ok.
- %% 存在错误风险的写法
- %% NewLiist不可预期,存在死循环风险
- loop([H | T]) ->
- NewList = do_something(H,T),
- loop(NewList);
- loop([]) ->
- ok.
c. 不要相信客户端,上行的数据都需要验证,前端的请求都要做合法性判断,防止出现外挂、刷钱刷物品、刷金币的情况。
d 不要写过多的case ,if嵌套,最好不要大于3个嵌套,通过 try catch 方法写扁平化的代码。
7. 自动化工具
自动化工具可以避免出错,还把开发人员解放出来,提高生产效率。对于重复性,有规律的代码(如数据存取,通信协议),可以分离出来,让工具自动生成。有了生成工具后,修改协议,新加字段等操作,简单方便,不用为增加数据表中一个字段,而改十多个函数接口的修改;也不用担心前后端协议不一致的问题。
8. 监控系统
通过erlang:system_monitor/2,监控系统long_gc,large_heap等情况。
9. 性能分析工具
准备好top memory,top message_queue等查看系统属性的工具,出问题时可以随时查看。
本文链接:http://www.kongqingquan.com/archives/221
这几天在弄个ERLANG的长连接测试程序,主要是要在一个服务器上建20万条长连接.
于是找到了以下内容.
---------------------------------------------
前些天给echo_server写了个非常简单的连接压力测试程序,
代码:
- -module(stress_test).
-
- -export([start/0, tests/1]).
-
- start() ->
- tests(12345).
-
- tests(Port) ->
- io:format("starting~n"),
- spawn(fun() -> test(Port) end),
- spawn(fun() -> test(Port) end),
- spawn(fun() -> test(Port) end),
- spawn(fun() -> test(Port) end).
-
- test(Port) ->
- case gen_tcp:connect("192.168.0.217", Port, [binary,{packet, 0}]) of
- {ok, _} ->
- test(Port);
- _ ->
- test(Port)
- end.
一开始我的这个stress_test客户端运行在windows上面, echo_server服务器端运行在linux上面。 结果接受了1016个连接就停止了. 于是我用ulimit -n 改了服务器端的文件描述符数量为10240. 接着还是如此,折腾了几天,最终还是没有搞明白。
于是就求助于公司的linux编程牛人,结果让我一倒... 客户端没有修改文件描述符个数. windows上得在注册表里面改.
牛人开始对这东西的性能感兴趣了,刚好我摸了一阵子erlang的文档,于是我俩就走向了erlang网络连接的性能调优之旅啦~~过程真是让人兴奋。 我们很快通过了1024这一关~~到了4999个连接,很兴奋.
但为什么4999个连接呢, 检查一下代码终于发现echo_server.erl定义了一个宏, 最大连接数为5000. 我又倒~~
修改编译之后, 连接数跑到101xx多了, 太哈皮了!
再测102400个连接时,到32767个连接数erl挂了~说是进程开得太多了. 好在记得这个erl的参数+P,可以定义erlang能生成的进程数. 默认是32768. 改了!
后面不知怎么着,在81231个连接停止了. 新的性能瓶颈又卡了我们. 好在牛人对linux熟, 用strace(这东西会莫名地退出), stap查出一些苗头. 我也想到在otp文档好像提过另一个limit,那就是端口数...在此同时我们发现erlang在linux上是用的传统poll模型. 但查erlang的源代码发现是支持epoll的. 在网上搜了半天,终于搜到了个maillist的帖子.
代码
- $./configure --enable-kernel-poll
由于我们的测试服务器是双核的,我们在配置的时候也打开了smp支持. 欢快的make & make install之后....
把 /proc/sys/net/ipv4/ip_local_port_range 的内容改成了1024到65535. 最多也也能改成65535 :)
代码
- $echo 1024 65535 > ip_local_port_range
另外再添加一个erl的环境变量
代码
- $export ERL_MAX_PORTS=102400
于是开始跑了,不过这次跑不一样了
echo_server
- $erl -noshell +P 102400 +K true +S 2 -smp -s echo_server start
stress_test
- $erl -noshell +P 102400 +K true +S 2 -smp -s stress_test start
这里的+K true,表示使用内核poll,+S 2 表示两个核. 这样可欢快啦~~~ 10w大关过咯! 而且比刚才没用epoll的速度快暴多~~
于是我们又开始了204800个连接发测试了~~~
用top一看cpu占用率极低,服务器只在5%左右。 内存也不是很大~~
来源: http://www.lupaworld.com/tutorial-view-aid-10191.html
posted on 2016-03-28 08:21
思月行云 阅读(3282)
评论(0) 编辑 收藏 引用 所属分类:
Erlang