我不得不承认,我的能力不足以写出一个100%不会宕机的游戏服务器程序,这也不能全怪我的能力太弱,谁让咱国内网游玩家数量庞大,哪个游戏刚上线时没有挤的爆满过?还有些或是猎奇,或是谋私的个人和组织,在制造着千奇百怪,匪夷所思的数据包及操作流程来试探你的服务器。这些都曾是我在服务器宕机后向老板开脱的理由。
当WOW终于来到中国时,我一边欣喜着终于可以在艾泽拉斯的大陆上自由翱翔,一边却咒骂着9C的破服务器,动不动就宕机。当然,身为游戏程序设计师的我明知道,这大部分的错误都不应归罪于代理商9C,但是,谁让blizzard是我心目中的神,谁又让WOW成为我游戏制作的教科书呢。好吧,我知道上面这段极力追捧blizzard跟WOW的话可能早已让你恶心连连,不堪入目了,对不起,忘了这一节,让我们继续。
服务器宕机后都发生了些什么?
显然的,宕机后玩家会骂,就像我在玩WOW时那样,骂游戏公司,骂老板,骂GM。非常抱歉,我们可爱的玩家们似乎并不清楚,这个时候最该骂的其实是我们这些程序员们。长久的遗忘被我们当成了包容,以至于游戏程序员在公司里都养成了趾高气扬,不可一世的坏毛病:看吧,策划们,你们做的太烂了,数值不平衡,玩法没新意,只会照抄WOW跟大菠萝,能怪玩家骂你们吗?运营不得力,买服务器的钱不知道去了哪里,游戏里卡的要死,偶尔办个活动还没半点吸引力,能不被玩家骂你是无良运营商吗?GM们能不天天被骂家指着骂吗?……呃,又扯远了。
赶紧先把服务器重启吧。老板正站在你的身后,一脸愁容,虽然暂时还没有发作,但看得出来:老板很生气,后果很严重!
玩家们很快又回来了,不得不为玩家们的毅力和执着精神而感动,更为自己的错误而愧疚,凌晨时分,服务器启了又宕,宕了又启,如此反复,可热情的玩家们依然陪着我在折腾。哦,当年安其拉开门的时候,我也曾这样折腾过。
这个时候不是你一个人在战斗。GM们在忙碌地处理着玩家不断打来的投诉电话:刚买的装备在宕机后消失了;花光了身上所有材料合成的武器回档了,但材料却没有还给我……数据库维护组的同事们也在紧张的恢复着数据,尽可能的将玩家的损失减到最少。
真是一件令人沮丧的事。
真的该试着做点什么了吧!
既然我们非常不愿意看到宕机的情况发生,但又无法100%保证写出来的服务器程序一定不会出错,那我们就在当机发生后的抢救措施上花点功夫,让玩家的损失不至于太大,也让我们的维护人员少些压力吧。
一个最简单也最有效的做法是为每一台服务器都配备物理冗余,同步更新冗余服务器上的状态,当宕机发生时,立即将处理切换到后备服务器上。只是,物理冗余的代价太大,从成本方面考虑,老板可能不大愿意点头。
既然不能做硬冗余,那就再来考虑软的吧。
如果只是简单的启动冗余进程,其实是换汤不换药的做法。原来能跑1000人的服务器,由于同时运行了两个相同的进程,使得CPU和内存开销都翻了倍,结果是只能跑500人了。还是要加服务器。
看来只能更深一层,从架构设计上来动手了。
假设我们的游戏世界是由多个独立场景构成的,那么在实现上我们可以让这些场景在进程上也独立,这样做的好处是可以使得一个场景的宕机不会影响到其他场景的正常运行。如果我们的游戏世界物理上没有分隔,是一个无缝的大世界,我们也可以人为的将其分成多个独立区域,所需要做的额外工作是处理好那些站在区域边界附近的对象。事实上,现在的无缝大世界也都是这样实现的。
有了这样一个前提,我们再来看这个已宕掉的场景该如何处理。
还是老办法,赶紧先把它拉起来吧。一个具体可行的方案是,由场景管理器,或者你也有可能叫它世界服务器,来监视各个场景进程的运行状态,当某个场景异常失去联系时,由管理器来将其重新启动。这里需要再花点心思的是,如何让玩家数据正常地发送到新启动的场景进程中,而且这个过程对于客户端来说是透明的。
这个方案听起来似乎不错,只是,如果宕掉的是场景管理器进程,那该怎么办呢?
按照前面的描述,场景管理器可以看作是整个游戏世界的中心,它以一个指挥者的身份维护着游戏世界的有序运行,所以它的宕机对整个游戏世界的影响也将会是巨大的。
有没有什么办法能够使得场景管理器进程再次启动后能够恢复先前的状态呢?
我们可以为管理器和场景进程定义一套协议,使得管理器不仅能够创建并恢复一个已有场景,而且场景管理器还能通过现有的场景进程数据恢复出自己。
一个理论上可行的方案是,场景管理器与场景进程间保持TCP长连接,并以一定频率进行心跳联系,任意一方发现联系中断或长时间未收到心跳包后都会立即做出处理。
如果是管理器发现场景进程失去联系,那就启动新的场景,如前面所描述的那样。如果是场景进程发现管理器失去联系,那就立即启动重连过程,直接再次连接上管理器,然后立即将自己当前的状态和负责的场景ID报告给管理器。管理器通过这些上报的数据就能恢复出游戏世界内的场景对应关系表,也就是恢复出了自己原来的状态。
进程是恢复出来了,可我们忘了最重要的内容:数据。当场景进程宕机后,上面保存的玩家属性数据也随之丢失了,虽然我们能够再次将这个场景创建出来,并把原来在这个场景内的客户端数据重新定向过来,但这些客户端对应的玩家对象的数据却没有了,游戏仍然无法继续。
也许我们可以再做一点修改,把场景内的玩家数据分离出来,保存到一个独立的进程上,比如,我们可以把这个进程叫做数据服务器,或者数据中心之类的。一个隐含的要求是,数据服务器的逻辑实现非常简单,简单到你可以认为它是绝对安全的,不会宕机。所以,保存在这里的玩家数据也就是绝对安全的。
让我们在这个问题上稍微再深入一点。
场景进程上每次执行玩家的游戏逻辑时都要异步地到数据服务器上来存取数据,这个开销可能太大,而且会使得一些游戏逻辑的实现变的很复杂,那么,把一些会频繁使用到的数据直接保存在场景进程中,当数据发生改变时同步更新到数据服务器上,这样可能会比较容易接受。
老板全都满意了吗?
从理论上来说,我们已经解决了场景进程宕机和管理器宕机后的状态恢复问题,并且在场景恢复后也不会因为丢失了玩家数据而无法继续进行游戏,而且,只要处理得当,这个过程对客户端来说可以是完全透明的,也就是玩家根本不知道服务器上有个进程意外结束,我们做了这么多的工作来将它恢复了。
事实上,这个过程的透明也是必须的,我们并不需要嚷嚷着告诉我们的用户,也就是玩家,我们做了多少多少事情来让你玩的更顺畅,又花了多少多少精力来解决因为服务器宕机而引起的麻烦,对于最终的用户来说,他只需要享受最好的服务。闲话少说,让我们继续。
真的已经完全解决了所有问题吗?
想象这样一个场景:我带着几个刚刚降临到艾泽拉斯大陆的伙伴冲向了艾尔文森林,去开荒霍格!正在霍格只剩下一丝血的时候,服务器稍稍卡了一下,等我缓过神来,面前的霍格骤然消失,地上也不见尸体。找了一圈,它正在出生点摇头晃脑,也在四处张望,但头顶上的血条分明是,满血!
怎么回事?
处理这张地图的场景进程意外结束了,服务器的宕机处理机制很快地恢复了这个场景进程,并且把我的客户端数据重新定向到了新场景。只是,事情并不是一切都完美。因为这个场景是完全重新创建的,这意味着所有的怪物也是重新创建并被摆放到了初始位置,所以,只剩下一丝血的霍格碰上了好运气……
类似的还有,正在护送NPC返回营地,在稍微停顿了一会儿之后,NPC又重新回到了原来的地方,等等。
虽然这比起最初的“客户端被迫断开连接,服务器端数据丢失”要进步了许多,但会给我工资的老板仍然可能不太满意,他希望,霍格应该还在我的面前,而且只有一丝血,那个跟着我的NPC也应该还在我旁边……
我要是不能说服老板,这是“根本不可能完成的任务!”,那也就只能坐下来再试一试。
也许,可以考虑将所有对象的数据都保存到数据服务器上,当然,这要求每个怪物都跟玩家一样,有一个唯一ID,这一点实现起来可能会有些麻烦。
再不然,为对象提供一个从已有的内存数据构造的方法,这样便可以使用共享内存来保存现场数据,再从共享内存中恢复出原来的对象。理论上来说,这个方法是可行的,只是,这三十多个字的文字描述要用C++来实现也可能将会是一项极大的挑战,所以,这也仅只是可供参考的一个尝试方案。
我想,我们走的够远了
让我们先暂停一会儿,回过头来看一看最初的目的。其实我们想要的只是尽可能的让服务器进程不要宕机,如果实在是没有办法,就尽可能的让宕机后的玩家损失比较小,不需要我们做大量的工作去做善后处理。
很简单的需求,似乎我们纠缠的有些过头了。
写出能够稳定运行的程序是对程序员的最基本要求,如果一个程序连稳定性都不具备,那根本都不用再去考虑功能啊、扩展啊等其他标准了。但是,正如我最开始所说的,没有一个人能够100%保证他写出来的服务器程序是绝对不会崩溃的。我们所能要求的只是尽可能的仔细,尽可能的多一些必要的测试,离安全尽可能的更近一步。
剩下的就是在宕机后如何降低损失的问题了。
对于一般的MMOG来说,玩家在进入游戏时会从数据库中将该玩家的所有相关数据读到内存,以便快速的进行游戏逻辑的处理,而在玩家下线时再将数据的改动存回数据库。
显然的,当服务器进程出现意外宕机时,内存中所有的数据都丢失了,这也就造成了玩家数据的回档,而且玩家在游戏中呆的时间越长,回档的损失就越大。所以,一个被广泛采用的做法是为玩家数据实现一种定时存盘的机制,就像现在大多数的单机游戏一样,AutoSave。比如,每5分钟自动为玩家存一次盘,这样就可以使得回档的最大损失控制在5分钟以内。
另外,对于一些重要数据的变动,比如玩家花大量游戏货币购买了一件贵重的武器装备,这时可将玩家数据立即做一次存盘操作,这也将有效的减少玩家的重大损失。
听起来这是一项不错的技术,在意外宕机的时候最多只回档5分钟,而且还没有贵重物品的损失,玩家应该是可以接受的吧。
我已经听到了数据库维护员的咆哮
“数据库已经快要崩溃了,你就不能让每秒需要执行的SQL语句少一点吗?”
“呃………”
我一直以为我们的数据库非常强大,可以处理任何的数据,唯一的缺点就是查询速度比直接内存读取要慢很多。所以我使用了异步数据存取的方法,并且开启了多个数据库操作线程来并行的执行我的请求,运行的效果看起来还不错。
也许,我应该来算一算,每秒种究竟丢了多少条操作请求给数据库。
请允许我再自私一回,我已经很久没有提到WOW了……
大概可信的数字是,WOW一组服务器的玩家数量在3000到5000之间,去掉最大的数,再去掉最小的数,最后的平均值是,4000吧,就算4000。
4000人在线,假设也是每5分钟定时存盘一次,再假设所有玩家的存盘时间是平均分布的。这样算下来,每秒种就会有67个玩家向数据库发出存盘请求操作。
才67个,数据库维护组的同事就跟我说不堪重负了?笑话,这数据库服务器是谁买的?
先别急,67是玩家数,但是每个玩家的存盘请求不会只有一条SQL语句。
虽然每个游戏的内容都各有差别,但是一款MMOG需要存入数据库的数据少不了会有技能、物品、任务、宠物、好友、公会这些东西。取决于游戏的类型差异,每个游戏都会有自己的存盘方式,比如我可能会把所有的技能ID作为一条数据来存储,但是我也有可能把每个技能作为一条单独的记录来存储,这样可以方便对技能附加数据的扩展,等等。
但是,游戏中的物品存储大概都是相同的,只能是一件(组)物品作为一条记录来存储。
而且,可以说游戏中存储量最大的就是物品数据。算一算你的角色背包有多大,50格? 100格?还是200格?不要忘了银行、摆摊位、装备拦、宠物背包和邮箱这些地方也能放物品。并且,在游戏进行过程中,玩家背包中物品数据的变动也是相当的频繁,不断的有药品被用掉,不断地又有些小玩意儿被捡起来,不久后,它们又被卖给了NPC。
虽然你可以使用一些巧妙的比较算法来过滤掉那些实际上没有发生变动的物品更新,另外也不是所有的玩家物品数据变动都很频繁,但在实际运营中,尤其是当玩家的背包格数都很多的时候,物品数据的存盘的确会成为一个很大的问题。
除了物品,还有玩家的基本属性存盘,社会关系存盘等等,再加上全局公共数据的存盘,如公会数据,拍卖行物品数据,如果老板也要我在游戏中开上一家拍卖行的话。
这么一算下来,似乎是有些多了。
再一次的挑战
具体的数字将取决于游戏的类型和设计的数据表结构。
而数据库服务器能承担的每秒查询数则取决于数据库服务器的软硬件配置情况。
但是一般来说,数据库维护人员可能会告诉我,当每秒执行的SQL语句数达到1000条时,数据库服务器将会感受到明显的压力,我可能也会看到数据库执行队列中的请求数一直在增长,也可能会看到数据库服务器间歇性地拒绝响应,等等。
看起来我们又一次的面对到了巨大的打冷战。
这个问题的起因是什么?我们不希望服务器进程宕机时回档太久,所以我们增加了一个玩家数据定时存盘的机制,结果却导致了数据库请求的骤然增多。
那再退回到这个起点处,将定时存盘的时间间隔延长点,比如10分钟才存一次?数据库的压力会有缓解,但最初的问题却又会有所暴露。真是个两难的问题。
既想要玩家数据存盘间隔时间短一点,又不想给数据库造成的压力太大。
同样的需求似乎出现过很多回了:在中间加一层代理做缓冲。我们姑且称这一层代理为数据库代理服务器,它所要完成的工作是从场景进程收集玩家的定时存盘请求数据,再以一个低一点的频率写入到数据库。
听起来这又像是一个换汤不换药的做法,写入数据库的时间间隔还是变长了。但实际上在前面我们就曾经描述过,如果服务器进程不会出现意外的宕机,玩家数据只需要在他上线时读取,在他下线时写入即可,中间添加的这些定时存盘过程完全只是为了防范宕机回档所造成的巨大损失。
因为这个中间代理层的加入,我们把场景进程宕机的隐患与数据丢失的后果隔离开来了,现在即使场景进程宕机,数据还在数据库代理服务器上,当然这里又隐含了一个条件:数据库代理服务器需要足够稳定,不会在场景进程之前先宕掉。事实上,因为这个代理进程的工作是,我们完全有理由相信,这个进程是非常稳定的,那样的话,多久时间才把缓存的数据真正写入数据库,就看你自己的喜好了。
该结束了吧
是否有些似曾相识的感觉?
没错,前面我们曾经描述过一个数据服务器,也是这样说的。
所以,数据服务器,数据库代理服务器可以合并到一起,来共同保证数据的安全。
再加上场景进程与管理器进程的恢复协议,让服务器的重启对玩家保持透明。
看起来这个晚上可以睡个安稳觉。