并行过滤器调度器

注意

该规范已被 efried 放弃,原因是它自 2015 年以来一直停留在积压目录中。

此 backlog 规范讨论了并行性和 Nova 中当前过滤器调度器的问题。这对于从 cells v1 用户迁移到 cells v2 尤其重要。

问题描述

我们需要 nova 过滤器调度器在典型的公有云中表现良好,即使在从 cells v1 迁移到 cells v2 之后也是如此。

关于当前 nova-scheduler 的一些关键观察

  • 如果您运行两个 nova-scheduler 进程,它们会相互竞争,直到 nova-compute 资源跟踪器更新数据库后,它们才会发现彼此的选择。这导致许多部署选择 nova-scheduler 进程的活动/被动 HA 设置。

  • 资源跟踪器最终决定实例是否可以容纳。如果请求最终出现在一个已满的计算节点上,构建会出错,并让重试系统找到另一个要尝试的计算节点。但是,我们会在三次尝试后停止重试,这会延长用户的构建时间,因此最好避免这些重试。

  • 部署者经常选择先填充,以确保他们为他们提供的更大的 flavor 留出空间。然后,他们使用 IO-ops 过滤器来确保同一节点上不会发生太多构建。将这些结合起来,这会使上述竞争情况变得更糟。

  • 已经添加了决策的随机化来减少竞争的影响,但这意味着它正在做出“更差”的决策。

  • 查询数据库是调度过程中最昂贵的部分。

  • 基于 C 的 DB 驱动程序和 eventlet 意味着调度器在 eventlet 线程池非常小,理想情况下小于 5 时表现最佳。否则,您会发现它会发出几个数据库调用,然后处理它从数据库获取的(现在过时)信息。

  • Caching Scheduler 被添加进来,以定期更新主机状态,以便没有用户请求需要等待数据库查询。它使用 consume_from_instance 来更新缓存状态以供未来的请求使用。请注意,只有在下一个轮询周期,数据才会刷新以包含有关任何删除请求的信息。状态也仅限于每个调度器进程。

  • 直到 kilo 结束,许多过滤器和权重也进行了数据库查询以获取诸如主机聚合之类的信息。帮助隔离调度器与 nova 其余部分的工作已经删除了所有这些额外的数据库调用,因此它现在仅定期从数据库获取状态。

cells v1 将系统分片,因此每个 cell 都有一个 nova-scheduler。每个 cell 通常有数百个主机。当构建请求进来时,cells v1 首先选择一个 cell,然后在该 cell 内部,常规 nova-scheduler 选择该 cell 中的一个计算节点。API cell 从每个子 cell 中的 nova-cells 进程获得一个插槽列表。子 cell 的 nova-cells 进程会定期查看所有主机的状态,以及当前的一组 flavor,并报告每个 cell 可用的插槽数。这些插槽基于内存和磁盘,而不是实际的每个 flavor。该系统有几个限制,但对于本次讨论的关键限制是

  • 一旦进入一个 cell,构建重试尝试仅发生在同一 cell 内的计算节点之间。如果在某个 cell 中失败构建,则无法尝试另一个 cell。如果某个 cell 变得已满,则无法在需要调整大小时将 VM 移动到另一个 cell。

  • 报告的插槽具有隐藏的依赖关系。如果您有 2 GB VM 的空间,系统还会报告两个 1 GB VM 的插槽。无法表达这三个插槽是相关的。如果调度器选择使用 2GB 插槽,当下一个请求使用其中一个 1GB 插槽时,当它到达该 cell 时,它会发现容量已经被之前的请求使用了。

  • 当前的 cells 调度器在调度决策之间不会更新其内存状态,也没有随机化。考虑两个 cell,一个有 12x1GB 插槽,另一个有 10x1GB 插槽。如果您收到 15 个 1GB 插槽的请求,它们都会发送到报告 12 个插槽的 cell。其中 2 个构建请求将失败,因为该 cell 没有空间。有计划在这些插槽之间随机分配构建,但这只会限制此问题的影响,而不是消除它。

  • 这在 Google omega 论文中被描述为“两级调度”

总而言之,当前的 nova-scheduler 在以下情况下表现最佳

  • 只有一个 nova-scheduler 进程正在运行

  • 它会定期从数据库刷新其状态

  • 它会用它所做的任何先前决策来更新其内存状态

  • 它一次做出一个决定,零并行性

这是我们需要寻找扩展 nova-scheduler 的新方法的基础,以便我们能够扩展到现有 cells v1 用户能够迁移到 cells v2 的水平,在 cells v2 中,单个 nova-scheduler 部署必须处理当前 15-20 个 nova-scheduler 正在处理的负载。

用例

考虑一个部署,您拥有超过 10k 个 hypervisor,并且构建请求在 15 分钟内至少有 1k 的爆发。

这对于将要迁移到 cells v2 的 cells v1 用户尤其相关。

提议的变更

此规范旨在同意问题并列出一些可能的解决方案。

备选方案

多个调度器未来

Nova 有两个调度器,随机调度器和过滤器调度器。在未来,越来越有可能出现多个适用于特定用例和使用模式的调度器。

考虑到这一点,我将专注于替换上述描述的典型公有云用例。

远离过滤器和权重

现有调度器的一种替代方案是让 nova-compute 节点从共享队列中拉取构建请求,而不是从中央调度器推送工作。

这对于先扩散来说效果很好,但需要一些仔细的协调才能使先填充调度器工作。您可能需要一个中央系统来授予新的计算节点从队列中拉取权限,或者类似的东西。

如果您从队列中拉取消息并发现您无法服务该请求,您可以将该消息放回队列。理想情况下,您应该分片队列的数量以限制此类重试所需的案例。但是,很难在每个租户关联和反关联规则的情况下做到这一点。

虽然我真的希望有人探索构建这样的驱动程序,但此规范不考虑这种方法。它将专注于对当前过滤器调度器的更直接的替换。

分区过滤器调度器

减少问题大小的好方法是将更大的问题分解成更小的部分。让我们更详细地看看这一点。

一个主要问题是不同调度器之间的干扰。理想情况下,我们不希望多个调度器将工作分配给相同的 nova-compute 节点,因为它们将争夺相同的资源。理想情况下,每个调度器都应该查看不同的主机子集。

与此要求作斗争的是集群范围内的行为,例如关联和反关联规则,在理想情况下,我们需要知道系统的完整状态,而不是仅仅查看系统的子集。

可以进行动态分区,但为了简单起见,我将专注于系统的静态分区。静态分区的缺点是它们往往具有容量规划影响。如果所有请求的子集被路由到特定的一组主机,那么您需要确保增加主机数量以匹配该主机子集的的需求。

在 cells v1 中,顶级调度用于尝试在许多随着扩展而添加的组之间传播负载,但这种两级调度导致了许多其他竞争。

考虑到这些想法,我们可以考虑一个有趣的用例

  • 某些主机组可以具有映射到特定 flavor 的特定硬件。例如,SSD 与非 SSD 本地存储与所有存储来自 cinder(没有本地磁盘)

  • 将 Windows 和 Linux VM 放在不同的 hypervisor 上是很常见的做法,以便能够最好地利用批量许可节省。这是基于用户构建请求对主机进行区分的非常相似的划分。

让我们考虑为这些主机组中的每一个主机组创建一个单独的 nova-scheduler 集群。我们可以根据请求规范将请求路由到每个调度器集群。flavor 需要在所有构建请求中,并且可以路由到每个子集中的一个。诸如关联之类的全局概念在这些主机组之间没有意义,并且请求路由器可以检查这些类型的约束。

在 cells v2 世界中,您将在每个组中拥有多个 cell。为了简单起见,我们可以假设每个完整的 cell 都注册到其中一个(且仅一个)调度器集群。实际上,我们可能希望每个主机都知道它应该向哪个调度器报告内容。

此分区的优点是,您需要独立地为这些主机组中的每一个主机组进行容量规划,而不管如何实现调度。

还有许多其他可能的划分,但似乎这是最简单且将帮助许多大型云用户从 cells v1 迁移到 cells v2 的划分。让我们考虑另一个划分,例如使用租户的哈希值来选择一些不同的主机子集。您需要大量的租户和/或租户之间的均匀使用,否则您最终必须根据这些不同租户的需求增加每个组的容量。当这些调度器查看重叠的主机子集时,您可以改善资源的传播,但您倾向于在不同的调度器集群之间遇到一些干扰。

虽然这些替代分区方案在实施了此处讨论的其他增强功能后可能很有用,但我将此规范的范围限制为最简单的分区方案,即基于请求的 flavor 对主机进行不同的划分,用于初始版本。这种方法的的主要缺点是它限制了分区的影响到非常大的云部署,那些有几个不同的主机组的部署,这些主机组的容量是独立管理的。

使用资源跟踪器实现“分布式”锁定

已经进行了各种讨论,关于让资源跟踪器持久化它发出的资源声明,以便这些声明在 nova-compute 服务重新启动后仍然存在。在此之上,我们可以添加一些 RPC 调用,以便 nova-conductor 或任何其他节点能够在 VM 移动操作期间获取这些声明中的一个,例如调整大小和实时迁移,您不希望新的 VM 构建占用您即将使用空间的位置。还讨论了如果未在一段时间内使用该声明,这些声明应过期。这应该可以防止由于未使用的资源跟踪器声明而导致的容量泄漏等故障模式。这将原本可能成为分布式锁定机制的东西转变为每个 nova-compute 锁定系统,这应该意味着锁争用更少,并且通常更容易解决问题。

当资源跟踪器将其当前可用资源报告给调度器时,它会减少空闲资源量以考虑其资源上的当前声明。

现在,假设调度器能够在将选定的主机返回给 nova-conductor 之前获取这些声明中的一个。这将把声明请求从 nova-compute 中的构建过程的非常开始移动到调度器。这将允许调度器为请求的资源构建声明集合,然后再将选择返回给调用者,调度器已选择哪些资源。如果检测到问题,调度器可以重试,直到它获得对给定资源请求到调度器的所有必需声明为止。

将所有这些放在一起,您会看到调度器将开始看到其他调度器的决策,因为其他调度器获取的声明会更快地显示在共享状态中。

更进一步,我们可以确保调度器在将刚刚获取的声明返回给调度器的调用者之前,等待该声明出现在共享状态中。

另一个可能的变体是考虑一个与“比较和交换”DB 调用系统非常相似的声明系统。当调度器发出声明时,它可以告诉计算节点仅在计算节点仍然具有相同的空闲资源并且调度器当前认为它具有的情况下才发出该声明。如果调度器对资源有不同的看法,它应该更新其内部状态以查看这是否仍然是发送请求的最佳节点。可以通过对当前报告的主机状态进行哈希来完成。假设这种哈希不会在实例从声明状态变为使用该声明的状态时发生变化。

似乎这些策略的组合应该有助于确保调度器能够在将选定的计算节点返回给调度器调用者之前处理来自其他并行调度器的大多数竞争。这应该可以降低任何仍然可能发生的调度器竞争的成本。

从查询 DB 状态转变为消耗更新流

如上所述,调度过程的最昂贵部分不是运行过滤器和权重的列表,而是从数据库获取更新当前主机状态。

我们当前使用 Caching Scheduler 来降低这些 DB 调用的成本,但使用过时的数据,该数据会定期更新内存以减少其过时影响。一个有趣的替代方案是仅仅消耗当前状态的更新,而不是每次都必须获取完整的副本:https://blueprints.launchpad.net/nova/+spec/no-db-scheduler

这与 omega 论文中讨论的共享状态调度器非常相似。在这种情况下,共享状态使用每个调度器中的内存结构实现,并且需要向所有消费者提供更新流。

如果您需要重新启动 nova-scheduler 进程或启动额外的 nova-scheduler 进程,它们需要返回到“开始”并消耗所有更新,以便其状态与所有其他调度器同步,然后再开始服务任何请求。确保所有计算节点偶尔报告其完整状态意味着存在一个可以修剪旧更新的点,并且仍然可以获得系统的完整视图。

no-db-scheduler 的摩擦痛点是维护看起来像 DB 日志实现的代码的复杂性。能够有效地修剪旧更新,以便任何新的调度器只需要赶上少量数据。事实证明,Kafka 已经实现了许多这些语义,并且已经证明它可以在极大的规模上工作:http://kafka.apache.org/documentation.html#introduction

看来我们可以创建一个基于 Kafka 的系统,以高效地获取系统当前状态的增量更新,而不是必须进行昂贵的数据库调用来获取我们感兴趣的所有主机的状态。

内存问题

一直以来,人们担心我们是否可以在内存中存储系统中所有主机及其当前状态的列表。

看来,在实际操作中,这将会是我们最不担心的问题,在确定此解决方案能够达到的规模上限时。

数据模型影响

REST API 影响

安全影响

通知影响

其他最终用户影响

性能影响

其他部署者影响

任何解决方案都需要一种从现有调度器进行实时升级的方法。

开发人员影响

实现

负责人

主要负责人:无

其他贡献者:无

工作项

依赖项

测试

现有的 Tempest 测试将能够确保调度器能够作为旧调度器的直接替代品。

应该增强 grenade 测试(或类似的测试),以测试现有调度器和这个新调度器之间的迁移。

最好调查一些功能测试来压力测试调度系统,这样我们就可以模拟在某些生产场景中看到的情况,并证明新系统是否有所改进。

文档影响

参考资料

Google omega 论文:http://research.google.com/pubs/pub41684.html

历史

修订版

发布名称

描述

Train

已放弃