如何设计一个性能可扩展的MMO(大型多人在线)游戏 分布式 系统是一件富有挑战性的任务,需要能够灵活有效地扩展分配计算资源,包括千万玩家在一个共享虚拟世界中彼此交互,实现身临其境的体验。
游戏目标:
1.游戏同时会有很多在线玩家。
2.游戏侧重探寻 可玩很多游戏 玩家之间频繁互动。
3.游戏应该能够立即决定在某个指定地图上玩家的数量。
4.游戏设计可以指定使用短暂的地图,在小团体的球员中选择共享脚本经验。
5.游戏服务器使用 分布式 部署架构。
6.游戏服务器使用 分布式 网络连接模式来管理游戏客户端的连接。
7.支持横向扩展,通过增加服务器个数等资源方式提升服务器的负载能力。
8.有一个长期的产品计划:将不断向游戏中增加内容和扩大整个虚拟世界的规模大小。架构设计必须支持这种业务规模的扩展。
9.业务和运营计划将寻求不断扩充容量,通过增加服务器的方式,而不是使用更强的服务器替换旧的正在运行的服务器。
为了实现以上设计目标,需要采取面向职责的游戏服务器策略,根据功能职责划分游戏为一个个子集类型。这种将职责分配到服务器类型的方式类似于你设计类或其他软件模块单元的方式,但是有一个更高的抽象级别,包括:
1.将高层次的游戏活动进行功能降解( functional decomposition ),这样以便发现关键操作。
2.为典型的游戏场景编写用例以发现主要的业务行为。
3.按照操作类型将行为归组,包含行为和数据(这是一个对象),这些组其实是一种候选人职责,确认一个服务器类型拥有各自的候选人职责。
4.确认每个服务器类型执行的所有功能都应该不超过它的单一高级职责。
5.避免在一个用例中重复在服务器类型之间进行后退前进(back-and-forth)的交互。
6.避免出现跨不同服务器类型的长请求,也就是避免同步通信。
7.每个操作被看成异步的独立的事件。
8.如果可能,支持每个服务器类型有多个进程实例,限制单例,同时也避免多例带来的复杂性和消耗。
9.不断迭代直至这些服务器类型设计划分比较稳定了,能够应付需求的变化和未来新技术的融合。
从以上几个设计要点,我们需要将这个大型多人在线游戏系统划分为几种服务器类型,这几个服务器类型类似面向对象中类设计,是一种类别组,代表一组功能的聚合,这种类型划分方法就像将人划分为男人和女人两种类型一样。
下面是几个用来举例的游戏类型,不同具体游戏可能不同,但是可以借鉴:
1. Gameplay类型,执行所有核心游戏操作,包括游戏系统的逻辑和游戏效果,其状态包含人物状态,点数,游戏影响,经验值等等。
2.AI类型,执行所有人工智能AI的思考,主要控制NPC也就是非玩家角色的行为,比如您在买卖物品的时候需要点击的那个商人就是NPC,还有做任务时需要对话的人物等等都属于NPC,NPC行为包括路径查找 决策决定和预测等等,其状态是一个有限状态机,决策树和目标列表。
3.Visibility显示类型,使用空间几何和空间分区数据来计算在当前游戏场景虚拟世界中应该显示哪些东东。其状态包含定位位置,视野范围,障碍物和地图几何结构。
4.物理类型,使用空间几何和静态碰撞数据来侦测移动物体将和静物的碰撞,也包括模拟不受控制的移动,比如掉入等等,强调玩家移动时遵循游戏的规则。其状态包括位置,碰撞物 空间几何和路径发现可选地图。
5.目录类型,这个服务器类型维持一个集群中游戏定位信息,对于一个指定的游戏对象,能够指定相应的服务器类型中哪个服务器实例来运行这个游戏对象。
游戏服务器类型之间的交互如下图,客户端通过连接服务器接入,连接服务器和游戏服务器类型之间通过事件消息异步通讯。
游戏对象与状态管理
前面设计了游戏的服务器类型,将一个大游戏划分成几个模块,现在,我们要决定不同的游戏服务器类型的实例中运行的游戏是什么?包括该游戏当前状态等管理。
将游戏状态封装到一个逻辑抽象中是一个有用的设计方式,这个抽象称为游戏对象game object,也就是代表游戏中所有有意义的实体,包括玩家角色 NPC 条目 互动世界对象等等。
这个抽象对象的关键是ID标识,ID必须在整个游戏系统中唯一代表游戏对象实例,需要有足够的值空间,一般使用UUID或GUID或128位整数类型。
不同服务器类型使用不同的游戏状态类型,这样,一个指定的游戏对象类型也许会在不同的服务器类型中有不同的实现,一个处理物理模拟的服务器类型也许有和位置相关的行为和数据等等。一个处理玩家角色优化的服务器类型也许更注重可穿戴的东西,虚拟增强和其他属性。
这是一种有态的stateful的设计模式,游戏将状态保留在内存中以便重复使用直至其不再需要,这和我们通常将状态保存在关系数据库的系统设计是有区别的,每个游戏对象实例在某个时间只存在一个服务器类型的具体实例服务器中,这样降低对象创建的开销,有助于数据状态的一致性,游戏对象禁止在服务器实例之间复制移动,这使得定位某个游戏实例更加具有确定性,避免多线程资源竞争的发生。
游戏对象的定位
游戏对象之间通过异步事件交互,这样通常能实现高 并发 ,这能确保游戏对象无论是在同一个进程服务器中或不同服务器进程里都能保持交互的一致性,当对象在不同服务器进程中交互时,这些通过消息方式分发到对方的游戏对象中(目标对象)。
游戏必须提供在服务器集群中定位这些目标游戏对象,然后将事件发给它们,实现细节可能不同,但是必须考虑下面情况:
1. 在游戏服务器代码中使用逻辑地址定位目标游戏对象,这个逻辑地址应该包括目标游戏对象的ID,但是不应该包括任何物理主机地址或物理路由信息,可以包含有关目标游戏对象所在的服务器类型信息,比如逻辑地址的结构可以是: <server_type>::<object_id>,但是不推荐: <host_address>:<port>::<object_id>
2.定义一个服务器类型(目录类型)作为游戏对象实例的注册器,这个服务器应该可以将一个游戏对象的逻辑地址映射到其真正的物理地址,也就是说,它的职责是根据事件中包含的目标逻辑地址寻找相应的服务器类型,然后在这些服务器类型中分配符合对应ID的目标游戏对象接受这个事件。
3. 使用一致性哈希或 分布式 哈希表(DHT)分配游戏对象到某个服务器实例进程中,一致性哈希容易实现,快速而确定,它适合热点数据,但是不允许在运行时重新分配ID,而动态哈希表更灵活也更复杂,运行在运行时重新定位游戏对象,支持更健壮的失败恢复。
4. 可以考虑 缓存 游戏对象的地址在本地内存中,以减少访问目录服务器的来回损耗时间,但是游戏对象一旦被创建就不能在服务器进程之间移动。
扩展性
以上介绍的这种游戏服务器设计模式的好处是支持有效的扩展游戏集群规模,也就是水平扩展而不是垂直扩展,这种方式支持只要增加新的服务器实例或更多服务器类型,就能增加系统的处理容量和抗高负载能力,增加新的负载能力就能满足新的在线游戏玩家数量的增加。
每个服务器类型的服务器进程个数是独立变化的,这就能让你有针对性地分配计算资源给不同的服务器类型,所以,关键是设计游戏之初要根据职责区分服务器类型。
扩展集群规模有两种方式:静态集群成员和动态集群成员,前者是手工增加或去除集群服务器实例成员,后者是基于负载测量策略来动态增加服务器。
静态集群成员是一种比较容易理解和实现的方式,需要维持一份集群成员的清单,需要将这些清单分发到每台服务器中,用清单来控制每个节点运行哪些服务器。其缺点是重新分配服务器类型实例时需要整个系统停机更新,而且不是失败容错的,更新一个节点清单意味着需要更新所有服务器中的清单备份。
动态集群成员提供更灵活有效的方式应对负载,但是复杂且需要特殊逻辑,这种系统首先要监控集群的性能和健康情况,根据需要增加或移除服务器实例,这样的系统必须掌握集群的不同服务器类型,它们的性能要求特点和处理需求。
一种简单的办法是采取中央集权服务器和数据存储,但是这会带来单点风险,现在流行的是使用gossip协议或基于共识 consensus-based 协议,比如Raft协议等等。
当使用这些模式必须考虑以下几点:
1. 首先起步通过手工方式实现,这样你可以了解系统的很多细节,如果决定走向动态解决方案,能够知道该做什么。
2.如果使用手工配置,意味着自动化构建和部署,高效率的自动化工具就很重要,能够降低集群因为重新配置耽误的时间。
3.无论采取手工或自动,可以采取Actor实现游戏服务器,首先,游戏对象这个抽象非常适合Actor模型,其次,Actor能处理复杂的 并发 ,最后,第三方Actor框架都能支持动态集群成员策略,比如:Microsoft的 Project Orleans 4)(.Net) , Akka (Java/Scala) 和 Akka.NET
4.如果你希望走上自动化路途,考虑云平台,云主机能够根据需要自动提供资源。
持久化
玩家的状态需要持久化保存到数据存储中,这种方式需要避免采取特殊的数据库架构和技术,可以按照下面考虑设计持久化系统:
1.持久化解决方案应该对频繁写+不频繁读进行过优化。玩家会产生非常频繁的状态改变,所有这些必须保存以保证数据一致性,因为游戏对象不会在服务器进程之间移动,玩家状态需要存储在其会话中。
2.持久化系统必须支持 分布式 操作,可以扩展。
3.一个write-back写回的内存in-memory缓存能够保住优化写操作,降低存储系统的负载,但是会有数据丢失的风险,这种策略在存储系统也就是数据库系统不支持扩展时比较有效。
以上是一个大型多人在线MMO游戏的 分布式 解决设计,能够将游戏在多个服务器进程之间加载运行,同时支持成千上万个连接客户端,这种游戏系统通过水平扩展就能提高玩家数量和额外的功能,分配游戏职责到不同的服务器类型是设计关键,能够让游戏的不同部分独立扩展伸缩,这种架构支持手工会自动的扩展技术。
总结主要概念如下:
1. 不同服务器类型有不同的玩游戏的职责。
2. 游戏对象某个时间只存在单个服务器类型实例中。
3.在集群中需要提供一个功能来查找游戏对象。
4. 游戏对象之间同归异步事件交互
原文参考: