处理重塑的提供者树

https://blueprints.launchpad.net/nova/+spec/reshape-provider-tree

虚拟驱动程序需要能够改变它们暴露的提供者树的结构。在迁移现有资源时,现有的分配需要与库存一起移动。并且必须以避免竞争条件的方式进行,在这种情况下,第二个实体可能会对正在移动的库存创建或删除分配。

问题描述

用例

  • libvirt 驱动程序当前在计算节点提供者上清点 VGPU 资源。为了利用提供者树,libvirt 需要为每个物理 GPU 创建一个子提供者,并将 VGPU 库存从计算节点提供者移动到这些 GPU 子提供者。在已经将 VGPU 资源分配给实例的实时部署中,分配需要与库存一起移动。

  • 希望建模 NUMA 的驱动程序同样需要创建子提供者,并将多个类别的库存和分配(处理器、内存、NUMA 对齐的 NIC 上的 VF 等)移动到这些提供者。

  • 驱动程序正在使用自定义资源类。该资源类被添加到标准集合中(使用一个新的、非 CUSTOM_ 名称)。为了使用标准名称,驱动程序必须将库存和分配从旧名称移动到新名称。

这些只是现在或将来可能存在的示例情况。我们在这里描述的是一个通用的枢轴系统。

提议的变更

总体流程如下。红色部分仅在需要重塑时发生。这仅代表计算启动时的正常路径。

../../../_images/reshape-provider-tree1.svg

请注意,对于快速前进升级,Resource Tracker 路径实际上是 离线升级脚本

SchedulerReportClient.get_allocations_for_provider_tree()

将实现一个新的 SchedulerReportClient 方法

def get_allocations_for_provider_tree(self, context, nodename):
    """Retrieve allocation records associated with all providers in the
    provider tree.

    :param context: The security context
    :param nodename: The name of a node for whose tree we are getting
            allocations.
    :returns: A dict, keyed by consumer UUID, of allocation records:
            { $CONSUMER_UUID: {
                  # The shape of each "allocations" dict below is identical
                  # to the return from GET /allocations/{consumer_uuid}
                  "allocations": {
                      $RP_UUID: {
                          "generation": $RP_GEN,
                          "resources": {
                              $RESOURCE_CLASS: $AMOUNT,
                              ...
                          },
                      },
                      ...
                  },
                  "project_id": $PROJ_ID,
                  "user_id": $USER_ID,
                  "consumer_generation": $CONSUMER_GEN,
              },
              ...
            }
    """

消费者不总是实例(它可能是“迁移” - 或将来由 Nova 未创建的其他事物),因此我们不能只使用实例列表作为消费者列表。

我们无法获取与关联共享提供者所有的分配,因为其中一些分配将属于其他主机上的消费者。

因此,我们必须发现与“本地”树(由 nodename 标识)中的提供者关联的所有消费者

for each "local" provider:
    GET /resource_providers/{provider.uuid}/allocations

我们不能使用这些分配,因为我们会错过共享提供者的分配。因此,我们必须获取仅针对上述发现的消费者所有分配。

for each consumer in ^:
    GET /allocations/{consumer.uuid}

注意

如果所有消费者的分配都位于共享提供者上,我们仍然会丢失数据。我没有找到解决这个问题的好的方法。但这种场景在短期内不会发生,因此它将作为代码注释中的一个限制记录下来。

返回一个字典,键为 {consumer.uuid},包含结果分配记录。这是 update_provider_treeupdate_from_provider_tree 期望的新 分配参数 的形式,并返回它。

ReshapeNeeded 异常

将引入一个新的异常,ReshapeNeeded。它用作从 update_provider_tree 发出的信号,表明必须执行重塑。这是为了性能原因,这样我们就不需要在不需要时 get_allocations_for_provider_tree

ReshapeFailed 异常

将引入一个新的异常,ReshapeFailed。它仅在需要重塑、尝试且不成功(即当 Placement POST /reshaper 调用失败时)从 update_from_provider_tree 抛出。这样我们就可以在 _update_available_resource_for_node 中显式捕获它并关闭计算服务。

对 update_provider_tree() 的更改

分配参数

将向 update_provider_tree() 添加一个新的 allocations 关键字参数

def update_provider_tree(self, provider_tree, nodename, allocations=None):

如果为 None,则 upgrade_provider_tree() 方法不得执行重塑。如果它决定需要重塑,则必须引发新的 ReshapeNeeded 异常。

当不为 None 时,allocations 参数是一个字典,键为消费者 UUID,分配记录的形式如下

{ $CONSUMER_UUID: {
      # NOTE: The shape of each "allocations" dict below is identical to the
      # return from GET /allocations/{consumer_uuid}...
      "allocations": {
          $RP_UUID: {
              "generation": $RP_GEN,
              "resources": {
                  $RESOURCE_CLASS: $AMOUNT,
                  ...
              },
          },
          ...
      },
      "project_id": $PROJ_ID,
      "user_id": $USER_ID,
      # ...except for this, which is coming in bp/add-consumer-generation
      "consumer_generation": $CONSUMER_GEN,
  },
  ...
}

如果 update_provider_tree() 正在移动分配,则必须就地编辑 allocations 字典。

注意

我不喜欢该方法就地编辑字典而不是返回副本的想法,但它与我们处理 provider_tree 参数的方式一致。

虚拟驱动程序

当前覆盖 update_provider_tree() 的虚拟驱动程序需要更改签名以适应新参数。这项工作将在本蓝图的范围内完成。

当虚拟驱动程序开始在嵌套提供者中建模资源时,它们的实现需要

  • 确定是否需要重塑并根据需要引发 ReshapeNeeded

  • 通过处理提供者库存和指定的分配来执行重塑。

这项工作不在本蓝图的范围内。

对 update_from_provider_tree() 的更改

SchedulerReportClient.update_from_provider_tree() 方法更改为接受一个新的参数 allocations

def update_from_provider_tree(self, context, new_tree, allocations):
    """Flush changes from a specified ProviderTree back to placement.

    ...

    ...
    :param allocations: A dict, keyed by consumer UUID, of allocation records
            of the form returned by GET /allocations/{consumer_uuid}. The
            dict must represent the comprehensive final picture of the
            allocations for each consumer therein. A value of None indicates
            that no reshape is being performed.
    ...
    """

allocationsNone 时,update_from_provider_tree() 的行为与之前(在 Queens 中)相同。

对资源跟踪器 _update() 的更改

_update() 方法获得一个新的参数,startup,它从 update_available_resource() 传递下来。

在当前调用 update_provider_treeupdate_from_provider_tree 的地方,代码流程将更改为大约

try:
    self.driver.update_provider_tree(prov_tree, nodename)
except exception.ReshapeNeeded:
    if not startup:
        # This isn't supposed to happen during periodic, so raise
        # it up; the compute manager will treat it specially,
        # killing the compute service.
        raise
    LOG.info("Performing resource provider inventory and "
             "allocation data migration during compute service "
             "startup or FFU.")
    allocs = reportclient.get_allocations_for_provider_tree()
    self.driver.update_provider_tree(prov_tree, nodename,
                                     allocations=allocs)
...
reportclient.update_from_provider_tree(context, prov_tree, allocs)

对 _update_available_resource_for_node() 的更改

目前,这是捕获 资源跟踪器 _update 周期性任务的所有异常、记录它们并忽略它们的地方。我们将添加新的 except 条件来处理与重塑相关的异常,这些异常将真正使计算服务崩溃(即不记录并忽略)。这些异常应该只在启动时合法地到达此方法。

Placement POST /reshaper

在新的 placement 微版本中,将引入一个新的 POST /reshaper 操作。有效负载的形式如下

{
  "inventories": {
    $RP_UUID: {
      # This is the exact payload format for
      # PUT /resource_provider/$RP_UUID/inventories.
      # It should represent the final state of the entire set of resources
      # for this provider. In particular, omitting a $RC dict will cause the
      # inventory for that resource class to be deleted if previously present.
      "inventories": { $RC: { <total, reserved, etc.> } }
      "resource_provider_generation": <gen of this RP>,
    },
    $RP_UUID: { ... },
  },
  "allocations": {
    # This is the exact payload format for POST /allocations
    $CONSUMER_UUID: {
      "project_id": $PROJ_ID,
      "user_id": $USER_ID,
      # This field is part of the consumer generation series under review,
      # not yet in the published POST /allocations payload.
      "consumer_generation": $CONSUMER_GEN,
      "allocations": {
        $RP_UUID: {
          "resources": { $RC: $AMOUNT, ... }
        },
        $RP_UUID: { ... }
      }
    },
    $CONSUMER_UUID: { ... }
  }
}

在单个原子事务中,placement 替换 inventories 字典中的每个 $RP_UUID 的库存;并替换 allocations 字典中的每个 $CONSUMER_UUID 的分配。

返回值

  • 204 No Content 成功时。

  • 409 Conflict 任何提供者或消费者生成冲突时;或者如果检测到并发事务。对于前者,应该使用适当的错误代码,以便调用者可以判断是否需要刷新 GET 并在重试操作之前重新计算必要的重塑。

  • 400 Bad Request 任何其他失败时。

直接访问 Placement

为了使 离线升级脚本 成为可能,我们需要使 placement 可通过导入 Python 代码而不是作为独立服务来访问。最快的途径是使用 wsgi-intercept 允许 HTTP 交互,使用 requests 库,仅通过网络进行数据库流量。这允许客户端代码使用相同的 API 修改 placement 数据存储,而无需运行 placement 服务。

一个实现,作为一个名为 PlacementDirect 的上下文管理器,已经被合并。该上下文管理器接受一个 oslo config,由调用者填充。这允许调用代码控制它希望如何发现配置设置,最重要的是 placement 使用的数据库。

这个实现提供了一个快速的解决方案,以满足 Placement POST /reshaper 离线使用的直接需求,同时允许在未来更优雅的解决方案出现时扩展。

离线升级脚本

为了方便快速前进升级,我们将提供一个脚本,在所有服务(数据库除外)都离线时执行此重塑。它看起来像

nova-manage placement migrate_compute_inventory

…并按以下方式运行,对于主机上的每个 nodename(ironic 除外)

  • 使用 直接访问 Placement 启动 SchedulerReportClient。

  • 通过 SchedulerReportClient.get_provider_tree_and_ensure_root() 获取 ProviderTree。

  • 实例化适当的虚拟驱动程序。

  • 执行 资源跟踪器 _update 中记录的算法,就好像 startupTrue 一样。

我们可能参考 https://review.openstack.org/#/c/501025/ 获取需要虚拟驱动程序的升级脚本示例。

备选方案

Reshaper API

邮件列表线程etherpad、IRC、hangout 等中讨论了 Placement POST /reshaper 的替代方案。它们包括

  • 不提供原子 placement 操作 - 一次执行资源跟踪器所需的必要操作。由于竞争条件而被拒绝:调度程序可能会根据不正确的容量信息对正在移动的库存进行调度。

  • “锁定”正在移动的库存 - 通过提供锁定 API 或通过设置 reserved = total 来实现 - 而资源跟踪器执行重塑。由于这是一个 hack;并且从部分故障中恢复会很困难而被拒绝。

  • “合并”形式的新 placement 操作

    • PATCH(或 POST)带有 RFC 6902 样式的 "operation", "path"[, "from", "value"] 指令。

    • PATCH(或 POST)带有 RFC 7396 语义。JSON 有效负载看起来像 Placement POST /reshaper 中描述的稀疏版本,但仅包含更改。

  • placement 操作的其他有效负载格式(请参阅 etherpad)。我们选择我们所做的,因为它重用了现有的有效负载语法(因此可能能够重用代码),并且它提供了预期最终状态的完整规范,这是一种 RESTful 的方式。

直接 Placement

对于 直接访问 Placementwsgi-intercept 模型的替代方案

  • 直接访问对象方法(进行一些重构/清理)。由于我们失去了模式验证和微版本逻辑而被拒绝。

  • 创建更简洁、pythonic 的包装器来包装这些对象方法。为了方便起见,在短期内被拒绝。如果对直接 placement 的需求超出 FFU 脚本,我们可能会在长期内采用这种方法。

  • 使用 wsgi-intercept 但在 REST 层之外创建 pythonic 包装器。这也是一个长期选项。

通过 update_provider_tree() 进行重塑

  • 我们考虑每次都将分配传递给 update_provider_tree,但收集分配会很昂贵,因此我们需要一种仅在必要时才执行此操作的方法。进入 ReshapeNeeded 异常

  • 我们考虑在每个周期性间隔运行检查和重塑(如果需要)算法,但我们决定除了在启动时之外,我们永远不应该执行重塑。

数据模型影响

无。

REST API 影响

请参阅 Placement POST /reshaper

安全影响

无。

通知影响

无。

其他最终用户影响

请参阅 升级影响

性能影响

新的 Placement POST /reshaper 操作可能很慢,并且会锁定多个表。它的使用应限制为重塑提供者树。最初,我们可能会即使没有进行重塑,也从 update_from_provider_tree 使用 reshaper;但如果发现这会对性能造成问题,我们可以将其限制为仅重塑场景,这将非常罕见。

收集分配,尤其是在大型部署中,可能很繁重且缓慢,因此我们仅在计算启动时执行此操作,并且仅当 update_provider_tree 指示需要重塑时才执行此操作。

其他部署者影响

请参阅 升级影响

开发人员影响

请参阅 虚拟驱动程序

升级影响

支持实时升级。 资源跟踪器 _update 流程将在计算启动时运行并根据需要执行重塑。由于我们不支持跳过实时升级的版本,因此虚拟驱动程序特定的更改可以从一个版本删除到下一个版本。

为快速前进升级提供的离线升级脚本。由于 FFU 中的每个步骤都使用每个发布的代码库运行代码,因此任何与 virt 驱动程序相关的更改都可以从一个发布版本中删除到下一个发布版本。但是,请注意,该脚本始终必须运行,因为只有在特定计算节点上运行的 virt 驱动程序才能确定该计算节点是否需要重塑。(如果不需要重塑,则该脚本将不执行任何操作。)

实现

负责人

工作项

参见 提议的变更

依赖项

测试

功能测试增强,包括针对 Placement POST /reshaper 的 gabbi 测试。

通过他们的 VGPU 工作,在 Xen (naichuans) 和 libvirt (bauzas) 中进行实时测试。

文档影响

参考资料

历史

修订版

发布名称

描述

Rocky

引入

Stein

重新提出