Fork me on GitHub
随笔 - 215  文章 - 13  trackbacks - 0
<2024年4月>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011


专注即时通讯及网游服务端编程
------------------------------------
Openresty 官方模块
Openresty 标准模块(Opm)
Openresty 三方模块
------------------------------------
本博收藏大部分文章为转载,并在文章开头给出了原文出处,如有再转,敬请保留相关信息,这是大家对原创作者劳动成果的自觉尊重!!如为您带来不便,请于本博下留言,谢谢配合。

常用链接

留言簿(1)

随笔分类

随笔档案

相册

Awesome

Blog

Book

GitHub

Link

搜索

  •  

积分与排名

  • 积分 - 204205
  • 排名 - 127

最新评论

阅读排行榜

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使用中应注意的问题和优化点。

  1.  单节点还是多节点

Erlang节点之间的通信是透明的,节点内部和外部之间的调用一致。基于这样的特性,很多人会选用多节点,把各子系统(登陆节点,玩家节点,地图节点,全局节点等)分配到不同的节点中,以支持更多的在线玩家。这样做的出发点是好点是好的,但会引起一列表的问题:登陆、转场逻辑复杂,节点间的消息广播频繁,玩家数据同步、一致问题,内存消耗,运维复杂化等。相比之下,单节点就简单多了,不用考虑节点通信,玩家数据保证一致,运维方便,一机多服。在开启SMP的情间下,单节点的性能已经很好。对页游的业务,同时在线达到5000人已经非常少见,即使达到也是首服当天才会出现,单节点完全可以应付这样的情况,所以没必要用多节点,增加系统复杂性。

2. 消息广播

消息广播是游戏中的性能消耗大头,主要包括地图的行走、PK广播,世界聊天广播。世界聊天广播可以通过CD等策划手段限制,行走和PK包的广播实时性高,只能通过技术手段解决。地图中的广播包,只需发给视野内的玩家就可以,不用全地图广播。视野内的玩家可以通过九宫格划分,以 X,Y为主键,映射到对应的玩家数据。以九宫格方式查找玩家非常高效,我第一个游戏,地图中的玩家起初是保存在一个列表中,每次广播时都要遍历列表,找出同屏玩家,消息广播非常低效,特别是在PK时,CPU占用高。用九宫格优化后,一切问题都解决了。还有一个优化广播问题的方法是数据包缓存。

3. 缓存-数据库,网络

缓存是用空间换时间,它是性能优化中常用的方法。数据库缓存,开服时把玩家的必要数据加载到内存中,可以减少玩家的登陆延时,应对玩家并发登陆,刷新也很有效。同时玩家数据没必要实现存库,对于坐标,经验,金钱等变化频繁的值,如果实时存在,会很容易压跨数据库或对存库进程造成消息阻塞。玩家改变的数据可以缓在内存中,定时存库,或下线时再存库。网络中的消息包也可以在应用层给缓存起来,达到一定长度或延时一定时间后再发出去。虽然虚拟机和TCP层会做缓存,最好还是在应用层做一次缓存。

4. 进程-每玩家应该有几个进程

其实每玩家一个进程已经足够,代码简单,方便维护,性能开销小。没必要为每个玩家开启了网络,物品、任务等进程,多个进程不但造成进程间通信开销,还不好维护。

5. 善用进程字典

Erlang中是不建议用进程字典的,但进程字典是数据存取最快的方式,对于游戏这种高性能要求的应用,进程字典是不二的选择。使用进程字典时要切记在对应的进程中操作,最好按功能把put,get操作封装到模块接口中,避免误用。

6. 代码规范

a. 代码应该简单,逻辑清晰,把功能细分到函数中。函数一般不多于30行,每个模块不多于1000行。

b. 写尾递归函数一定要有清淅的退出条件,不要在函数中改变退出条件。一个退出条件不明确的尾递归,是造成消息阻塞,内存耗尽的主要原因之一。


  1. %% 一个明确的尾递归函数:
  2. loop([H |T]) ->
  3.     do_something,
  4.     loop(T);
  5. loop([]) ->
  6.     ok.

  7. %% 存在错误风险的写法
  8. %% NewLiist不可预期,存在死循环风险
  9. loop([H | T]) ->
  10.     NewList = do_something(H,T),
  11.     loop(NewList);
  12. loop([]) ->
  13.     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写了个非常简单的连接压力测试程序,

代码:
  1. -module(stress_test).
  2.   
  3. -export([start/0, tests/1]).
  4.   
  5. start() ->
  6.     tests(12345).
  7.   
  8. tests(Port) ->
  9.     io:format("starting~n"),
  10.     spawn(fun() -> test(Port) end),
  11.     spawn(fun() -> test(Port) end),
  12.     spawn(fun() -> test(Port) end),
  13.     spawn(fun() -> test(Port) end).
  14.   
  15. test(Port) ->
  16.      case gen_tcp:connect("192.168.0.217", Port, [binary,{packet, 0}]) of
  17.     {ok, _} ->
  18.             test(Port);
  19.     _ ->
  20.         test(Port)
  21.     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的帖子.
代码
  1. $./configure --enable-kernel-poll  

由于我们的测试服务器是双核的,我们在配置的时候也打开了smp支持.  欢快的make  & make install之后....
把 /proc/sys/net/ipv4/ip_local_port_range 的内容改成了1024到65535.  最多也也能改成65535 :)

代码
  1. $echo 1024 65535 > ip_local_port_range  

另外再添加一个erl的环境变量

代码
  1. $export ERL_MAX_PORTS=102400  

于是开始跑了,不过这次跑不一样了

echo_server
  1. $erl -noshell  +P 102400 +K true +S 2 -smp -s echo_server start  
stress_test
  1. $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 思月行云 阅读(3252) 评论(0)  编辑 收藏 引用 所属分类: Erlang

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理