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

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

 前言

不管是在业界开源领域,还是内部分享中,很少会有专门针对游戏业务特征进行专门设计的组件、类库或者框架。我们从游戏的客户端方面来看,一款专业的游戏客户端引擎,已经是游戏开发的标配,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将数据写回数据库

    总结

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

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

    相关文章
    |
    27天前
    |
    缓存 监控 定位技术
    |
    2月前
    |
    消息中间件 缓存 NoSQL
    Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
    【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
    78 6
    |
    2月前
    |
    缓存 Java Shell
    Android 系统缓存扫描与清理方法分析
    Android 系统缓存从原理探索到实现。
    60 15
    Android 系统缓存扫描与清理方法分析
    |
    2月前
    |
    监控 Java Linux
    Linux系统之安装Ward服务器监控工具
    【10月更文挑战第17天】Linux系统之安装Ward服务器监控工具
    55 5
    Linux系统之安装Ward服务器监控工具
    |
    2月前
    |
    存储 监控 网络协议
    服务器压力测试是一种评估系统在极端条件下的表现和稳定性的技术
    【10月更文挑战第11天】服务器压力测试是一种评估系统在极端条件下的表现和稳定性的技术
    122 32
    |
    1月前
    |
    存储 安全 关系型数据库
    Linux系统在服务器领域的应用与优势###
    本文深入探讨了Linux操作系统在服务器领域的广泛应用及其显著优势。通过分析其开源性、安全性、稳定性和高效性,揭示了为何Linux成为众多企业和开发者的首选服务器操作系统。文章还列举了Linux在服务器管理、性能优化和社区支持等方面的具体优势,为读者提供了全面而深入的理解。 ###
    |
    2月前
    |
    NoSQL Linux PHP
    |
    2月前
    |
    缓存 NoSQL Ubuntu
    大数据-39 Redis 高并发分布式缓存 Ubuntu源码编译安装 云服务器 启动并测试 redis-server redis-cli
    大数据-39 Redis 高并发分布式缓存 Ubuntu源码编译安装 云服务器 启动并测试 redis-server redis-cli
    57 3
    |
    2月前
    |
    Apache 数据中心 Windows
    将网站迁移到阿里云Windows系统云服务器,访问该站点提示连接被拒绝,如何处理?
    将网站迁移到阿里云Windows系统云服务器,访问该站点提示连接被拒绝,如何处理?
    |
    2月前
    |
    域名解析 缓存 网络协议
    Windows系统云服务器自定义域名解析导致网站无法访问怎么解决?
    Windows系统云服务器自定义域名解析导致网站无法访问怎么解决?