如何设计分布式的"小"游戏

前言

  • 从前….做的游戏基本都是小区概念的,区和区之间数据在物理上是隔离的,网络一般也都是隔离开的(除了少数游戏会有一个注册发现discover的单点服务器统一管理服务器之间的链接)
  • 是否开区从服务端开发的角度,一般是单区(一组服务器)ACU(最高同时在线)服务端是否撑的住(主要是内存、磁盘IO、带宽、cpu)
  • 当然很多服务端设计里也会把单区做成进程可扩展的方式,比如MMO游戏根据场景分不同的GameService(逻辑服务器进程)开房间玩法的游戏会有多组BattleService(战斗服务器)分摊压力,也避免在某个进程出问题的时候不至于整区不能游戏
  • 绝大部分小区的游戏服务端数据存储(DB)是个单点进程,或者有些不用单独的DBService,直接由GameService操作数据库的读写,数据库可能是Mysql、MongoDB(落盘的物理存储)+ redis、memcache(高速内存存储)
  • 玩家玩游戏需要选择他自己游戏角色所在的区,然后进入这个区开始游戏

常见的游戏

常见的MMO游戏服务端架构

  • 玩家通过服务端区列表选择所在的区(拿到LoginService的IP:Port)
  • 链接LoginService认证账号,获得这次登陆的凭证(token、signature)GateService的IP:Port,同时LoginService把凭证存储到Redis或者通过消息同步给GateService
  • 链接GateService(长连接),进行游戏
  • 客户端的交互操作经由GateService转发到GameService,GameService的回执和通知消息经由GateService发送给玩家客户端
  • GameService 通过 DBService 从数据库(DataBase)拿到玩家的游戏数据,数据变更后发送到DBService,DBService负责在合适的时间写入数据库(这里一般认为GameService的存储请求到达DBService,DBService将新的数据覆盖到内存就算成功,DBService会在一定的时间后将数据写入数据库(磁盘写入操作);因为一般情况磁盘读写是最耗时的操作)
  • 图中黄色较粗的链接线表示内网的数据传输,一般内网的带宽是1G,服务端一般认为这里是不会有带宽资源不够的情况
  • 图中没有单独的BattleService,因为对于MMO游戏来说,场景同步一般才是最影响服务端性能部分,游戏服务器的架构也更倾向处理场景同步的问题
  • 而对于没有场景同步需求的游戏,或者一场战斗只是几个人的游戏,比如1V1的2人游戏,MOBA类型的10人游戏;更在意的是战斗事件处理的速度到客户端的网络延迟,大多这样的游戏是把战斗从GameService单独拆分出来,或许还会把Matchmaking(匹配)服务单独拆分出来

也许你的游戏也有遇到过

  • 游戏开服的时候,运营厂商疯狂的开区,甚至你想和朋友在一起玩的时候还需要排队进入
  • 游戏运营到中期,不用排队进服了,但是好友列表、公会成员列表里面的头像灰态的越来越多
  • 游戏运营到后期,运营商开始公告合服相关的信息,2+ 个区合并到一起,期望回到游戏前中期的盛世,也节省部分运营成本

倾向的需求

  • 在游戏里的任何人都可以在一起玩(开房间的玩法)
  • 不再有或者减轻服务端崩溃(重启)对玩家造成的影响
  • 服务端还是需要更新的,但是这种更新维护对玩家尽量是透明的
  • 没有合服的动作,或者合服对玩家是透明的

这些都指向了 大区概念(分布式)的游戏服务器架构

大区概念的数据

  • 数据集中:数据集中存放,数据后挂slaver节点备份数据,当master数据服宕机,使用slaver的数据
    • masterslaver 数据同步是一个异步的过程,如果使用数据集群的处理方式
      (1 * master + N * slaver,每次更新必须保证 1 * master + M * slaver 更新成功返回成功(M < N))
      master 宕机后只是有更大可能选择到了一个数据最新的 slaver 节点,这是可以被接受的么CAP 布鲁尔定理
    • 假如是一个全球大区(全球通服)的游戏,数据节点部署到那个位置才是合适的?
      (相对中心位置并且有不错网络环境支持的新加坡还是重点运营的大洲?)
    • 在倾向分布式的系统架构里,单点服务的存在一定是最容易被拿出来说道的话题

  • 数据分区:战斗只关心拿到正确的数据,不在意数据从哪里来的
    • 只有一个数据节点不仅会加大资源竞争,而且一但这个数据节点崩溃或者停机,所有玩家都不能玩了,数据分区减少了资源压力,如果有某几个数据节点出问题,也只影响部分玩家
    • 玩家战斗需要的数据量理论上是小于完整数据量的,如果创建数据时选区规则是就近数据区路由的方式,也可以保证在大部分时候,玩家是从较近的数据节点,经过较少的路由,较快的拿到了角色的全量数据
    • 当然,如果玩家在亚洲建的账号(数据区在亚洲),但是后来玩游戏是在欧洲,他需要经过跨州的网络拿到自己的数据(一般数据区只有数据存储的逻辑,这种情况可以迁移数据到欧洲的数据区)
    • 如果常规做法是匹配到战斗是客户端从服务端拿到加密的战斗相关的角色数据包,直接发送到战斗服务器,战斗服务器解出数据并校验的方式(没有战斗服务器和数据区的直接交互),也存在另外的问题:在遇到跨数据区交互的内容,比如排行榜、好友、公会的功能,即使这些功能里只有玩家角色少量的信息,也会存在跨区数据查找的过程,这个过程是不可靠和难控的
    • 这种数据交互一些的做法参考:玩家ID自带可解出数据分区的信息(或者数据分区和ID绑定(避免数据区迁移需要变更ID的情况)),好友服务器向数据区服务请求数据内容(主动要),一定时间内没有返回(超时时间),直到 到数据。
    • 这样做法的问题显而易见,每次查看好友列表都需要向N组数据服务器请求角色数据。所以一般好友服务器会缓存好友列表界面需要的数据,比如角色名(name)、角色等级(level),这些数据发生变更的时候,数据服务器主动推送变更通知到好友服务器,好友服务器更新自己的缓存。
    • 作为推送的消息,数据服务器是不能确定推送内容一定被好友服务器接收并处理了,特别是全球服跨物理大洲的推送
    • 一般情况,好友里数据的更新延迟是可以被接受的,我们这里也假设好友列表刷新的数据可以接受一定程度的延迟和不正确
    • 这种情况下我的建议是:数据服应尽量保证数据推送到了好友服务器(至少在出现网络问题的时候有重复推送的机制);对于重传N次后依然没有到达好友服务器或者好友服务器没有正确处理掉的数据,用版本号确认的方式做补偿更新
    • 补偿更新具体操作是:好友的详细信息里有一个Version标记(这个标记由获得好友详细信息的数据一起带过来,如果只是用作后面建议的数据同步方式,建议这个Version(Friend.Version) == 数据服务器用户数据的Version(User.Version)),假设Version自增的,理论上 Friend.Version <= User.Version;客户端的一些行为:比如打开好友详情面板(展示单个好友更多信息)时,带着Friend.Version 到 数据服务器请求 单个用户详情信息,Friend.Version != User.Version 的时候,数据服务器检查到Friend.Version != User.Version,主动补偿推送最新的数据到好友服务器。(当然这种补偿的做法可以是双向的,某些时候好友服务器发现自己数据错误的时候,也可以主动向数据服务器要数据(主动要)

  • CAP wiki (https://zh.wikipedia.org/wiki/CAP%E5%AE%9A%E7%90%86)
    • 在理论计算机科学中,CAP定理(CAP theorem),又被称作布鲁尔定理(Brewer’s theorem),它指出对于一个分布式计算系统来说,不可能同时满足以下三点:[1][2]
    • 一致性(Consistency) (等同于所有节点访问同一份最新的数据副本)
    • 可用性(Availability)(每次请求都能获取到非错的响应——但是不保证获取的数据为最新数据)
    • 分区容错性(Partition tolerance)(以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择[3]。)
    • 根据定理,分布式系统只能满足三项中的两项而不可能满足全部三项[4]。理解CAP理论的最简单方式是想象两个节点分处分区两侧。允许至少一个节点更新状态会导致数据不一致,即丧失了C性质。如果为了保证数据一致性,将分区一侧的节点设置为不可用,那么又丧失了A性质。除非两个节点可以互相通信,才能既保证C又保证A,这又会导致丧失P性质。

  • 简易的消息流程
  • 其中 Operator Change Avatar 标识一次 Package for battle 内部数据修改应重传一次 Package for battle encode 数据
  • 加密方式有很多种,简单的如图示,数据服务器持公钥编码,战斗服务器持私钥解码

大区概念的网络

  • 跨大洲的网络通讯:物理距离在我的理解里是无法避免的
    • 一条消息从亚洲到欧洲,平均需要200ms,这只是平均值(https://wondernetwork.com/pings/),在极端情况这个值会很大,大到玩家觉得游戏很卡
    • 传说一些有钱的游戏购买了运营商的专线网络做加速,但是那个成本实在太高了
    • 考虑如何减轻网络延迟的影响,或者想办法把玩家尽量放到离他很近的游戏服务器(需要基于游戏玩法本身考虑)
    • 确保在跨区交互上也是符合CP(参考CAP)
    • 下面的一些描述基于游戏是一个开房间的方式,战斗服务器需要在战斗开始拿且仅拿一次数据,结算返回到逻辑服务器(离数据近的服务器)做数据更新
    • 战斗开始,战斗服务器拿到的数据是否可以相信客户端发送到服务器的数据(客户端从数据服务器拿到战斗需要的加密数据包,并在战斗开始的时候发送到战斗服务器,战斗服务器解密并验证数据合法性),这样做可以避免跨大洲的数据交互
    • 战斗结算,以类似战斗开始的方式由客户端转发结算数据到逻辑服务器
    • 理论上,上面描述的方式是可行的
    • 如果战斗服务器的算力很强大,延迟只考虑网络的情况,给玩家选择战斗服务器的时候需要就近选择(ping值较优的那台)
    • 玩家登录后获取所有(尽可能多的)战斗服务器的列表,并做ICMP的网络探测并上报数据给到匹配服务器(匹配战斗服务器用的一组服务器Matchmaking)
    • 匹配服务器基于一定的规则,尽量把玩家分配到网络较优的战斗服务器上

帧同步的数据校验

  • 帧同步的游戏,这里一般是在战中有玩家操作的游戏(如果只是战前策略的游戏,用服务器比客户端帧数快的方式先跑完战斗结果再同步给客户端播放更合适)
  • 服务端在游戏里更多是做消息转发(广播)的内容,伤害等都在客户端做,如何避免客户端作弊呢?
  • 分两种最常见的帧同步游戏讨论
  • #1. 1V1的
    • 假设客户端是A、B,A计算自己的战场数据伤害等,也模拟计算B的战场数据(不然A看到B的更新会比B自己看到的慢至少1帧)
    • 客户端需要同步的消息分两种:玩家操作类的和当前战场快照
    • A把自己的战场快照同步给B,B根据自己的模拟数据需要做一次战场同步
    • 如果让B在这个过程做一个快照间的差值计算
    • 这里存在一个所有权的问题:
      • 如果把A作为完全的控制方,那相当于把A的机器当作传统CS架构的S端,一切以A为准,B的一些操作内容会先发到A执行具体结果,再回传给B做结果播放;这样子逻辑看起来很清楚,结构也简单,但是做为B会感受到延迟带来的不好的体验(操作手感和表现延迟)
      • 如果AB各控制一部分内容,比如主动操作的执行结果,而其他比如场景内的内容由A控制;那AB在客户端感受上都可以得到一个较好的手感和表现,但是控制权的问题也稍复杂点
  • #2. 断线重连的问题

(待续….)

https://gist.github.com/jboner/2841832
https://colin-scott.github.io/personal_website/research/interactive_latency.html

------ 本文结束 ------
------ 版权声明:转载请注明出处 ------