欢迎来到Tungsten Fabric用户案例系列文章,一起发现TF的更多应用场景。“揭秘LOL”系列的主人公是Tungsten Fabric用户Riot Games游戏公司,作为LOL《英雄联盟》的开发和运营商,Riot Games面临全球范围复杂部署的挑战,让我们一起揭秘LOL背后的“英雄们”,看他们是如何运行在线服务的吧。
作者:Kyle Allan和Carl Quinn(文章来源:Riot Games)译者:TF中文社区

我们是Kyle Allan和Carl Quinn,在Riot的基础架构团队工作。欢迎阅读这个系列的第二篇文章,详细介绍我们如何在全球范围内部署和操作后端功能。在本文中,我们将深入探讨部署生态系统的第一个核心组件:容器调度。

在Jonathan的第一篇系列文章中,讨论了Riot的部署历史和我们面临的挑战。特别是,他概述了当我们为《英雄联盟》不断添加基础架构设施时,尤其是面对“为每个应用程序手动配置服务器”这样的场景下,我们软件部署难度不断加剧。后来,出现了一个名为Docker的工具,改变了我们的服务器部署方法——进一步在我们内部就迭代出来Admiral,它是我们用于集群调度和管理的内部工具。

重要的是,应用程序部署的旅程还远远没有结束,它还在不断发展,我们正在为下一个阶段做准备(可能采用DC/OS,稍后讨论)。本文介绍了如何到达这一步,以及为什么做出这样的决定,希望其他人也可以从这个故事中有所收益。

什么是调度(Scheduling),为什么要调度

当Docker横空出世,并且Linux容器化成为一种更广为人知的技术时,我们意识到,可以通过容器化基础架构的实施而受益。Docker容器映像提供了一个不变的、可部署的“神器”,它可以一次构建并部署在开发、测试和生产中。此外,它还保证生产环境中运行的映像的依赖性,与测试期间的依赖性完全相同。

另一个好处尤其重要:Docker允许将部署单元(容器)与计算单元(主机)解耦,它通过利用调度程序将容器分配给主机(希望以一种智能的方式),从而消除了服务器与应用程序之间的耦合——给定的容器可以在任意数量的可能的服务器上运行。

通过将后端服务打包成Docker映像,并且可以随时将其部署并扩展到服务器集群,我们应该能够迅速适应变化。我们可以添加新的玩家功能,当流量增加时进行扩容,并快速推出更新和修复程序。在考虑将容器内的服务部署到生产环境时,需要解决三个主要问题:

  1. 给定一个主机集群,如何选择一组特定的主机来接收一组容器?
  2. 这些容器实际上是如何在远程主机上启动的?
  3. 容器“死机(或者关机)”时会发生什么?

这三个问题的答案是,我们需要一个调度程序——一种在服务集群层面运行并执行我们的容器策略的服务。调度程序是维护集群、确保容器在正确的位置运行,以及在容器退出时重新启动它们的关键组件。

例如,我们可能要启动诸如Hextech Crafting之类的服务,该服务需要六个容器实例来处理其负载。调度程序负责查找具有足够内存和CPU资源以支持这些容器的主机,并执行使这些容器运行所需的任何操作。如果这些服务器之一发生故障,调度程序还负责为受影响的容器查找替换主机。

当我们决定使用调度程序时,就快速进行原型设计,以便了解容器化服务在生产中是否适合我们。此外,我们需要确保现有的开放源代码选项可以在目前的环境中运行,或者确保维护人员愿意接受我们的调整。

为什么要自己写

在开始编写Admiral调度程序之前,我们调查了现有集群管理器和调度程序的状况。都有谁在Docker主机集群之间调度容器,它们是如何做到的?它们的技术还能解决我们的问题吗?

在最初的研究中,我们调研了一些项目:

Mesos + Marathon

  • 这些技术已经相当成熟并且可以大规模使用,但是安装起来却复杂且棘手,这使得它们难以进行尝试和评估。
  • 当时,它们对容器的支持还非常有限,没有跟踪Docker的快速发展,并且在Docker生态系统中表现不佳。
  • 它们不支持容器组(pods)——我们认为需要将sidecar容器与许多服务捆绑在一起(附注:sidecar是容器日志的一种模式)。

LMCTFY => Kubernetes

  • Kubernetes刚刚从LMCTFY演变而来,尽管它看起来很有希望,但尚不清楚它的未来发展是否能满足我们的需求。
  • Kubernetes还没有一个约束系统可以像我们需要的那样进行容器放置。

Fleet

  • Fleet是后来开放源代码的,当时还不够成熟。
  • Fleet似乎更专注于系统服务的部署,而不是常规应用程序服务。

我们还原型化了一个小型命令行工具,该工具可通过REST与Docker API进行通信,并且成功演示了如何使用此工具来协调部署。然后,我们决定继续编写自己的调度程序。

我们借鉴了研究过的系统的一些最佳功能,包括Kubernetes的Pods和Marathon的约束系统背后的核心思想。我们的愿景是跟踪这些系统的体系结构和功能,在可能的情况下影响它们,并最终尝试在将来与其中之一融合。

Admiral概述

在创建了一个基于JSON的基础部署元数据语言(我们称为CUDL,ClUster描述语言)之后,我们开始编写Admiral。CUDL成为Admiral在其REST API中使用的语言,两个主要组成部分如下:

  • 集群——一组Docker主机。
  • 打包(Packs)——启动一组一个或多个容器所需的元数据。类似于Kubernetes Pod加Replication控制器。

集群和打包具有两个不同的方面:spec和live。每个方面都代表对容器生命周期不同阶段的描述。

Spec,表示元素所需的状态

  • 从某些外部事实来源(如来源控制)发布到Admiral
  • 一经交付给Admiral便一成不变
  • Spec集群和主机描述了集群中可用的资源
  • Spec打包描述了运行服务所需的资源、约束和元数据

Live,表示元素已实现的状态

  • 镜像实际的运行对象
    • Live集群和主机镜像正在运行的Docker守护程序
    • Live打包镜像正在运行的Docker容器组
  • 通过与Docker守护进程通信,实现可恢复性

Admiral用Go编写,并且在生产数据中心中运行时,被编译并打包到Docker容器中。Admiral有几个内部子系统,其中大部分如下图所示。

从用户的角度来看,与Admiral的交互是通过其提供的admiralctl命令行工具进行的,该工具通过REST API与Admiral进行通信。借助admiralctl,用户可以通过标准指令访问Admiral的所有功能:POST新的Spec打包以进行调度,DELETE旧包(Packs),以及GET当前状态。

在生产过程中,Admiral将使用Hashicorp的Consul存储Spec状态,定期对其进行备份,以防发生灾难性故障。万一完全丢失数据,Admiral还能使用从各个Docker守护程序检索到的Live状态中的信息,来部分重建其Spec状态。

协调器(reconciler)属于Admiral的核心,是驱动调度工作流程的关键子系统。协调器会周期性地将实际的Live状态与所需的Spec状态进行比较,并且在出现差异时,调度所需的操作,以便将Live状态恢复正常。

Live状态及其驱动程序包通过缓存Live主机和容器状态,并通过其REST API提供与集群主机上所有Docker守护程序的通信,来支持协调器。

深度调度

Admiral的协调器可对Spec打包进行操作,有效地将其转换为Live打包。在将Spec打包提交给Admiral时,协调器将创建容器并使用Docker守护程序启动它们。正是通过这种机制,协调器实现了我们前面所述的最重要的两个高级调度目标。当协调器收到Spec打包时,它将:

  1. 评估集群的资源和打包的约束,为容器找到合适的主机。
  2. 知道如何使用Spec中的数据在远程主机上启动容器。

让我们看一下在Docker主机上启动容器的示例。在此示例中,我们将使用本地Docker守护程序作为Docker主机,并与Admiral服务器的本地实例进行交互。

首先,我们使用“admiral pack create ”命令启动一个打包。此命令针对特定集群,并将Spec打包的JSON 提交到Admiral服务器。

你能注意到,几乎在刚刚运行命令后,容器就已经在我的机器上启动。这个容器是使用我的打包文件中的参数启动的,如下所示:

接下来,在调用“admiral pack create”之后,我们可以使用“show”命令来查看Admiral创建的Live打包。这里的命令是“admiral pack show ”。

最后,通过点击容器中的服务,我们可以验证打包是否正常工作。使用来自“admiral pack show”命令的信息,我们可以通过一个简单的curl来拼出我们的服务:

在Admiral内部,协调器始终处于运行状态,以确保集群的Live状态始终与所需的Spec状态相匹配。这样,当容器由于崩溃而失败并退出,或者由于硬件故障而导致整个服务器不可用时,我们还可以进行恢复业务。协调器努力确保状态匹配,以便玩家永远不会遇到中断问题。此功能解决了我们前面提到的第三个,也是最后一个问题:当容器意外退出时,我们可以快速恢复,并且将影响控制到最小。

下面将展示通过“admiral pack create”命令启动的现有容器。然后,我将终止该容器,并停止其执行。在几秒钟内,协调器启动了一个新的容器(具有不同的ID),因为它意识到Live状态与Spec状态不匹配。

资源和约束

为了最好地分配容器,调度程序必须洞悉主机集群。解决此问题有两个关键组件:

资源——服务器可用资源的一种表示形式,包括内存、CPU、I/O,以及网络等其他资源。

约束——打包随附的一组条件,可为调度程序提供有关可放置打包的限制的详细信息。例如,我们可能要放置一个打包实例:

  • 在整个集群中的每个主机上
  • 在名为“myhost.riotgames.com”的特定主机上
  • 在集群里每个标记的区域中

通过在主机上定义资源,我们使调度程序可以灵活地决定将容器放置在何处。

通过在打包集(packs)上定义约束,我们可以限制调度程序的选择,以便将特定的模式强制应用到集群中。

结论

对于Riot而言,Admiral是我们部署技术不断发展的重要组成部分。通过利用Docker和调度系统的功能,我们能够比以前更快地向玩家交付后端功能。

在本文中,我们深入研究了Admiral的一些功能,并展示了如何在一组机器集群之间调度容器。就像Jonathan在他的第一篇文章中提到的那样,开源世界已经迅速转向非常相似的模型。展望未来,我们将转移Admiral的工作,并专注于部署DC/OS,它已成为调度容器工作负载的领先的开源应用程序之一。

如果你经历了类似的旅程,或者觉得自己有话要补充,非常欢迎与我们取得联系。


更多“揭秘LOL”系列文章
● 揭秘LOL背后的IT基础架构丨踏上部署多样性的征程


关注微信:TF中文社区