下载APP
登录
关闭
讲堂
算法训练营
Python 进阶训练营
企业服务
极客商城
客户端下载
兑换中心
渠道合作
推荐作者

第二章 调度

2018-02-27 薛命灯
在第一章中,我们介绍了 Riot 应用部署的发展简史以及我们曾经面临的挑战。我们特别强调了在发展过程中遇到的困难,为了更好地支持《英雄联盟》,我们加入越来越多的基础设施,而手动分配服务器的方式给我们带来了很大麻烦。Docker 的出现改变了服务器的部署方式,我们开发了内部工具 Admiral,用于集群的调度和管理。
要知道,这个旅程尚未结束,它还在不断演化,我们后续还准备使用 DC/OS。这一章将介绍我们是如何发展到今天这个地步的,以及在这一过程中我们所做的重要决定,希望对读者有所裨益。

什么是调度以及为什么要有调度

随着 Docker 的问世以及 Linux 容器化技术的普及,我们意识到对基础设施进行容器化一定能够给我们带来好处。Docker 容器镜像提供了一种不可变的可部署文件,一次构建就可以用在开发、测试和生产环境里。除此之外,我们还能确保生产环境的镜像与测试环境的镜像是一样的。《英雄联盟》在线服务运维之道
Docker 还为我们带来了另一个好处:Docker 通过调度器将容器分配给主机,从而解耦了部署单元(容器)和计算单元(主机)。也就是说,服务器和应用程序之间的耦合性没有了,容器可以运行在任意台服务器上。
后端的服务被打包进了 Docker 镜像里,我们可以在任何时候将它们部署到多台服务器上,因此能够快速做出变更。我们可以开发新的功能,对流量较大的功能进行扩展,快速地进行更新和问题修复。不过,要在生产环境中使用容器进行应用部署,需要解决三个问题:
如何从主机集群中选择适量的服务器来部署容器?
如何远程启动这些容器?
如果容器挂掉了该怎么办?
答案是我们需要一个调度器——一个运行在集群上的服务,执行我们设定好的容器策略。调度器是一个非常重要的组件,它可以确保容器在正确的地方运行,一旦发生崩溃立即将其重启。比如,我们的海克斯工匠系统需要六个容器实例,调度器负责找出拥有足够内存和 CPU 的主机来运行这些容器,并看管好它们。如果其中的一台服务器发生宕机,调度器需要负责找到另一台替代主机,确保容器可以继续运行。
在考虑使用调度器时,我们要求调度器能够帮助我们快速地创建原型,及早知道容器化的服务是否能够在我们的生产环境中正常运行。另外,我们还要确保现有的开源产品能够适用于我们的环境,而且维护人员愿意使用它们。

为什么要开发自己的调度器

在开发我们自己的调度器 Admiral 之前,我们对现有的集群管理器和调度器进行了调研。在调研过程中,我们研究了一些开源项目。MesosMarathon
这些框架很成熟,而且支持大规模集群,但它们太过复杂,安装起来也很取巧,所以用户体验并不是很好。
那个时候,它们支持的容器很有限,无法跟上 Docker 的发展步伐,在 Docker 生态系统里并没有那么耀眼。
它们不支持容器群组(pod),但我们需要这样的功能,因为我们要为很多服务绑定边车(sidecar)。
Kubernetes 从 LMCTFY 发展而来,虽然它看起来雄心勃勃,但我们不确定它未来的演化是否能够满足我们的需求。
Kubernetes 当时还没有一个能够满足我们需求的约束系统。
Fleet 当时才刚刚开源,不像现在这么成熟。
它似乎更适合用于部署系统服务,而不是用于部署一般的应用服务。
我们也尝试开发了一个命令行小工具,通过 REST 与 Docker API 发生交互,并成功地使用该工具进行部署编配。于是我们决定继续使用自己的调度器,我们从调研过的框架里借鉴了一些经验,比如 Kubernetes 的 Pod 概念和 Marathon 的约束系统。我们的目标是搞清楚这些系统的架构和功能,并在未来某个时刻把这些功能汇聚在一起。

Admiral 概览

我们先是开发了一种叫作 CUDL(CIUster Description Language,集群描述语言)的元数据语言,它是基于 JSON 的,用于基础部署。后来,我们开发了 Admiral。Admiral 将 CUDL 用在它的 REST API 当中。CUDL 包含了两个主要的组件:
Cluster——一系列 Docker 主机。
Pack——用于启动容器的元数据,类似 Kubernetes 的 Pod 和复制控制器。Cluster 和 Pack 包含了 Spec 和 Live,它们分别代表了容器生命周期的不同阶段。Spec 代表了元素的预期状态:
从外部源(如源码控制系统)发送到 Admiral 上。
发送到 Admiral 后就不可变。
Spec Cluster 和 Host 描述了集群中可用的资源。
Spec Pack 描述了运行一个服务所需的资源、约束和元数据。Live 代表了元素的实际状态:
反映实际的运行对象。
Live Cluster 和 Host 反映了运行中的 Docker 后台进程。
Live Pack 反映了运行中的 Docker 容器群组。
通过与 Docker 后台进程交互来恢复状态。
Admiral 使用 Go 语言开发,在部署到生产环境时被编译并打包到一个 Docker 容器里。Admiral 包含了多个内部子系统,如下图所示。
用户使用基于 REST API 的命令行工具 admiractl 与 Admiral 展开交互。用户可以通过这个工具访问到 Admiral 的所有功能:POST 新的 Spec Pack 进行调度、DELETE 旧的 Pack,以及 GET 当前的状态。
在生产环境,Admiral 使用 Consul 来保存 Spec 状态,并定期备份,以备不时之需。如果发生数据丢失,Admiral 可以使用从 Docker 后台进程获得的 Live 信息来重建部分 Spec 状态。
调节器(reconciler)是 Admiral 的核心子系统,是驱动调度工作流的关键组件。调节器定时对实际的 Live 状态和预期的 Spec 状态进行比对,如果出现差异,就会调度相应的措施让 Live 状态恢复正常。
Live 状态和它的驱动包为调节器提供支撑,它们缓存 Live 主机和容器状态,为集群主机上的所有 Docker 后台进程提供基于 REST API 的交互。

深度调度

Admiral 的调节器直接操作 Spec Pack,再将它们转成 Live Pack。Spec Pack 被提交到 Admiral 之后,调节器就可以创建容器,并使用后台进程运行容器。调节器就是通过这种机制实现了前面提到的两个高级调度目标。调节器在收到一个 Spec Pack 时会做以下几件事情:
评估集群资源和 Pack 的约束,为容器寻找合适的主机。
使用 Spec 数据在远程主机上启动容器。我们举一个在 Docker 主机上启动容器的例子。我将会使用本地的 Docker 后台进程作为 Docker 主机,并让它与本地的 Admiral 实例发生交互。
首先,我们使用admiral pack create <cluster name> <pack file>命令启动一个 Pack。这个命令会向 Admiral 提交 Spec Pack。
这个命令会启动一个容器,这个容器使用了 Pack 文件里指定的参数。
{
"name": "dat.blog_scout",
"description": "A pack of the Scout service for usage in our engineering blog post.",
"service": {
"location": "dev.local.test",
"discovery": {},
"appname": "dat.scout"
}, "containers": [
{
"image": "datd/scout",
"version": "1.0.0",
"ports": [{
internal": 8080,
"external": 8080
}]
}
],
"count": 1
}
接下来,在调用admiral pack create之后,我们可以用show命令来查看由 Admiral 创建的 Live Pack:admiral pack show <cluster name> <pack name>
最后,我们通过向容器里的服务发起请求来验证我们的 Pack 是否正确。admiral pack show命令为我们返回了一些信息,然后我们使用 curl 向服务发起请求。
在 Admiral 里,调节器一直处于运行状态,确保集群的 Live 状态和预期的 Spec 状态相匹配。如果某个容器发生崩溃,或者整个服务器因硬件问题导致不可用,我们都能够及时发现。调节器可以确保玩家们不会感觉到被中断。
这就解决了我们之前提到的第三个和第四个问题:如果一个容器突然退出,我们可以快速恢复,将影响降到最低。
下面列出了通过admiral pack create命令启动的容器。然后我将容器停掉,几秒钟之后,调节器会启动一个新的容器(拥有不同的 ID),因为调节器知道 Live 状态和预期的 Spec 状态不一致。

资源和约束

为了更好地分配容器,调度器必须对集群了如指掌。有两个关键组件可用于解决这个问题:
资源( R e s o u r c e )——表示服务器可用的资源,包括内存、CPU、IO 和网络。
约束(Constraint)——Pack 中包含的一系列条件,让调度器知道该将 Pack 放在哪里。例如,我们有可能将 Pack 实例放置在以下几个位置:
放在集群的每台主机上。
放在某台叫作“myhost.riotgames.com”的主机上。
放在集群里打了标签的区域。
通过定义主机的资源,我们让调度器具备了足够的灵活性来决定将容器放置在哪台服务器上。通过在 Pack 中定义约束,我们可以对调度器做出限制,强制将某些模式应用在集群上。
Admiral 是 Riot 部署技术演化当中最为关键的一个组件。借助 Docker 和调度系统的强大力量,我们可以更快地将后端新功能交付给玩家。
更多内容,请点击《英雄联盟 | 在线服务运维之道》阅读专题全部文章。
 写留言

精选留言

由作者筛选后的优质留言将会公开显示,欢迎踊跃留言。