游戏服务器缓存系统如何设计

简介: 游戏服务器缓存系统如何设计

 前言

不管是在业界开源领域,还是内部分享中,很少会有专门针对游戏业务特征进行专门设计的组件、类库或者框架。我们从游戏的客户端方面来看,一款专业的游戏客户端引擎,已经是游戏开发的标配,flash,Cocos,Unity,Unreal等,但是服务器端,我们几乎找不到同样重量级的产品(当然有针对海外开发者快捷开发的服务器平台,比如GAE,GameSparks,PlayFab等能满足常规的抽卡游戏服务器的业务)。

在游戏服务器端开发所有要面对的问题中,有两个是最核心和最普遍的:一是和客户端的网络协议通信;二是游戏用户的数据处理。对于和客户端通讯的这个问题,大量的游戏开发者会使用“通用”的开源组件,比如Protocol Buffer、Thrift、Jetty、Node.js等等通信或RPC框架。当然,很多大厂有自己封装的网络库,遇到问题可以有计划的进行排查和修复,如果你用到了一些开源的网络库遇到了蛋疼问题,你可能一个礼拜也不一定能解决,我曾经在thrift的rpc调用上遇到过假死的情况,为了解决这个问题我研读他们的代码,发现是没有设置超时时间导致的io读写被阻塞了。

在游戏业务领域中,一般情况是客户端发起请求,服务器端接收请求,再根据请求处理对应的业务逻辑并做出响应。

除此之外,我们还会遇到“数据同步”方式:游戏中某个角色的HP、人物移动,技能的buff生效,活动生效,需要在客户端和服务器之间、客户端和客户端之间同步。这造成了一般情况下通信协议的大量增加。

对于这个问题,不管是Memcache还是MySQL,或者是Redis,都不能完全满足游戏开发者的需求。很多团队尝试过各种组合和修改,试图创造出利用现有开源软件,建设既能迎合灵活的需求变化,又具备高延迟和高可用的数据处理系统,但最后这些努力基本上都很难圆满成功。因为我们在游戏服务器端代码中,还是充斥着大量的内存、缓存管理,数据同步、落地等等代码。而且每个游戏都要重新去写一遍这些类似的功能,不能不说是一种浪费。

如果我们要想出一种能满足“游戏”这个业务领域的数据系统设计,那么就一定要搞清楚为什么在如此之多的开源项目和游戏团队中,没能实现完美契合的原因。

通信模型分析

游戏中的通信,一般和操作有关。这些操作一般分为两类:

    • UI面板类操作
    • 战斗场景操作

    这两者的最大区别,就是UI面板类操作一般无需让其他玩家看见。而战斗场景操作则需要广播给所有玩家看到。

    在第二种情况下,一般就不是客户端主动发起,而是服务器端直接推送实际数据,然后客户端直接显示这些数据。这个模式和简单的“推送”还不一样,而应该更进一步,是一种从服务器端发起的,向客户端“同步”数据的请求。

    因此,一个好的游戏服务器端框架,应该是能同时支持请求-响应模型和“推送同步”模型的。

    业务数据处理流程

           在各种游戏中,MMORPG是数据处理最为复杂的一类,也是最典型的一种“重服务器端”的游戏类型,因此可以作为游戏业务中通用性的参考标准。在MMORPG中,我们可以发现,数据的处理需求,和一般互联网业务大相径庭,它体现出的是一种明显的“集中”式的数据处理需求。我们可以从一般MMORPG的服务器架构中体现出来:

    image.gif编辑

    在游戏业务中,一般我们都会发现以下的特点:

    1、低延迟:游戏中用户会产生大量操作,并要求服务器“实时”反馈,在大量动作类型的游戏中,一般都会要求服务器的反馈时延在50ms左右。因此游戏开发者都习惯于尽量减少后台进程间的交互,尽管这对提高系统吞吐量很不利。

    2、大量实时交互:在线游戏的特点,就是很多玩家可以通过服务器“看见”彼此,能实时的互动。因此我们必须要把用户的在线数据,集中到一起,才能提供互相操作的可能;而且A用户操作B用户的数据,是最常见的数据操作,所谓战斗玩法,就是互相修改对方的数据的过程。

    3、数据集中:游戏是一个几乎完全虚拟的世界,在游戏中的数据,实际上很少能在其他系统中产生价值。而游戏逻辑也禁止通过游戏以外的方式,修改游戏的数据。所以游戏中的数据,一般都会集中存放在单独的数据库中。由于没有数据共用的需求,所以也不需要把GameServer里面集中的逻辑划分出很多单独的进程模块来。

    4、数据变更少:实际上游戏的数据变更还是很快的,比如游戏中的每次中弹,都要减少HP的数值。但是游戏里的数据,一般都遵守这样一个规则:“变化越快的数据,重要性越低”。也就是说,游戏中是可以容忍一定程度的数据不一致和不完整的。而游戏中的数据,一般会分成两类:玩家存档和游戏设置。

    对于玩家存档来说,其单条数据量一般不大,但会有大量的记录数,因为每个玩家都会有一个存档。但是其读取、修改,一般很典型的和玩家的登录、登出、升级等业务逻辑密切关联,所以其缓存时机是比较容易根据业务逻辑来把握的。而对于游戏设置数据来说,几乎只有升级游戏版本的时候才会修改,大部分运行时是只读的,其缓存简单的读入内存就解决问题了。

    一般缓存在游戏场景应用的缺陷

    缓存的价值

    不管缓存系统如何设计,其本质永远是「用空间换时间」,也就是提升数据获取的效率。同时,也可以作为一种兜底降级方案,当源挂掉后可以先用缓存内的数据。

    一般跨进程的缓存系统,无法解决游戏要求的低延迟问题。级别是同机房,每次数据存取都需要10-20ms的时间,对于游戏战斗中大量的数据读、写来说,是很难接受的。(但是一些回合制战斗、低频操作还是有用的)

    通用型的缓存系统或者数据库,一般都比较难集结多个进程,形成一个完整的数据存储网格。这让玩家间的互相交互产生了额外的难度,开发者必须先想办法确定玩家的数据在哪个后台进程上,然后才能去读写。一般的数据库或缓存系统,为了保证数据的一致性或者完整性,往往会需要牺牲一些分布式的能力。而这种牺牲在游戏业务中,其实是一种浪费,因为游戏的很多数据都无需这种能力。

    通用性数据系统一般不依赖于特定的语言,所以很少能直接把某种“对象”存入到数据系统中。在游戏开发中,需要存储的数据结构数量往往是非常大量的:一个普通的游戏,基本上都会超过100种数据结构。对于每个数据结构,都去建表或者编写序列化/反序列化配置,是一种非常累人的工作。——明明在代码中,已经用编程语言定义了他们的结构,还要重复的搞一次。

    根据上面说的这些问题,我们实际上是需要另外一种完全不同设计思想的数据系统。对于游戏业务来说,一个好用的数据系统,应该包括这样一些特点:

    可以利用GameServer进程内的内存进行自动化的缓存管理。由于GameServer进程往往集中了大部分的逻辑运算,所以大部分的数据缓存也应该在这个进程中,这样才能符合游戏所需的延迟要求。

    自动进行数据落地和容灾管理。由于游戏数据中有大量的“过程数据”,所以其一致性和完整性要求会稍微低于其他业务,所以应该利用这一点,让GameServer本身也可以是分布式的程序,从而提高系统整体的吞吐量。

    具备良好的编程易用性。最好是能直接存取编程中的对象,避免反复对数据结构的描述,节省大量的开发时间。

    现代游戏服务器端的几个典型运行时架构

    游戏本身的逻辑复杂性,导致了架构上也是分成很多不同的“门派”。和互联网/电商日渐趋同的架构不一样,游戏的“运行时架构”,往往会向着不同方向更加的分化,而不是统一。下面就讲讲游戏领域架构的几个主要分支:

    1MMORPG

    这一类游戏主要采用“分区分服”类的架构。从表面上看,这类游戏的服务器架构似乎非常简单,就是硬生生的把游戏世界按照硬件集群分开来,克隆出很多的个平行的游戏世界。服务器中的通讯、计算、存储能力都是每个游戏世界单独一份。但事实上,并没有这么简单。MMORPG类的游戏,在服务器端主要有几个挑战:

    一个是海量网络广播的挑战,由于有大量的玩家的实时互动,比如玩家地图移动,动作表现都需要广播大量的数据包通知给其他客户端;

    第二个是大量计算任务需要快速的数据缓存的挑战,在游戏的战斗中,每一个动作都几乎需要对数据做读写,由于涉及大量不确定的玩家数据,所以在整个服务器中快速查询、修改玩家数据变的延迟变得非常苛刻。

    但是MMORPG类游戏,在业务领域上又有几个突出的特点:

    一是基本都是对在线玩家数据的操作,很少像电商那样,都是对持久化数据做操作;

    二是在线数据的分布,有一个虚拟的“游戏地图”作为分布的脉络,玩家总是从一个游戏场景,走向另外一个场景,数据是按场景来聚合的;

    三是游戏逻辑虽然复杂,但较少产生关联性的查询,只要少量的如“拍卖行”,“排行”这样的数据,是需要比较复杂的关联到其他数据单元的。

    所以MMORPG的服务器端架构,很自然的就采用以内存作为整个虚拟世界的缓存,然后按游戏地图进行进程分布的样子。由于数据都在内存中,才能满足战斗的低延迟响应。而按地图分布的进程,可以让玩家在游戏的过程中,在切换地图的过程中,把内存数据在进程间搬迁,这样既自然又实用,因为大部分的数据关联操作(比如战斗)都是以游戏地图为纽带的。

    在持久化存储上,基本上都很容易使用NoSQL来做,因为几乎都是在玩家登录的时候加载数据到内存,离线登出时回写到持久化且销毁内存数据,所以完全可以只通过一个索引来完成玩家数据的存取。

    image.gif编辑

    上图非常简单的描述了MMORPG游戏服务器的基本架构。

    2、棋牌类

    和MMORPG不同,棋牌类服务器的特点是,需要海量的用户在一起玩,比如需要有一个容纳所有玩家的游戏大厅,可以在那里找到所有的在线玩家一起玩;但是每个具体的战斗过程,又是小规模几个玩家交互的,比如需要同时开启成千上万的房间(或者桌子)来进行具体的游戏。因此这类游戏的服务器挑战,就有两个:一是全局所有玩家需要互相交互的需求;二是管理大量的小群体玩家动态交互通信群组的需求。

    为了解决这两个问题,一般来说服务器端会分为两个部分,一是大厅服务器,一是房间服务器。大厅服务器是一个巨大的广播集群,负责不太实时的数据传输和查询。房间服务器是一组可以快速租用、退还的小型实时广播服务进程。

    在大厅服务器中,所有的在在线的玩家,都按其ID来分布在多个进程中的一个,在玩家之间的查询、广播操作时,采用多个服务器并行操作,最后汇总结果的方式来提供。这样的操作延迟是会比较高,但是能让海量的用户数据存储到不同的机器上。

    而房间服务器则会负责提供具体的游戏广播功能,一旦玩家组成了群组进入,大厅服务器会拷贝数据到房间服务器,而房间服务器就只对这几个玩家负责了,游戏结束则清理掉这些玩家数据,准备新的游戏。

    image.gif编辑

    2、MOBA类

    从本质上来说,类似《英雄联盟》这类游戏,他们的游戏逻辑比较复杂,所以“房间服务器”里面要运行的逻辑会比较多,在MOBA类游戏里,有一个最大的难点,是“自动匹配”。

    只有尽量在更大的范围内匹配在线玩家,等待的时间才会更短,匹配结果才更准确;然而大量的数据充斥在有限的进程空间中,本身又会导致承载压力,被迫要把数据分散到其他进程去。这个矛盾是贯穿匹配系统的问题。

    为了解决这个问题,有些游戏采用退缩的策略,就是降低匹配的准确度,尽快的把玩家匹配起来。这样随机的把玩家放入不同进程的匹配队列中,只要人满了就开始玩,这样也是可以的。有一些游戏则费比较大的功夫,做一个分布式的内存缓存,希望尽量多的玩家在一起匹配,付出的代价就是需要更多服务器间的数据交互,以及延迟。

    image.gif编辑

    上面说的这集中典型的模式,在现在的游戏服务器架构中,往往并不是单一出现的。比如现在的《魔兽世界》,就可以让存档在不同服务器的玩家,都连到同一个在线场景服务器中玩。而如《DNF》这类游戏,社交场景所连接的“大厅服务器”本身也是可以按地图划分的。尽管游戏服务器架构的形态日趋复杂和分化,但其中的思想是统一的,就是“按业务逻辑所要求的数据缓存布局”来分布。

    缓存实现的几种方案对比

        方案 1

           原理

           读数据时从缓存内读取,如果缓存不存在则从数据库中筛选,并放入缓存内

           数据变更时直接操作数据库,并清除缓存

          实现方式

           启动一个game_server进程将所有 ets建立起来,ets会分为 ets_player,ets_bag 等将数据分开储存到ets表中,一般以玩家id作为ets的key,读取某个数据会优先访问ets表中数据,再从数据库筛选

           这个方案,缺点十分明显,只能减少部分读取操作,在频繁更改数据的情况下,缓存几乎没有作用,缓存在不停的被清理,当需要读取数据时又需要重新从数据库中拉取。

       方案2


           原理

           读取策略与方案1一致

           数据变更时变更缓存内数据,不操作数据库,当玩家下线时再将缓存数据同步到数据库中

           实现方式

           同样会启动一个game_server进程将所有 ets建立起来,不同的是ets中的每一行多了一个是否修改的标识位,玩家读取数据依然优先访问ets表中数据

           当玩家修改数据,例如升级后,修改ets_player中对应的数据,并将ets中修改标识设为已修改

           玩家下线后,顺序检查各个ets中的数据,将标识为已修改的数据同步到数据库中

           方案2与方案1相比,最大的变更在于数据更改时不清理缓存,而是修改缓存,这就避免了频繁的从数据库中筛选数据。

           将数据库同步操作积累到玩家下线而不是立即写库,可以减少许多sql语句,例如玩家在上线期间由 1级升到5级,下线写库只会执行一条sql,而即时写库则需要4条,对2,3,4,5分别执行update

           最初我们以为这套方案已经可以满足游戏线上运行需求,直到我们做了一个压力测试(膝盖中了一箭?),例如你有10多个系统,玩家,竞技场,背包,抽奖,等等等等那么在玩家下线时会出现数据库操作高峰。在压力测试时峰值十分明显,尤其是开服初期导量十分多的情况。

       方案3

           原理

           由于玩家下线时间可能会出现峰值,所以我们想到了使用定时写库这种方式,将写库时间设定为我们认为合适的间隔

           实现方式

           每一张数据表都启动一个对应的game_server来管理数据,进程内部会创建一个ets表,玩家读取/修改数据时都会访问对应的game_server

           当玩家修改信息后,game_server内部会记录修改的信息。

           game_server内部启动一个定时器,每隔一段时间将修改的数据同步到数据库中

           与方案2相比,方案3将写库时间控制在自己手里,在游戏开新服导量期间,可能设定数小时同步一次数据,可以很大程度减少sql压力

           但这套方案也有缺陷,由于玩家查询/修改数据都需要访问同一个game_server,部分game_server如玩家信息,就会出现瓶颈,出现超时现象

      方案4


           原理

           为了解决方案3中game_server访问的瓶颈问题,我们在game_server外层又添加了一层缓存

           实现方式

           在方案3的基础上,又将方案1,2中的cache_process添加回来,玩家读取数据会先访问cache_proces中的数据,如果其中没有再访问对应的game_server进程

           最终 读取数据流程       玩家获取数据 -> cache_process中查找 -> game_server 中查找 -> 数据库中查找

           玩家更改数据 -> 更改cache_process

            -> 更改game_server 中数据 -> 一定时间后 game_server将数据写回数据库

    总结

    游戏服务器和普通互联网业务服务器端,最大的区别实际上就在于“状态”。游戏服务器的状态是实时快速变化的、可以容忍丢失的、需要大量广播同步的;普通互联网业务服务器的状态一般是持久化的、不容忍丢失的、只和特定客户端相关的。

    所以一个好的游戏服务器框架,在通讯和数据这两个基本层面,会和一般我们所接触的开源组件有很大的差异。所以现在大部分的游戏公司的服务器端,其实都不是使用完整的一个框架,甚至几个不同的项目,其服务器端架构都不一样。大家在看到阿里巴巴公司共享大量的开源软件的时候,也应该看到电子商务的业务特点,其实是比较适合做这种统一框架的。网易、腾讯、金山在游戏研发领域,其实也有很多的经验,但是很少能有一些开源软件公开出来。主要原因还是游戏领域的“通用性”太难把握。

    相关文章
    |
    21天前
    |
    Linux 网络安全 数据安全/隐私保护
    SSH工具连接远程服务器或者本地Linux系统
    SSH工具连接远程服务器或者本地Linux系统
    20 0
    |
    4月前
    |
    弹性计算 Linux Shell
    阿里云ecs linux系统如何进行系统盘的扩容
    【1月更文挑战第25天】【1月更文挑战第122篇】阿里云ecs linux系统如何进行系统盘的扩容
    229 1
    |
    7天前
    |
    Linux 网络安全 数据库
    linux centos系统搭建samba文件服务器 NetBIOS解析 (超详细)
    linux centos系统搭建samba文件服务器 NetBIOS解析 (超详细)
    |
    8天前
    |
    存储 安全 Unix
    服务器的常用系统
    【5月更文挑战第5天】服务器的常用系统
    31 9
    |
    15天前
    |
    监控 负载均衡 网络协议
    |
    19天前
    |
    监控 安全 Linux
    Linux系统之安装ServerBee服务器监控工具
    【4月更文挑战第22天】Linux系统之安装ServerBee服务器监控工具
    59 2
    |
    19天前
    |
    缓存 Linux
    linux系统缓存机制
    linux系统缓存机制
    |
    25天前
    |
    JavaScript 前端开发 UED
    Vue工具和生态系统: Vue.js和服务器端渲染(SSR)有关系吗?请解释。
    Vue.js是一个渐进式JavaScript框架,常用于开发单页面应用,但其首屏加载较慢影响用户体验和SEO。为解决此问题,Vue.js支持服务器端渲染(SSR),在服务器预生成HTML,加快首屏速度。Vue.js的SSR可手动实现或借助如Nuxt.js的第三方库简化流程。Nuxt.js是基于Vue.js的服务器端渲染框架,整合核心库并提供额外功能,帮助构建高效的应用,改善用户体验。
    21 0
    |
    1月前
    |
    Java 关系型数据库 MySQL
    阿里云服务器Linux系统部署JavaWeb项目
    阿里云服务器Linux系统部署JavaWeb项目
    |
    1月前
    |
    Linux
    查看服务器的配置,系统,cpu等信息
    查看服务器的配置,系统,cpu等信息
    26 0