Tacticool移动在线射击者元服务器体系结构

Pixonic DevGAMM会谈的另一个话题-这次是PanzerDog的同事们带来的。 Pavel Platto公司的首席软件工程师使用面向服务的体系结构拆解了游戏的元服务器,并告诉他们选择了哪些解决方案和技术,如何扩展以及如何扩展以及面临哪些困难。 报告文本,幻灯片以及指向mitap的其他演讲的链接,一如既往地在切口下。


首先,我想为我们的游戏演示一个小的预告片:


该报告将包括3部分。 在第一篇中,我将讨论我们选择了哪些技术以及为什么选择它;在第二篇中,我将讨论我们的元服务器的布置方式;在第三篇中,我将讨论我们使用的各种支持基础架构以及如何在不停机的情况下实现更新。


技术栈

元服务器托管在Amazon上,并用Elixir编写。 它是一种具有编程参与者模型的功能性编程语言。 由于我们没有操作人员,因此程序员需要参与操作,并且大多数基础结构都使用HashiCorp Terraform描述为代码。

Tacticool目前处于公开测试阶段,该元服务器已经开发了一年多,并且已经运行了将近一年。 让我们看看这一切是如何开始的。



当我加入公司时,我们已经在C / C ++混合和PostageSQL存储上实现了作为整体的基本功能。 此实现存在某些问题。

首先,由于C的级别较低,因此存在许多难以捉摸的错误。 例如,对于某些播放器,由于数组在重用之前不正确地归零,所以匹配配对挂起。 当然,很难找到这两个事件之间的关系。 而且,由于在代码中普遍修改了多个线程的状态,因此竞态条件并非没有。

并行处理大量任务也是不可能的,因为服务器是在大约10个工作进程启动时启动的,这些工作进程被对Amazon或数据库的查询所阻止。 即使我们忘记了这些阻塞请求,该服务也会因为数百个除了ping之外没有执行任何操作的连接而崩溃。 此外,该服务无法水平扩展。

在花费了几周的时间来查找和修复最关键的错误之后,我们决定从头开始重写所有内容比尝试解决当前解决方案的所有缺点要容易得多。

当您从头开始时,尝试选择一种有助于避免上述某些问题的语言是很有意义的。 我们有三个候选人:

  • C#;
  • 去吧
  • 长生不老药。



C#在“熟人”列表中,因为 客户端和游戏服务器是用Unity编写的,团队中的大多数经验都是使用这种编程语言。 之所以考虑使用Go和Elixir,是因为它们是为开发服务器应用程序而创建的现代且相当流行的语言。

先前迭代的问题帮助我们确定了评估候选人的标准。

第一个标准是使用异步操作的便利性。 在C#中,首次尝试没有出现使用异步操作的便捷工作。 这导致了这样一个事实,即我认为解决方案是“笨拙的”,在我看来,这种解决方案仍然有些偏颇。 在Go和Elixir中,设计这些语言时已考虑到此问题,它们都使用轻量级线程(在Go中,它们是goroutine,在Elixir中,它们是进程)。 这些流的开销比系统线程小得多,并且由于我们可以成千上万个创建它们,因此我们很抱歉阻止它们。

第二个标准是具有竞争流程能力。 开箱即用的C#除了线程池和共享内存外不提供其他任何功能,必须使用各种同步原语来保护对它们的访问。 Go以goroutine和通道的形式提供了一个不太容易出错的模型。 另一方面,Elixir提供了一种参与者模型,而无需通过消息共享内存。 共享内存的缺乏使得有可能在运行时实施对竞争性执行环境有用的技术,例如诚实的外卖多任务处理和垃圾回收,而不会中断世界。

第三个标准是使用不可变数据类型的工具的可用性。 我所有的开发经验都表明,相当多的错误与错误的数据更改有关。 一个解决方案很久以前就存在-不可变的数据类型。 在C#中,可以创建这些类型的数据,但要花费大量的样板。 在Go中,这是完全不可能的。 在Elixir中,所有数据类型都是不可变的。

最后一个标准是专家人数。 在这里结果很明显。 最后,我们选择了Elixir。

通过选择托管,一切都变得更加简单。 我们已经在Amazon GameLift中托管了游戏服务器,此外,Amazon提供了大量的服务,这些服务将使我们减少开发时间。



我们完全投降到云中,并且自己没有部署任何第三方解决方案-数据库,消息队列-所有这些都由Amazon为我们管理。 在我看来,这是小型团队想要开发在线游戏的唯一解决方案,而不是其基础架构。

我们确定了技术的选择,让我们继续介绍元服务器的工作方式。



通常:客户端通过Web套接字连接连接到Amazon的负载均衡器; 平衡器将这些连接分散在多个前端实例之间,前端将客户端请求发送到后端。 但是前端和后端通过消息队列间接通信。 每种类型的消息都有一个单独的队列,前端根据消息的类型来确定将消息写入何处,而后端则侦听这些队列。

为了使后端可以将对请求的响应发送到客户端或某种事件,每个前端都有一个单独的队列(专门为其分配队列)。 并且在每个请求中,后端都会收到一个前端标识符,以确定应将响应写入哪个队列。 如果他需要发送事件,他将调用数据库以查找客户端连接到哪个前端实例。

使用一般方案,让我们继续讲细节。



首先,我将讨论客户端-服务器交互的一些功能。 我们使用二进制协议是因为它非常有效并且可以节省流量。 其次,对于使用该帐户进行更改的任何操作,服务器不会将这些更改发送给客户端,而是发送给该帐户的完整(更新)版本。 这虽然效率略低,但无论如何都不会占用太多空间,并且极大地简化了我们在客户端和服务器上的生活。 而且,前端可确保客户端一次仅执行一个请求。 这使您可以捕获客户端上的错误,例如,当玩家在播放器看到先前操作的结果之前切换到另一个屏幕时。

现在介绍一下前端的布置方式。



前端本质上是一个侦听Web套接字连接的Web服务器。 对于每个会话,创建两个过程。 第一个过程服务于Web套接字连接本身,第二个过程是描述客户端当前状态的状态机。 基于此状态,它确定来自客户端的请求的有效性。 例如,在授权完成之前,几乎所有请求都无法完成。 由于除了这些会话外,前端上没有任何状态,因此添加新的前端实例非常容易,但是删除旧的实例会有些困难。 卸载之前,您需要让所有客户端完成其当前请求,并要求它们重新连接到另一个实例。

现在了解后端的外观。 目前,它包含五个服务。



第一个处理与帐户相关的所有事项-从购买游戏内货币到完成任务。 第二个则处理与比赛相关的所有事情-它直接与GameLift和游戏服务器进行交互。 第三种服务是用真钱购物。 第四和第五负责社交互动-一个负责朋友,另一个负责聚会游戏。

从体系结构的角度来看,每个后端服务看起来都是完全相同的。 它们是一组管道,每个管道处理一种类型的消息。 管道包含两个元素:生产者和消费者。



生产者的唯一任务是从队列中读取消息。 因此,它是以完全通用的形式实现的,对于每个管道,我们只需要指出那里有多少个生产者,从哪个队列读取数据以及每个生产者将服务多少个消费者。 另一方面,消费者是为每个管道单独实现的,并且是具有唯一强制功能的模块,该功能接受一条消息,执行所有必要的工作并返回需要发送给客户端或游戏服务器的其他服务的消息列表。 生产者还实施背压,以使消息数量急剧增加而不会造成过载,并且要求消息的数量不超过拥有免费消费者的数量。

后端服务不包含任何状态,因此我们可以轻松添加和删除旧实例。 删除前唯一要做的就是要求生产者停止阅读新消息,并给消费者一点时间来完成对活动消息的处理。

与GameLift的互动如何发生? GameLift由几个组件组成。 在我们使用的游戏机中,这是一个FlexMatch媒人,一个放置队列,用于确定在哪个特定区域托管与这些玩家的游戏会话,以及由游戏服务器组成的舰队本身。



这种互动进行得如何? Meta仅与媒人直接通信,向他发送请求以查找媒人。 并且他通过相同的消息队列将配对过程中的所有事件通知给meta。 并且,一旦他找到合适的一组球员开始比赛,他就向放置队列发送请求,该队列依次为他们选择服务器。

meta与游戏服务器的交互非常简单。 游戏服务器需要有关帐户,漫游器和地图的信息,并且元数据会在一条消息中将所有这些信息发送到专门为此比赛而创建的队列。



游戏服务器在激活后便开始侦听此队列并接收其所需的所有数据。 在比赛结束时,他将结果发送到该meta正在侦听的常规队列。

现在,让我们继续使用我们使用的其他基础结构。



部署服务非常简单。 它们都在Docker容器中工作,我们使用Amazon ECS进行编排。 它比Kubernetes简单得多,当然还不那么复杂,但是它执行了我们需要的任务。 即:当我们需要填写某种错误修正时,扩展服务并发布版本。

我们最后使用的最后一项服务是AWS Fargate。 它使我们不必独立管理运行Docker容器的机器集群。



作为主要存储,我们使用DynamoDB。 首先,我们选择它是因为它非常易于操作和扩展。 我们还通过Amazon ElasiCache托管服务将Redis用作附加存储。 在需要立即将数百个游戏帐户中的数据返回给客户端的情况下(例如,在同一评分表中或在好友列表中),我们将其用于全局玩家评分任务并缓存基本帐户信息。

用于存储配置,元游戏机制,武器,英雄等的描述。 我们使用一个JSON文件,该文件附加到需要它的服务的图像上。 因为对于我们来说,使用更新的数据推出新版本的服务(如果发现了一些错误)要比做出在运行时从某个外部存储动态更新此数据的决定要容易得多。

对于日志记录和监视,我们使用了很多服务。



让我们从CloudWatch开始。 这是一项监视服务,所有Amazon服务的指标都在其中聚集。 因此,我们决定也从我们的元服务器发送指标。 对于日志记录,我们在客户端,游戏服务器和元服务器上都使用一种通用方法。 我们将所有日志发送到亚马逊服务Kinesis Firehose,后者又将其传输到Elasticseach和S3。

在Elasticseach中,我们仅存储相对较新的数据,并在Kibana的帮助下查找错误,解决游戏分析的某些任务并构建可操作的仪表板,例如,使用CCU时间表和新安装的数量。 S3包含所有历史数据,我们通过Athena服务使用它,该服务在S3中的数据之上提供了SQL接口。

现在介绍一下我们如何使用Terraform。



Terraform是一种工具,它允许您以声明方式描述基础结构,并且如果描述中有任何更改,它会自动确定为使基础结构具有更新外观而需要采取的措施。 因此,仅需一个描述,我们便获得了几乎相同的登台和生产环境。 而且,这些环境是完全隔离的,因为它们部署在不同的帐户下。 Terraform对我们而言唯一的重大缺点是GameLift的不完全支持。

我还将讨论如何在不停机的情况下实施更新。



发布更新时,我们会提出大多数资源的副本:服务,消息队列,数据库中的一些标签。 下载新版本游戏的玩家将连接到此更新的群集。 但是那些尚未更新的玩家可以继续使用旧版本的游戏玩一段时间,并连接到旧群集。

我们如何实现它。 首先,在Terraform中使用模块引擎。 我们分配了一个模块,在其中描述了所有版本化的资源。 这些模块可以使用不同的参数多次导入。 因此,对于每个版本,我们都将导入此模块,以指示该版本的编号。 DynamoDB中缺少一种方案也为我们提供了帮助,这使得可以在更新期间(而不是在更新期间)执行数据迁移,但可以将其推迟到每个帐户,直到其所有者登录到游戏的新版本为止。 在平衡器中,我们只需为规则的每个版本进行指示,以便它知道将不同版本的玩家路由到何处。

最后,我们学到了一些东西。 首先,整个基础架构的配置必须自动化。 即 我们用双手设置了一些东西,但是迟早我们在设置中犯了一个错误,因为这会导致fakaps。



最后一件事-您需要为基础架构的每个元素都拥有一个副本或备份副本。 而且,如果您不为某件事做这件事,那么这件事将使我们失望。

听众的提问


-但是,由于某种错误,自动缩放比例可能会过于高涨,您会因此而烦恼,您会得到很多钱吗?

-对于自动缩放,仍然设置限制。 我们不会设置太大的限制,以免损失很多钱。 这是主要的解决方案+监控。 如果太强,可以设置警报。

-您目前的限制是多少? 相对于当前基础结构的百分比。

-现在我们已经在11个国家/地区进行了公开Beta测试,因此以某种方式评估它并不是一个太大的CCU。 现在,对于我们拥有的人数而言,基础设施的配置太高了。

-还没有限制吗?

-是的,只是它们比我们的CCU高10-100倍。 不要少做。

-您说您的前端和后端之间有线-这很不寻常。 为什么不直接呢?

-我们希望无状态服务能够轻松实现备份机制,以便该服务所请求的消息不会超过其具有免费处理程序的消息。 同样,例如,当一个处理程序失败时,队列会将相同的消息提供给另一个处理程序-也许它将成功。

-队列是否以某种方式持续存在?

-是的 这是一项Amazonian SQS服务。

-关于队列:游戏期间创建了多少个频道? 每次比赛您都有一定数量的频道吗?

-它产生的相对较少。 大多数队列(例如请求队列)是静态的。 有一个授权请求队列,有一个比赛开始队列。 在动态创建的队列中,每个前端只有一个队列(它在启动时为客户端的传入消息创建队列),对于每个匹配,我们都创建一个队列。 在这项服务中,它几乎不花任何钱,他们以相同的方式收取任何请求。 即 对SQS的任何请求(创建队列,从中读取内容)的成本都相同,与此同时,我们不会删除这些队列以进行保存,它们将在以后被删除。 它们的存在并不会使我们付出任何代价。

-在这种架构中,这对您来说不是一个限制吗?

-不

通过Pixonic DevGAMM进行更多对话


Source: https://habr.com/ru/post/zh-CN426875/


All Articles