利用 Golang 进行游戏开发和运营

利用 Golang 进行游戏开发和运营

大家好,我叫 Aaron Torres,是 Riot 开发者体验团队的工程经理。我们加速 Riot 的游戏团队如何在全球范围内大规模开发、部署和运营他们的后端微服务。我在公司工作了 3 年多一点,并且一直在编写 Go 代码。在加入 Riot 之前,我写了一关于Go的书,在 Riot 期间我将它更新到了第二版。在学习 Go 的同时,我还大量参与了将单体应用程序大规模迁移到微服务的工作,并且我还广泛使用 Docker 容器、容器编排层(如 Kubernetes)以及协调全球服务群部署所涉及的一切。 

我在 Riot 管理两个名为 Service Lifecycle 和 Cloud Services Integration 的团队,它们都隶属于 Riot Developer Experience 或 RDX 团队。RDX 管理服务/应用程序和我们的基础设施之间的软件层。Riot 的计算基础设施历来由裸机数据中心和云提供商组成。这些服务中的大部分,包括 Docker 等第三方软件,都是用 Go 语言编写的。

我的团队拥有多项服务,包括我们的全球部署工具和位于我们软件定义网络之上的访问控制管理层,该网络管理服务之间的所有网络连接。此外,我们拥有一项云基础设施供应商服务,负责启动和管理 AWS 资源以供服务所有者使用。

在选择软件和语言时,我们鼓励 Riot 的技术团队为其产品寻找最佳选择。在本文中,我们将专门研究几个不同的团队如何使用 Go。我将邀请两位技术专家——RDX Operability 的 Chad Wyszynski 和 VALORANT 的 Justin O'Brien——讨论他们如何在项目中使用 Go。
.

Go 中的上下文

在 Riot,我们用于服务的两种主要语言是 Java 和 Go。因此,就支持而言,这两种语言都被视为一等公民——而且因为我们使用容器进行部署,所以它们都是可互操作的,并且相对容易打包和部署。我们喜欢 Riot 的 Go 有很多原因,包括:

  • Go 应用程序的分发就像下载和运行二进制文件一样简单。这对于构建 CLI 工具非常有用。

  • Go 是一种相对简单的语言,具有较小的语言规范,这使得可能来自其他语言背景的新工程师很容易入职。

  • Go 代码构建速度非常快,以至于您的编辑器可以在保存时重新构建以检查错误。

  • Go 有一个庞大而强大的标准库,包括一个生产级网络服务器。

  • Go 以 goroutine 和通道的形式具有强大的并行化和并发原语。 

  • Go 对最佳实践固执己见, gofmt 命令使每个人的代码看起来都差不多。

  • Go 很少破坏向后兼容性,当它确实破坏时,它通常是以库的形式而不是语言本身(用于依赖管理的模块)的形式。

  • Go 相对流行,这意味着有出色的第三方支持,并且供应商会经常包含用于开发的 Go 客户端库。

最近科技行业也出现了围绕 Go 的运动,特别是在微服务方面,它有助于利用这种兴趣并推动开发人员空间。它在etcdDockerKubernetesPrometheus等软件的系统空间中也变得越来越流行有用于结构化日志记录共识算法websockets的优秀库。此外,标准库包括TLSSQL支持等内容,因此您可以在 Go 中快速高效地工作。

用例:防暴部署工具

Service Lifecycle 团队的主要项目是我们的部署工具,它用于部署和管理在我们的 Docker 运行时中运行的服务的生命周期。如果您阅读了我们之前的“运行在线服务”系列,您将更好地了解我们正在处理的问题空间。我们的部署工具是用 Go 编写的,因为它使我们能够快速推出更新,并招募新工程师到我们的技术堆栈,并快速从早期开发迭代到生产。它由 MySQL 提供支持,单个实例可以针对多个数据中心位置。Go 让我们更容易解决许多挑战,包括:

  • JSON / YAML 支持

  • HTTP 客户端

  • 网络连接

  • API 集成

JSON / YAML 支持

我们的部署工具在自定义 YAML 规范上运行,该规范描述了应用程序需要运行的内容。有几个第三方 Go 库为我们实现了 JSONSchemaGo 还为将 Go 结构编组和解组为 JSON 提供本机支持,并为 YAML 提供第三方支持。

部署工具可能使用的结构化 YAML 模式。

HTTP 客户端

我们的工具与许多其他微服务连接,用于服务发现、日志记录、警报、配置管理、供应数据库等。主要的通信方法是 HTTP 请求。这意味着我们经常需要考虑诸如请求的生命周期、互联网信号、超时等问题。幸运的是,Go 提供了一个非常可靠的HTTP 客户端,其中包含一些您肯定想要调整的默认值。例如,默认情况下客户端永远不会超时。

执行 HTTP 请求并打印响应的正文。

网络连接

通常可以通过额外的安全层来隔离数据中心,尤其是在与合作伙伴区域合作时。我们用于多个项目的 Go 的一个非常有用的方面是 Go httputil 反向代理这使我们能够快速代理请求,为请求的生命周期添加中间件以注入额外的身份验证或标头,并使一切对客户端相对透明。

API 库

在 Riot,我们必须与各种第三方服务交互,包括 Hashicorp VaultDCOSAWSKubernetes这些解决方案中的大多数都提供了供 Go 应用程序使用的原生 API 客户端库。有时我们也会根据需要使用或分叉第三方库。在所有情况下,我们都能为我们的需求找到足够的支持。

此外,在开发过程中,我们很容易重新编译和运行我们的部署工具的本地版本,以进行快速测试或调试它还使我们能够轻松地与我们空间中的其他团队共享代码和库。

现在我们已经了解了我的团队如何使用 Go 进行部署,让我们看看另外两个示例。

用例:运行监控

大家好,我是 RDX 可操作性团队的 Chad Wyszynski,我想向您展示我的团队如何使用 Go 来最大程度地减少我们的操作监控管道中的请求延迟。Riot 的大部分日志和指标都流经我团队的监控服务。当出现问题时,它是一个持续的高流量峰值,因此服务必须保持高吞吐量和低延迟。谁愿意等待几秒钟来记录错误?Go 频道帮助我们满足这些要求。

运营监控服务存在一个目的:将日志和指标转发到后端可观察性平台,例如 New Relic。该服务首先将请求数据转换为后端平台期望的格式,然后将转换后的数据转发到该平台。这两个步骤都很耗时。该服务不会强制客户端等待,而是将请求数据放入有界通道中,以供另一个 Goroutine 处理。这允许服务几乎立即响应客户端。

但是当有界通道已满时会发生什么?默认情况下,Goroutine 将阻塞,直到通道可以接受数据。我们使用 Go 的 time.After 来绑定这个等待。如果通道在超时之前不能接受请求数据,则服务 503's. 客户端可以稍后重试请求,希望在一些指数退避之后。

当从一个可观察性后端迁移到另一个时,基于通道的设计真正获胜。Riot 最近将所有指标和日志从手动管道移至 New Relic当团队在新平台上配置仪表板和警报时,运营监控服务必须将数据转发到两个后端。多亏了 Go 通道,双重发送基本上不会增加​​客户端请求的延迟。我们的服务只是将请求数据添加到另一个有界通道。那么,最大服务器响应时间是基于 Goroutine 等待将数据放入目标通道的时间,而不是目标服务器响应的时间。

当我加入 Riot 时,我是 Go 新手,所以我很高兴看到通道和 Goroutines 的实际用例。我的同事 Ayse Gokmen 设计了最初的工作流程;我很高兴能分享我们的工作。

用例:VALORANT

Justin O'Brien 来自 Valorant 的竞争团队!我的团队将 Go 用于我们所有的后端服务——就像 Valorant 上的所有功能团队一样。我们整个后端微服务架构都是使用 Golang 构建的。这意味着从启动和管理游戏服务器进程到购买物品的所有事情都是使用 Go 编写的服务完成的。尽管将 Golang 用于我们所有的服务有很多好处,但我将讨论三个特定的语言特性:并发原语、隐式接口和包模块化。

并发原语

我们利用 Golang 并发原语来在操作开始变慢时增加背压,并行化独立操作,并在我们的应用程序中运行后台进程。这方面的一个例子是,我们经常发现自己在比赛的执行链中,但需要为每个玩家做一些事情,例如在开始比赛时为每个玩家加载皮肤数据。我们对共享函数的要求是在所有子例程执行完毕后返回并返回发生的任何错误的列表。

func Execute(funcList []func() error) []error

我们通过使用两个通道和一个等待组来实现这一点。一个通道是在每个thunk执行时捕获错误,而另一个是一个完成的通道,Goroutine 在等待组完成时发送该通道。语言特性使这种非常常见的模式易于实现。

隐式接口

我们广泛使用的另一个语言特性是隐式接口。我们大量利用它们来测试我们的代码并作为创建模块化代码的工具。例如,我们很早就计划在我们的所有服务中都有一个通用的数据存储接口。这是我们的每个服务用来与数据源交互的接口。

这个简单的界面允许我们实现许多不同的后端,以完成不同的事情。我们通常在我们的大多数测试中使用内存中的实现,并且小接口使得在测试文件中实现内联以用于访问计数或测试我们的错误处理等独特情况非常轻量级。我们还将 SQL 和 Redis 混合用于我们的服务,并使用此接口实现两者。这使得将数据存储附加到新服务变得特别容易,并且还可以添加更多特定情况,例如由 redis 支持的内存缓存中的直写,也非常有可能。

封装模块化

最后,我想指出的不一定是语言特性的一点是可用的第三方包的广泛选择,这些包通常可以与常见的内置包互换使用。由于 golang 包的模块化特性,这帮助我们进行了更改,我认为这些更改会是一个非常小的重构。例如,我们的一些服务花费了大量的 CPU 周期来序列化和反序列化 JSON。我们使用了 Golang 开箱即用的json 包当第一次编写我们所有的服务时。这适用于 95% 的用例,通常 JSON 序列化不会显示在火焰图上(现在我认为 golang 的内置分析工具也是一流的)。在某些情况下,特别是在序列化大型对象时,服务的大量时间都花在了 json 序列化程序中。我们着手进行优化,结果发现有许多替代的第三方 JSON 包与内置包兼容。这使得更改就像更改此行一样简单:

import "json"

到 :

import "github.com/custom-json-library/json"

之后,对 JSON 库的任何调用都使用了第三方库,这使得分析和测试不同的包变得容易。

Gopher 实践社区

亚伦又回来了!现在我们已经了解了 Riot 中的一些 Golang 用例,我想向您展示我们是如何相互联系的。团队在选择技术堆栈时的灵活性依赖于 Rioter 技术人员的协作环境。 

Riot Games 是一家非常社会化的公司,我们的技术部门鼓励 Rioter参与学习和发展社区。例如,我们的各种实践社区使具有共同兴趣的 Rioter 团体能够定期聚集在一起学习和分享。最活跃的技术社区之一是我目前运营的 Go 社区。有一个 Slack 频道来讨论新提案,我们每月举行一次聚会,成员们可以展示他们知道或了解的主题,或者用 Go 编写的 Riot 项目。 

我们还渴望让 Riot 之外的社区参与到开源库维护者的讨论中。CoP 也是协调影响多个团队的更改的地方,例如在模块镜像启动时围绕安全性进行的讨论。也有关于碰撞构建容器的讨论,处理我们可能遇到的问题,或者询问有关方法、工具或库的一般性问题,以在组织的另一部分寻求个人专业知识。

我个人喜欢有一个由跨团队和跨学科的 Go 爱好者组成的频道,以交流想法、讨论语言变化并分享我们遇到的库。当我们从旧的依赖解决方案过渡到 Go 模块时,这个频道是讨论的中心点,这是结识对该语言充满热情的工程师的好方法。 

Go CoP 的传单。

结束语

在 Riot,许多团队维护着用 Go 语言编写的服务和工具。Go 提供了强大的标准库和强大的第三方社区支持来帮助满足我们的开发需求。 

我们的实践社区是开发人员在 Riot 为 Go 使用做出贡献并分享他们的学习和经验的好方法。我们对 Riot 的 Go 未来感到兴奋,因为它能够在整个公司内保持灵活和高度沟通。

谢谢阅读!随时在下面发布任何问题或评论。

由 Aaron Torres 与 Chad Wyszynski 和 Justin O'Brien 发布