Erlang 游戏开发经验总结

早早就想写这篇文章,但这段时间忙于工作的事情,就不自觉地给了自己各种懒惰的理由。现在回头看下这个问题,总结下erlang 游戏开发经验。就当是,为我过去一段时间的erlang开发经历,画上一个小句号。

在写这篇文章前,我看过孔庆泉同学写过的Erlang 性能优化总结[2],字里行间有一点自己的体会,使得我心血来潮,好像重新做回了erlang开发的感觉。所以,现在回过头,整理下游戏开发对erlang的使用。

1. 架构设计

很多人都说Erlang天生分布式,归结原因是erlang分布式这块实现比较完善。 节点间的通信是透明的,无论是节点内的进程,还是不同节点的进程,都可以使用 erlang:send/2 发送消息,而且数据不需要做任何转换。
所以,很多项目会选用多节点架构,可以通过横向拓展(增加机器数量)来支撑更多负载,把性能压力大的子系统分配到不同的节点,这样,就可以 支持更多的在线玩家。
这样做的出发点是好的,但会引起一系列的问题:
1、业务逻辑的问题:
  跨节点逻辑变得复杂,玩家数据同步、一致性问题,节点断开及重连的处理等
2、语言局限性问题:
  节点间消息通讯要序列化和反序列化,原子 传输要转成字符串,二进制复制

那架构选择单节点,还是多节点?
没有绝对,这要看游戏而定。假如你的游戏像页游那样,分服而且各个服之间交互较少,单节点会比较适合。但如果像lol那种对战玩法的游戏,多节点会比较合适,登录和玩法在不同的节点,玩家对战时选择集群内压力较小的节点做战斗计算。

单节点,好处是玩家数据容易保证一致,运维方便。以常见的MMORPG游戏,单节点在16G内存,8核心CPU,可以支撑2k人稳定在线,考虑非活跃玩家,达到5000也是可能的。当然,游戏的框架要合理设计,做一些妥协,比如玩家寻路不能在服务端做,地图 九宫格的设计,排行榜不实时刷新,控制同屏玩家数量等 。现在,单节点做游戏服,还有一个重要原因是,现在的机器性能相较以前高很多了,加上erlang无锁的Actor设计在并发场合下解放了cpu,从某种程度上讲提高了cpu的运算能力。

单节点的设计:

Erlang 游戏开发经验总结_第1张图片

多节点的设计:

Erlang 游戏开发经验总结_第2张图片

erlang节点注意一个问题,默认erlang节点是全联通的,也就是当一个节点加入集群时,集群其他所有节点会和新加入的节点建立联系。全联通带来的问题,集群节点间两两连接,随着节点增加,连接数量呈N*(N-1)/2增长,越发恐怖,连接本身占用了端口资源。更坏的是,为了检测节点的存活,erlang会定期发心跳包检查,即使一分钟一个tick,节点多的话也会造成大量的网络风暴。
解决方法就是在集群中隐藏节点,就可以避免全联通,只要erlang启动加个参数即可 -hidden

2. 数据库

数据库io向来都是游戏的主要性能瓶颈,处理不好容易导致 游戏卡顿,甚至崩溃。 而通用的持久化策略就是,内存读写+定时持久化。玩家上线时,加载玩家频繁用到的数据到内存,玩家大多时候就是在读写这些数据。然后定时把这些数据从内存刷到磁盘,在玩家下线时也做一次持久化。

说下erlang自带数据库 - mnesia
实际上,erlang已实现了这样一套持久化机制,也就是 mnesia数据库。
mnesia数据存储是基于ets和dets实现,对于ram_copies表使用ets;disc_copies表同时使用ets和dets,数据读写使用ets,dets做持久化;而disc_only_copies表使用的是dets
这里先讨论 disc_copies的情况, mnesia启动时,会将  disc_copies表所有数据加载到ets表中,每次读数据都是读ets,写数据则会先写到ets,然后再写一份到日志文件中,等待定时(或定量)持久化刷到dets。
通常disc_copies表可以满足我们的业务需求,但使用mnesia要注意一个问题。前面也提到了,mnesia启动会将表中的数据加载到ets,假如你的表过大,就会导致内存被急剧消耗掉。(特征就是,ets所占的内存比率过大)
所以,使用mnesia表时,经常都是要disc_copies表和disc_only_copies表配合使用。那问题来了, 什么时候使用 disc_copies表,什么时候disc_only_copies表。

最简单的就是,对玩家数据动刀。玩家数据默认用disc_copies表,如果长时间没登录后将这个玩家的数据移到disc_only_copies表,等到他下次登录时再将数据移到disc_copies表。
之所以可以这么做,理由有两个:
 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 游戏开发经验总结的一些经验,后续,我还会找时间总结更多的开发经验。最后呢,建议大家少上复制网站,如红黑联盟,这些网站复制了还会把文章来源网址删了,然后给了搜索引擎几个钱就排到前面去了。这样带来的问题是,文章我一开始写错了,但后来改过来了,这些网站是不会更新的,看到的人还以为原来的内容是对的。


参考:

[1] http://blog.csdn.net/mycwq/article/details/50939354

[2] http://www.kongqingquan.com/archives/221

你可能感兴趣的:(Erlang 游戏开发经验总结)