很早就想写一篇这样的文章,可是第一工作较忙,第二,想用自己的开源服务器作为蓝本实现。由于自己前一段时间较忙,再加上自己也懒了一下,决定在这里补上,提供给大家参考。作为我将写出的"网络游戏服务器核心服务开发"的一部分(等我慢慢原创出来),希望通过这些文章,你可以大概了解以及学会如何开发一个高效的游戏服务器体系,并成组合在胸,其实游戏服务器做到极致就是简洁高效,减少复杂,这也是我开发的信条之一,如果你的服务器初级开发者都能看懂,那么你出错的几率就会很低,而且,更将带给更多的朋友信心。其实在以前参与开发某知名网游的时候,这个思想就存在了,一直在外面给别的朋友讲课,也大多提及这套系统,至今觉得这样的实现规则是我见过比较好的。但是自己重写的时候,却没有那么容易,很多地方想做的更通用一些,所以考虑的也就多了一些,好在是终于完成了。自己测试了一下,效能不错。其实这套系统,不仅仅可以用在游戏上,也可以用在很多应用方面,具有一定通用性。所以决定写出来,和大家分享。
在网络游戏中,由于数据的交互,我们时常需要这样的功能,就是从某一个介质(数据库文件或者其他)中获取玩家或者游戏的数据,然后在游戏运行期进行调用和修改。然后当玩家下线的时候,我需要把数据回写到介质(数据库,文件或者其他)中存储。
(方案1)
最简单的想到,我把玩家的数据放在数据库中,当玩家登陆的时候,我从数据库中将玩家信息载入内存,当玩家退出的时候,我再将内存写入数据库。OK,大功告成,一点也不复杂。恩,这个想法确实很好,好的是足够简单,代码很容易理解,因为简单,出错的几率也会很低。确实不失为一种解决这类问题的好方法。不过,话说归来,如果你的游戏足够的火爆,N多玩家兴奋的等待你的服务器开服的瞬间,进入你的游戏,体验你的游戏,当你开服的一刹那,3000-4000个玩家同时点击登录,会是一番怎样的景象?你可能会很骄傲,但是据我所知,数据库的访问可能未必能达到你瞬间处理登录3000人登录,我们把3000人排排队,初始化,登录,数据加载。你可能会说,数据库每条数据载入内存能达到1-2毫秒,以此计算,最慢我也可以用2秒多解决问题,实际真的如此吗?要知道,服务器还要有资源处理别的事情,不可能专心致志只处理你的登录。
好的,你说,既然如此,就让我们把方案再进一步改一下,不就是登录慢么,那么我可以在服务器启动的时候,从数据库把那些活跃的用户预先载入内存,这样服务器不就快了么?是的,很不错的方案,让我们把方案改进一下。
(方案2)服务器启动的时候,开始遍历数据库,一口气把最后登录过的5000个活跃用户数据资料,加载到内存中。好了,让我启动一下服务器,还是这个场景,3000-4000个迫不及待的玩家在等你开服的一刻。当你开服的时候,4000人中的2500人的数据可能已经在你的内存中了,那么,我只用查找剩下的1000人的数据就好了!服务器性能得到了大幅提升。没错,看上去很完美(其实也确实很完美),不过,当服务器运行一段时间过后,问题来了,现在同时在线的的玩家有4000人,如果我的服务器不稳定,突然在一个逻辑分支崩溃了,那么这4000个玩家在这段时间玩游戏的结果,岂不全付诸东流?
你肯定不会允许这样的事情发生,聪明的你,开始思考,如果我有一个定时器,没过一段时间,存储一下这4000个玩家的数据,那么就算服务器崩溃了,我也只会丢失距离上次存储时间一小段的数据而已。恩,对,说干就干。
(方案3)
服务器启动的时候,开始遍历数据库,一口气把最后登录过的5000个活跃用户数据资料,加载到内存中。然后我创建一个定时器,比如每5分钟运行一次,遍历一下我服务器上的所有活跃玩家,并存入数据库。很好,当你打开服务器的,有3000-4000个如狼似虎的玩家在涌入你的服务器开始享受游戏。大家很高兴在其中,但是玩家们发现,每到5分钟的时候,服务器就会变慢一段时间,过一会次恢复。滴答滴答,类似钟表的走动的声音,在深夜中格外的让人心烦。如果这时候你在看你的服务器,会发现每到5分钟的时候,CPU和内存就上去了,然后慢慢下来。有点类似心跳。
想做到更优秀的你,开始想,我有没有办法解决这个问题呢?毕竟,我的游戏服务器在到这个时间点,必须要做这个动作,这一点是没法省去的。就算优化代码,这部分成本依旧会拖累服务器,毕竟运算量在那里摆着。怎么办?你会想到,我的游戏服务器进程是一个,能不能我把这个动作,放在另一个进程去做呢?就比如你在饭馆吃饭,你点了你喜欢的菜点,服务器找到厨师下单,这时候服务员可以继续服务别的顾客,不用等菜品做完,只要厨师做完了,放在出餐口,按一下电铃,"叮咚",服务员去取,然后根据菜品的座位号送过去就行了,是不是很酷?我一直觉得,生活实际是我们最好的老师,在我面对有些问题百思不得其解的时候,生活中的点滴也许早就告诉了你最好的解决方法,所以我认为,程序员第一要具备的,就是一颗观察的心。呵呵,说多了,继续我们的话题。如果你有眼前一亮的感觉,那么祝贺你,你的思维进阶了!
好的,让我讨论一下,如何把这部分工作交给另一个进程,让它全权负责存储(厨师),而对于服务器(服务员),就不用管数据什么时候存储的,只要读取和修改就行了。所有游戏服务资源,专心提供给玩家服务。那么,我怎么做到,两个进程共同使用一个内存呢?这个。。。其实我一开始也不会,让我们谷歌一下,看看有没有前辈这么做过?当我点击搜索的时候,居然有10多万个符合条件的结果???什么?共享内存?这个东东可以跨进程访问内存?这不就是我要的东东吗?哈哈,看来,操作系统开发者早就替我们想到了这一点,嘿嘿,让我们学着站在巨人的肩膀吧。来,继续完善我们的方案。
(如果想了解共享内存的特性,请参考 )
(方案4)
服务器启动的时候,开始遍历数据库,一口气把最后登录过的5000个活跃用户数据资料,加载到共享内存中,然后游戏服务器从中读取,如果没有,那么从介质(数据库或者文件)中加载进来,放入共享内存,然后我们启动一个程序,这个程序代码很简单,定时将共享内存的数据刷入介质(数据库或者文件)中。好,依旧有3000-4000个如狼似虎的玩家等待进入你的游戏,当你服务器启动的时候,大家开始享受游戏,这时候你可以笑了,因为你发现,玩家再也不会卡了。共享内存还有一个很好的特性,就是你的进程如果崩溃了,只要不是所有的共享内存引用都没有了,下次启动内存还在,不用重新加载。玩家的数据得到了最大的保护。(windows的特性,Linux就算都崩了,只要你不清除,就一直在),哇塞,这个功能太有用了!我兴奋了,你呢?还有就是,就算数据存储失败了,游戏服务器不受影响。数据库崩溃了,只是此后不再缓冲中的玩家不能登录了,其他玩家可以继续游戏。好了,我的游戏服务器很强大了,不过问题又来了,当我的游戏服务器运行一段时间后,随着登录和离开的玩家越来越多,我的共享内存越来越大,这可不行!聪明的你肯定会想,我的游戏服务器有一个最大允许玩家在游戏中的数量,那么其他的数据是不是对我无用呢?比如你的服务器允许3000个玩家在线,可否把共享内存上限控制在最多加载10000个玩家数据(多余的7000个是活跃但没有登录的用户,用于增加登录玩家命中率),很好,想到这个,当然可以,为什么不可以?如果共享内存达到了10000个玩家数据,我只需要删除一个最不常用的玩家,替换成新玩家即可。好的,那么网上有没有这样的算法呢?我们继续谷歌一下,好家伙,依旧有十几万的索索结果,看来遇到我想到的问题的人还真不是少数。什么?LRU算法?这个貌似被经常提及。我可以使用它,不过我想更完美一些,我的删除算法是,最后最不常访问的玩家,而不是简单最不常登录的玩家,因为有时候,我需要查询别的不在线的玩家信息。我要最大的保持共享内存的命中率。MRU算法?呵呵,太好了,这个正好对我的胃口。
(如果想了解MRU算法,请参考)
(方案5)
服务器启动的时候,开始遍历数据库,一口气把最后登录过的5000个活跃用户数据资料,加载到共享内存中,然后游戏服务器从中读取,如果没有,那么从介质(数据库或者文件)中加载进来,放入共享内存,然后我们启动一个程序,这个程序代码很简单,定时将共享内存的数据刷入介质(数据库或者文件)中,在服务器对共享内存访问上,添加一个层,负责MRU算法。哇塞,你真是太伟大了,你的系统越来越健壮了,连单一服务器崩溃都无法阻挡你的前进了,而且你的共享内存大小是恒定的,连内存碎片都只有对你叹息的份了,它们也奈何不了你了。真棒!那么,聪明的你,可否让我们更完美一些呢?答案当然是可能的。那么,这个思路看上去哪里还有瑕疵呢?对了,就是共享内存本身,因为我的游戏服务器需要修改玩家数据,那样肯定是会修改共享内存的数据。而我的定时进程,也会写我的共享内存(比如标记存储成功的时间戳)。好家伙,我有不好的预感。多线程读写共享内存,如果两个进程同一时刻一起写一个位置,程序必崩无疑。这个家伙太讨厌了,我不能让他阻挡我。我要解决你!让我再去谷歌一下,我现在觉得谷歌上肯定有很多朋友遇到和我一样的问题。恩,什么,共享内存锁?信号量?这个真给我雪中送炭啊。不过,等等,再让我看看,什么,这个锁会降低系统的性能?也是,多线程共享资源,哪个锁能不降低性能呢?怎么办,我要完美,完美!不用锁,把服务器的性能体现到最强!有办法吗?有办法吗?在无数次纠结之后,我忽然想到,如果我把共享内存一份玩家数据,分为头+体。头由定时进程进行修改,体由游戏服务器本身修改。那么,会有改善吗?对于游戏服务器,我对数据头只读不写,对体只写。定时服务器我对头只写,对体只读。只要指针点不同,那么是否就能绕开锁呢?我要保证同一时刻,就算都写,只写一个地方。OK。如果你也想到了,证明你比我聪明多了,因为我想这个用了2天时间,呵呵。
(方案6)
服务器启动的时候,开始遍历数据库,一口气把最后登录过的5000个活跃用户数据资料,加载到共享内存中,然后游戏服务器从中读取,如果没有,那么从介质(数据库或者文件)中加载进来,放入共享内存,然后我们启动一个程序,这个程序代码很简单,定时将共享内存的数据刷入介质(数据库或者文件)中,在服务器对共享内存访问上,添加一个层,负责MRU算法。共享内存一个用户数据单元分为头和体,对于游戏服务器,只关心"体"就行了。对于存储服务器,它关心的是"头"。双写入错误,你拿我也没办法了吧。哈哈。哦,对了,可能机智的你会问,你这个想法依旧不完美,如果我的游戏服务器正在写还没写完,这时候你的存储进程恰好在存储,岂不会存储一个"半截"的脏数据?是的,说的好,关于这个问题,我的想法是,我的存储服务,每次都采用memcpy的方法把数据取出来。那么你肯定会辩驳,别蒙我!memcpy会被线程打断,OK,没错,不过,要符合玩家数据正在修改+存储服务正在存这个数据+memcpy正好拷贝到这个点。几率并不大,那么,就算发生了,你的数据5分钟后还会恰好在这里吗?如果次次都遇到,说明你要去买彩票了。脏数据会存在,那么我能控制在一段时间后会被健康的数据覆盖,就行了。
写到这里,大家肯定知道了一个思路,下面,就让我用代码来给大家说明一下,毕竟,百说不如一练,写出来才算本事!
好,下一章,我来讲讲怎么实现,当然,个人偏好,我用我的开源服务框架实现这个体系吧。
多级缓冲的服务器数据服务机制实现
内容版权声明:除非注明,否则皆为本站原创文章。