erlang中文(erlang语言教程)

本想早点写这篇文章,但这段时间工作很忙,不知不觉给了自己各种懒惰的理由。现在回头看看这个问题,总结一下二郎游戏开发的经验。只是简短的结束一下我过去的erlang开发经验。

写这篇文章之前,我看了gsdxm学生写的erlang性能优化总结[2],字里行间有一点自己的体会,让我觉得自己又心血来潮地开发Erlang了。所以,现在回去整理一下erlang在游戏开发中的使用。

1.建筑设计

很多人说erlang是自然分布的,归结起来就是Erlang分布实现的很好。节点之间的通信是透明的。无论节点中的进程还是不同节点中的进程,erlang:send/2都可以用来发送消息,数据不需要任何转换。

所以很多项目会选择多节点架构,通过水平扩展(增加机器数量)可以支持更多的负载,将高性能压力的子系统分布到不同的节点,从而支持更多的线上玩家。

出发点是好的,但会引发一系列问题:

1.业务逻辑的问题:

跨节点逻辑变得复杂,玩家数据同步和一致性问题,节点断开和重新连接处理等。

2.语言限制:

节点之间的消息通信应该序列化和反序列化,原子传输应该转换成字符串、二进制副本等。

架构选择单节点还是多节点?

绝对不会。这取决于比赛。如果你的游戏就像一个页面游,不同服务之间的交互较少,单节点会更适合。但是对于lol这样的游戏来说,多节点会更合适,登录和玩法在不同的节点。玩家在对战时选择集群中压力较小的节点进行战斗计算。

单节点,优点是玩家数据容易保证一致性和方便操作维护。普通的MMORPG游戏,单个节点16 GB内存,8核CPU,可以支持2k人稳定在线。考虑到不活跃的玩家,也有可能达到5000。当然游戏的框架要设计合理,也要做一些妥协,比如玩家在服务器上找不到路,地图九格的设计,排行榜不实时刷新,同屏玩家数量控制。现在,用单节点做游戏服还有一个重要原因。这台机器的性能比以前高得多。此外,erlang的无锁Actor设计解放了并发情况下的cpu,一定程度上提高了cpu的计算能力。

单节点设计:

多节点设计:

Erlang节点关注一个问题。默认情况下,erlang节点都是连接的,也就是说,当一个节点加入集群时,集群中的所有其他节点都会与新加入的节点建立联系。中国联通带来的问题,集群节点之间的连接是成对的。随着节点的增加,连接数增加了N*(N-1)/2,越来越恐怖。连接本身占用端口资源。更糟糕的是,erlang会定期发送心跳包检查来检查节点的存活情况。即使每分钟都有一个滴答,很多节点也会引起很多网络风暴。

解决方法是隐藏集群中的节点,这样可以避免完全连接,只要erlang启动并添加一个参数-hidden即可。

2.数据库资料库

数据库io一直是游戏的主要性能瓶颈,如果处理不当,很容易造成游戏堵塞甚至崩溃。一般的持久性策略是内存读写定时持久性。当玩家上线时,玩家经常使用的数据被加载到内存中,玩家大部分时间都在读写数据。然后,数据会定期从内存刷新到磁盘,并且一旦播放器离线,数据也会保持不变。

比如说erlang自带数据库mnesia。

事实上,erlang已经实现了这样一个持久机制,即mnesia数据库。

Mnesia数据存储基于ets和dets,ets用于ram _ copy表。disc _ copy表同时使用ets和dets,数据读写使用ets和dets进行持久化;而disc _ only _ copy表使用dets。

我们先来讨论一下disc _ copy的情况。当mnesia启动时,它会将disc _ copy表中的所有数据加载到ets表中。每次你读数据,你都会读ets。当你写数据的时候,你会先把它写到ets,然后写一个副本到日志文件,等待定时(或者定量)的持久刷去dets。

一般来说,disc _ copy表可以满足我们的业务需求,但是使用mnesia时有一个问题需要注意。如前所述,mnesia startup会将表中的数据加载到ets中,如果你的表太大,会导致内存的急剧消耗。(特点是ets的记忆比例太大)

因此,在使用mnesia表时,disc _ copy表和disc _ only _ copy表经常一起使用。那么问题来了,什么时候使用disc _ copy表,什么时候使用disc _ only _ copy表。

最简单的就是对玩家数据做点什么。默认情况下,播放器的数据在disc _ copy表中。如果他很长时间没有登录,将播放器的数据移动到disc _ only _ copy表中,然后在他下次登录时将数据移动到disc _ copy表中。

有两个原因可以做到这一点:

1.在游戏中

玩家数据所占的比例较大,调整玩家数据可以获得明显收益。

2. 玩家流失后回到游戏的可能性很小,就算有的话比例也不大。

这么做的弊端就是玩家流失后重新登录的时间较长,但通过这种方式减少的内存很可观。

mnesia的使用,还要注意3个问题:

1. mnesia单个表2G文件大小限制,所以要自己分表,或者使用表分片;

2. mnesia集群功能,过多的人说有坑,但我没有这方面的经验,就不做讨论;

3. mnesia事务并发性太差,尽可能不用mnesia事务,多脏写;事务可利用进程实现,保证数据安全。

3. 进程

每个玩家一个进程的设计已经成为了erlang游戏开发的潜规则了。这个没什么好讲,玩家进程修改自己的数据,进程消息同步处理机制保证数据一致性。可能有些游戏还会将玩家进程和scoket进程独立开,负责连接的建立和维护,协议封包解包,甚至做攻击的防范等。但如果玩家进程和socket进程同在一个节点内,显然整合在一个进程较好,erlang消息基于复制,中间多了一个进程,一次前后端交互要多了2次内存复制。

那么,除玩家外,其他进程怎么确定?

1. 地图进程

每个玩家都是独立的进程,玩家pk要交换两个进程的私有数据,就要发消息给另一个进程处理。假如是强pk的游戏,同时有N个玩家一起打斗,消息就会繁多。因为数据一致性问题,进程间的并发机制就会弱化成同步机制,增加了战斗时延。

所以,这里会引入地图进程,通常以一个地图一个进程。玩家进入地图时,会同步战斗相关数据到地图进程,玩家离开地图时,再将战斗数据同步回玩家进程。而在玩家进入地图到离开前的这段时间,一切的战斗计算都由地图进程完成。

或者有人会有疑惑,就算有了地图进程,还是有同步问题,地图进程还是要同步处理pk请求,无法并发处理,玩家进程还是要等待地图进程操作完成。

其实,对于玩家的pk请求,处理至少有两个过程,第一个过程是验证攻击的合法性,如是否有这个技能,技能cd,等等。第二个过程才是战斗计算,玩家进程检查合法性,再由地图进程做核心的战斗计算。另外一个,玩家进程除了战斗请求外,还有其他业务逻辑上的消息,容易出现进程挂起的情况,这时候,玩家进程不可能处理到战斗计算,就会导致战斗卡顿。

2.公共进程

公共进程指的是那些提供公共服务的进程,比如:

1. 社交类,有好友、帮派、组队等,这些服务管理着多数玩家的数据,都需要一个进程来管理;

2. 计算类,这类有一定的计算量,比如说排行榜,要有一个进程来承担计算;

3. 广播类,有聊天室、世界聊天、帮派聊天、地图广播等;

4. 开关类,有活动系统,比赛系统等等,控制游戏活动的开启和关闭。

erlang进程虽然廉价,但是不要太过随意创建进程,比如创建一个临时进程异步传输数据等等。虽然这在某种程度上提高了并发性,但进程的创建和销毁需要一定的系统消耗,而且会导致项目中进程数量不可控,可能系统莫名其妙多了很多进程,这些维护起来也麻烦。再说,erlang同时存在的进程有最大数量限制。

4. 进程字典与ets

进程字典是erlang游戏开发中最为常用的数据记录方式,理由很简单,因为它够快,差不多比ets快了一个数量级。但是,进程字典的数据为所在进程私有,无法跨进程直接get到进程字典的数据,而且,在进程被销毁时,进程字典的数据也会被回收。

再说下ets,对比进程字典,ets的适用场景是跨进程读写数据。遇到一个数据频繁被多个进程读到,就要考虑使用ets了。另外,ets有归属进程,但归属进程销毁时,ets的数据就会被系统回收。

对比进程字典和ets的实现,区别如下:

1. 锁方面,进程字典为无锁操作,ets是读写锁。

2. 数据方面,进程字典数据在进程中,查询没有多余的复制操作;而ets,因为数据不在进程中,查询时会复制一份到进程。

所以,进程字典通常是最优先的选择。假如玩家的数据是存储在mnesia,特别是玩家的核心数据,就有必要从mnesia读到放在进程字典中。前面讲到了mnesia是利用ets和dets实现的,玩家上线时将核心数据读到进程字典,这样,读写都在进程字典,就可以利用进程字典带来的性能提升。这个提升是很可观,毕竟快了一个数量级。

前面提到了ets归属进程销毁时,ets数据也会被回收?

那么如果预防数据丢失的问题,ets也提供了方法,通过设置继承者进程就可以了。

ets:new(person, [named_table, {heir, HeirPid, HeirData}])

当归属进程崩溃时,继承者进程就会收到信息 {‘ETS-TRANSFER’, Tid, FromPid, HeirData},并且,ets的归属权就会转交到继承者进程。

5. 消息广播

消息广播是游戏中的性能消耗大头,主要包括地图的行走、战斗广播,世界聊天广播。世界聊天广播可以通过控制玩家发送时间间隔来限制,地图中的广播包,比如行走和战斗包的广播实时性高,只需发给视野内的玩家就可以,不用全地图广播。所以常见的处理方式是,玩家的视野通过九宫格划分,玩家的地图广播只发送给九宫格内的玩家。

如何在服务器压力大时,进一步优化广播消息:

针对时效性高的数据包,不重要的广播包,可以选择性丢弃:

1、挂机玩家不发送战斗包,特别是战斗buff。

2、每N个包丢弃一个。

挂机玩家的判定,前端以玩家长时间没动作来判定,或者将游戏切到后台运行时判定。等玩家在游戏内点击鼠标,或键盘时解除挂机状态。

这里再谈下,消息广播还可能进一步优化。通常,后端发数据给前端都会对协议数据进行序列化(封包),然后再发给前端反序列化(解包)。而广播数据,对于每个玩家可能都是一样的,没必要每个玩家发给前端时都各自序列化一次,就只要序列化一次,然后每个玩家拿到数据后直接发给前端。

erlang消息广播要注意什么问题?

1、reduction计数

通常会启动一个消息管理进程,这个进程就负责把广播消息转发给对应的所有玩家进程。启用管理进程的一个好处是,进程发消息会扣除reduction,而且这个reduction扣除大小还受到接收者进程影响。假如直接在地图进程做消息广播,就会导致地图进程受到的调度极度减少,影响战斗计算。

2、消息复制

erlang消息发送基于复制,但对于比较大的二进制数据,则会优化成二进制引用,减少二进制复制带来的开销。所以,当一个消息要发给多个进程,特别是协议数据(发给前端也要先转成二进制),可以先转成二进制再发送。

结束语

文章到这里就结束了,这是我做 Erlang 游戏开发经验总结的一些经验,后续,我还会找时间总结更多的开发经验。最后呢,建议大家少上复制网站,如红黑联盟,这些网站复制了还会把文章来源网址删了,然后给了搜索引擎几个钱就排到前面去了。这样带来的问题是,文章我一开始写错了,但后来改过来了,这些网站是不会更新的,看到的人还以为原来的内容是对的。

Published by

风君子

独自遨游何稽首 揭天掀地慰生平

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注