为消费者添加世代

https://blueprints.launchpad.net/nova/+spec/add-consumer-generation

当多个进程尝试为给定的消费者分配资源时,已发现潜在的冲突,并且目前无法检测到这些冲突。我们建议在 Placement 的 consumers 表中添加一个世代字段,并为跟踪消费者更新实施与 ResourceProviders 相同的机制。

问题描述

当消费者消耗资源时,发布的分配是该消费者的完整分配集合,并且会覆盖该消费者的任何现有分配。当多个进程正在为消费者进行分配时,存在潜在的竞争条件。例如,Nova 正在创建一个实例,并要求 Neutron 创建所需的网络资源。Neutron 这样做,并创建分配。然后 Nova 声明其为实例提供的资源,并将对其当前分配的理解写入 Placement,从而覆盖 Neutron 的分配。

用例

作为使用 Placement 的服务,我希望知道我正在创建的分配是准确的,并且其他服务不会意外覆盖我创建的分配。

提议的变更

将在 consumers 表中添加一个 generation 列。这将是一个自动递增的整数,就像 resource_providers 表中的 generation 列一样。并且像 resource_providers generation 一样,它 intended 意在对 API 的用户来说是不透明的。此值将包含在 Placement 响应中,这些响应提供消费者的所有数据,并且必须包含在任何更改该消费者的分配的请求中。与 ResourceProviders 的更新一样,如果提供的消费者世代与当前值不匹配,Placement 将拒绝该请求并返回 409 Conflict 响应。由于在许多情况下将不存在现有的消费者记录,因此请求将提供 None 作为消费者世代。

由于这是一个 API 变更,将创建一个新的微版本。任何不支持此新微版本的旧服务将继续工作,但将容易受到上述竞争条件的影响。

注意

这里使用术语“Microversion 1.X”表示将为此新功能添加的微版本。“Microversion 1.X-1”用于指在此新功能添加之前直接使用的微版本。

警告

同时操作相同分配的两个客户端,其中一个使用预世代微版本,这是一个不安全的操作。

为了确保为所有分配记录存在消费者记录,我们将添加一个在线数据迁移,该迁移将查找分配表中所有没有对应于 consumers 表中记录的消费者 UUID,并在 consumers 表中填充具有该 UUID 的记录。因为我们不希望 consumers.project_id 和 consumers.user_id 列为 NULLable,我们将添加两个 CONF 选项,用于指示用于缺失消费者记录的项目和用户外部标识符。

PUT /allocations/{consumer_uuid}

以下部分详细介绍了从 PUT /allocations/{consumer_uuid} 调用中预期的行为。

没有消费者的现有分配记录

没有引用此消费者 UUID 的现有分配记录时,该调用将表现出以下行为

  • Microversion <1.8:始终成功。始终创建消费者记录,并且 CONF.placement.incomplete_consumer_{project|user}_id 的值将用于缺失的项目和用户标识符。将为这个新的消费者记录创建一个世代。

  • Microversion 1.8 - 1.X-1:始终成功,并且消费者记录始终使用请求有效负载中存在的 project_iduser_id 创建。将为这个新的消费者记录创建一个世代

  • Microversion 1.X:请求有效负载需要一个新的 consumer_generation 字段。它需要是 None,以指示调用者期望这是一个新的消费者。将创建一个带有世代的新消费者记录。

现有分配记录,但没有消费者的消费者记录

在这种情况下,存在引用此消费者 UUID 的现有分配记录,但是没有引用消费者 UUID 的消费者记录。这意味着分配记录是在微版本 1.8 之前创建的,并且在线数据迁移创建不完整的消费者记录尚未运行

在这种情况下,该调用将表现出以下行为

  • Microversion <1.8:始终成功。始终创建消费者记录,并且 CONF.placement.incomplete_consumer_{project|user}_id 的值将用于缺失的项目和用户标识符。将为这个新的消费者记录创建一个世代。

  • Microversion 1.8 - 1.X-1:始终成功,并且消费者记录始终使用请求有效负载中存在的 project_iduser_id 创建。将为这个新的消费者记录创建一个世代

  • Microversion 1.X:请求有效负载需要一个新的 consumer_generation 字段。它需要是 None,以指示调用者了解分配是使用旧版本创建的。

现有分配记录,现有消费者的消费者记录

在这种最终情况下,存在现有的消费者记录以及引用消费者的分配记录。分配必须是在或在微版本 1.8 之后创建的,或者创建不完整的消费者记录的在线数据迁移已经运行

在这种情况下,该调用将表现出以下行为

  • Microversion <1.X:始终成功并始终完全覆盖消费者的分配。Placement 服务将在尝试替换分配之前读取消费者的当前世代,并在分配替换事务结束时增加该世代。

  • Microversion 1.X:请求有效负载需要一个新的 consumer_generation 字段。它需要与消费者已知世代的值匹配。Placement 将检查其已知世代是否与给定的世代匹配,如果存在不匹配,则返回 409 Conflict。此外,如果另一个进程同时修改相同消费者的分配,则消费者的世代递增将失败,并返回 409 Conflict,指示发生了并发写入。调用者应重新读取消费者的世代,评估原始分配请求是否仍然有效,如果是,则重新发出分配请求。

POST /allocations

这种创建分配的变体是在微版本 1.13 中引入的,并且需要为参与分配的一个或多个消费者指定项目和用户。

没有消费者的现有分配记录

没有引用此消费者 UUID 的现有分配记录时,该调用将表现出以下行为

  • Microversion 1.13 - 1.X-1:始终成功,并且始终创建消费者记录,因为 project_iduser_id 将始终存在。将为这些新的消费者记录创建一个世代

  • Microversion 1.X:请求有效负载需要一个新的 consumer_generation 字段用于每个消费者分配部分。它需要是 None,以指示调用者期望这是一个新的消费者。

现有分配记录,但没有消费者的消费者记录

没有现有的消费者记录时,但是请求中引用的消费者存在分配记录,这意味着用户之前使用微版本 <1.8 创建了该消费者的分配。

在这种情况下,该调用将表现出以下行为

  • Microversion 1.13 - 1.X-1:始终成功,并且始终创建消费者记录,因为 project_iduser_id 将始终存在。将为这些新的消费者记录创建一个世代

  • Microversion 1.X:请求有效负载需要一个新的 consumer_generation 字段用于每个消费者分配部分。它需要是 None,以指示调用者期望这是一个新的消费者。

现有分配记录,现有消费者的消费者记录

存在现有的消费者记录时,该调用将表现出以下行为

  • Microversion 1.13 - 1.X-1:始终成功,现有的消费者记录将自动增加其世代,没有针对并发更新的保护

  • Microversion 1.X:请求有效负载需要一个新的 consumer_generation 字段用于每个消费者分配部分。它需要等于消费者已知世代的值。Placement 将检查其已知世代是否与给定的世代匹配,如果存在不匹配,则返回 409 Conflict。此外,如果另一个进程同时修改相同消费者的分配,则消费者的世代递增将失败,并返回 409 Conflict,指示发生了并发写入,并且调用者应重新读取消费者的世代并根据需要重试其请求。

DELETE /allocations/{uuid}

DELETE /allocations/{uuid} 没有更改。我们无法找到一种在 DELETE /allocations/{uuid} 调用中提供消费者世代的方法。

需要通过 PUT/POST 使用空分配字典来进行世代安全的删除。

备选方案

我们可以修改处理分配的方式,并允许使用 PATCH 方法以避免意外覆盖其他服务的分配。虽然这也会解决竞争条件,但许多人在 Rocky PTG 的讨论中并不赞成这种方法。

我们考虑将世代添加到 DELETE 上的标头、查询参数和有效负载中,但无法调和不一致性。

数据模型影响

将向 Placement 的 consumers 表中添加一个新的整数 generation 列,并创建相应的迁移脚本。

REST API 影响

  • /resource_providers/{uuid}/allocations - GET 方法将更改为返回消费者的当前世代值。返回的 JSON 将如下所示

    {'resource_provider_generation': GENERATION,
     'allocations':
       CONSUMER_ID_1: {
           # This next line will be added to the response.
           'consumer_generation': CONSUMER1_GENERATION,
           'resources': {
              'DISK_GB': 4,
              'VCPU': 2
           }
       },
       CONSUMER_ID_2: {
           # This next line will be added to the response.
           'consumer_generation': CONSUMER2_GENERATION,
           'resources': {
              'DISK_GB': 6,
              'VCPU': 3
           }
       }
    }
    
  • /allocations/<consumer_id> - GET 方法将在其响应中包含消费者世代

    {
        'allocations': {
            RP_UUID_1: {
                'generation': GENERATION,
                'resources': {
                    'DISK_GB': 4,
                    'VCPU': 2
                }
            },
            RP_UUID_2: {
                'generation': GENERATION,
                'resources': {
                    'DISK_GB': 6,
                    'VCPU': 3
                }
            }
        },
        'project_id': PROJECT_ID,
        'user_id': USER_ID,
        # This next line will be added to the response.
        'consumer_generation': CONSUMER_GENERATION
    }
    

    PUT 方法将更改为需要消费者世代,如果提供的世代与 consumers 表中的当前值不匹配,则返回 409 Conflict。有关预期行为的详细说明,请参见上方。

    除了上述更改之外,我们还将修改 PUT 方法以接受空分配。这将允许类似于 POST 的行为,并促进针对分配的并发更新安全的 DELETE 操作。

  • /allocations - POST 方法接受多个分配,并且模式将在新版本中进行修改,以在与 ‘project_id’ 和 ‘user_id’ 相同的级别添加一个必需的 ‘consumer_generation’ 值

        ... },
        "project_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255
        },
        "user_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 255
        },
        # This section will be added to the schema.
        "consumer_generation": {
            "type": "integer",
            "minimum": 1,
        }
    },
    "required": [
        "allocations",
        "project_id",
        "user_id",
        # This will be a new required field in the POST request
        "consumer_generation"
    ]
    

POST 将更改为需要每个消费者部分的消费者世代。

安全影响

通知影响

其他最终用户影响

性能影响

使用分配的服务将需要更新其代码以在发生冲突时重试分配,或者以其他方式处理分配失败。这可能会对整体性能产生非常小的影响,但在大多数情况下预计可以忽略不计。

其他部署者影响

开发人员影响

与 Placement 交互的服务的开发人员将需要修改他们的代码以指定新的微版本进行分配,并在任何分配创建或删除请求中提供适当的消费者世代。他们还需要添加处理程序代码,以防分配尝试返回冲突。

升级影响

将添加一个新的在线数据迁移钩子,该钩子将确保为引用在 consumers 表中没有相应记录的消费者 UUID 的任何分配创建消费者记录。两个新的 CONF 选项 – CONF.placement.incomplete_consumer_project_idCONF.placement.incomplete_consumer_user_id 将允许部署者设置用于在微版本 1.8 之前创建的分配的缺失消费者记录的项目或用户 UUID。

运行现有的 nova-manage db online_data_migrations CLI 命令将自动运行此在线数据迁移以创建缺失的消费者记录。

实现

负责人

主要负责人

ed-leafe

其他贡献者

cdent jaypipes

工作项

  • generation 列添加到 consumers 表,并创建相应的迁移脚本。

  • 修改所有分配处理程序代码以递增所有更改的消费者世代。

  • 修改输入和输出模式/有效负载以包含世代。

  • 添加世代冲突检查,如果世代不匹配,将返回 409。

  • 添加需要消费者世代用于所有分配的新微版本。

依赖项

测试

将添加功能测试来验证消费者生成值是否正确返回,以及针对该消费者的任何分配是否会更改生成值。它们还将验证具有匹配生成的分配请求是否成功,而不匹配生成的请求是否以 409 Conflict 失败。

文档影响

关于使用消费者生成值的相关信息需要更新 Placement 的开发者文档,并且使用 Placement 的服务应该更新以处理创建分配时收到的 409 响应。

参考资料