严格两级限制执行模型

本规范描述了在分层项目结构中,与资源相关的限制的严格执行模型的行为和用例。

bp strict-two-level-model

问题描述

在 Queens 版本中引入的统一限制 规范和实现,忽略了项目结构的所有细节。它的唯一执行模型是 扁平。这意味着与树的任何部分相关的限制都不会相互验证。

提议的变更

本规范详细介绍了严格的两级分层执行模型。

用例

  • 作为操作员,我希望能够设置顶级项目的限制,并确保其使用量永远不超过该限制,从而实现严格的使用

  • 作为负责管理跨项目的限制的用户,我希望能够以足够灵活的方式设置子项目中的限制,以便资源可以在顶级项目下的项目之间流动

这些用例是在关于统一限制的早期 讨论邮件列表中提到的。

模型行为

此模型

  • 要求项目层次结构不超过两层深度,这意味着层次结构仅限于父子关系

  • 要求每个树都有一个父节点,或树根

  • 允许父节点或树根拥有任意数量的子节点

  • 允许配额超额承诺,即聚合配额限制(而非使用量)可能超过父节点的限制。超额承诺和与超额承诺相关的用户体验是严格的两级层次结构的一个重要因素。

  • 不直接解决跨端点共享数据的问题,例如,每个 nova 不会意识到其他 nova 的配额消耗,这意味着用户可以在每个端点消耗全部配额。

此模型在 keystone 中实现限制验证,从而

  • 允许所有子限制的总和超过父节点或树根的限制

  • 不允许子限制超过父限制

  • 假定注册的限制是未指定项目特定覆盖的项目默认值

此模型由 oslo.limit 以以下方式使用

  • 要求负责资源的的服务实现用于 oslo.limit 计算项目树使用量的使用回调

  • 要求每次请求都计算使用量

oslo.limit 库将执行该模型,以确保整个树上的资源使用量总和不能超过父节点设置的资源限制。

此模型被称为 strict-two-level 执行模型。它是 严格的,因为资源在整个树上的使用量永远不能超过父限制。它被认为是一个 两级 模型,因为它仅假定在两层或更少层级的项目层次结构上工作。

执行图

以下图表说明了上述行为,使用名为 ABCD 的项目。假设所讨论的资源是 cores,并且 cores 的默认注册限制是 10。下图中的标签使用 limitusage 的简写表示法,分别为 lu

digraph {
   node [shape=box]

   A -> B;
   A -> C;

   A [label="A (l=20, u=4)"];
   B [label="B (u=0)"];
   C [label="C (u=0)"];
}

从技术上讲,BC 都可以使用最多 8 个 cores,从而导致

digraph {
   node [shape=box]

   A -> B;
   A -> C;

   A [label="A (l=20, u=4)"];
   B [label="B (u=8)", fontcolor = "#00af00"];
   C [label="C (u=8)", fontcolor = "#00af00"];
}

如果 A 尝试声明另外两个 cores,则使用量检查将失败,因为 oslo.limit 将从 keystone 获取层次结构,并使用服务提供的回调来查看 ABC 的每个项目的用法,以查看整个树的用法总和等于由 A.limit 设置的树的限制。

digraph {
   node [shape=box]

   A -> B;
   A -> C;

   A [label="A (l=20, u=6)", fontcolor = "#FF0000"];
   B [label="B (u=8)"];
   C [label="C (u=8)"];
}

尽管树的用法等于限制,我们仍然可以向树中添加子节点

digraph {
   node [shape=box]

   A -> B;
   A -> C;
   A -> D;

   A [label="A (l=20, u=4)"];
   B [label="B (u=8)"];
   C [label="C (u=8)"];
   D [label="D (u=0)", fontcolor = "#00af00"];
}

即使可以创建项目,树上当前的内核使用量也阻止 D 声明任何 cores

digraph {
   node [shape=box]

   A -> B;
   A -> C;
   A -> D;

   A [label="A (l=20, u=4)"];
   B [label="B (u=8)"];
   C [label="C (u=8)"];
   D [label="D (u=2)", fontcolor = "#FF0000"];
}

创建项目 A 的孙子节点是被禁止的,因为它违反了两级层次结构的约束。这是此设计的一个基本约束,因为它提供了一个非常清晰的升级路径。当请求失败是因为树限制已超过时,用户拥有提供支持工单中有意义的上下文所需的所有信息(例如,他们的项目 ID 和父项目 ID)。项目 A 的管理员应该能够相应地重新分配使用量。系统管理员也应该能够做到。在深度超过两层的树结构中提供此信息更加困难,但可以使用单独的模型来实现。

digraph {
   node [shape=box]

   A -> B;
   A -> C;
   C -> D;

   A [label="A (l=20, u=4)"];
   B [label="B (u=8)"];
   C [label="C (u=8)"];
   D [label="D (u=0)", fontcolor = "#FF0000"];
}

授予 B 声明更多内核的能力可以通过为 B 提供 cores 的项目特定覆盖来实现

digraph {
   node [shape=box]

   A -> B;
   A -> C;

   A [label="A (l=20, u=4)"];
   B [label="B (l=12, u=8)", fontcolor = "#00af00"];
   C [label="C (u=8)"];
}

请注意,无论进行此更新,任何后续声明树中更多 cores 的请求都将被禁止,因为该树的用法等于 A 的限制。如果从 AC 释放 cores,则 B 可以声明它们

digraph {
   node [shape=box]

   A -> B;
   A -> C;

   A [label="A (l=20, u=2)", fontcolor = "#00af00"];
   B [label="B (l=12, u=8)"];
   C [label="C (u=6)", fontcolor = "#00af00"];
}
digraph {
   node [shape=box]

   A -> B;
   A -> C;

   A [label="A (l=20, u=2)"];
   B [label="B (l=12, u=12)", fontcolor = "#00af00"];
   C [label="C (u=6)"];
}

虽然 C 仍然在其默认的 10 个 cores 分配下,但它将无法声明更多 cores,因为树的总使用量等于 A 的限制,从而阻止 C 重新获取它拥有的 cores

digraph {
   node [shape=box]

   A -> B;
   A -> C;

   A [label="A (l=20, u=2)"];
   B [label="B (l=12, u=12)"];
   C [label="C (u=8)", fontcolor = "#FF0000"];
}

创建或更新限制超过 A 限制的项目是被禁止的。即使所有 A 下的限制总和可以超过 A 的限制,但总使用量上限为 A.limit。允许子节点拥有大于父节点限制的显式覆盖会导致奇怪的用户体验并具有误导性,因为树的总使用量上限为父节点的限制

digraph {
   node [shape=box]

   A -> B;
   A -> C;

   A [label="A (l=20, u=0)"];
   B [label="B (l=30, u=0)", fontcolor = "#FF0000"];
   C [label="C (u=0)"];
}
digraph {
   node [shape=box]

   A -> B;
   A -> C;
   A -> D;

   A [label="A (l=20, u=0)"];
   B [label="B (u=0)"];
   C [label="C (u=0)"];
   D [label="D (l=30, u=0)", fontcolor = "#FF0000"];
}

最后,我们仍然假设 cores 的默认注册限制是 10,但我们将创建一个限制为 6 的项目 A

digraph {
   node [shape=box]

   A;

   A [label="A (l=6, u=0)", fontcolor = "#00af00"];
}

当我们创建项目 B 时,它是项目 A 的子项目,限制 API 应该确保项目 B 不假定默认值为 10。相反,我们应该遵守父节点的限制,因为没有单个子限制应超过父节点的限制

digraph {
   node [shape=box]

   A -> B;

   A [label="A (l=6, u=0)"];
   B [label="B (l=6, u=0)", fontcolor = "#00af00"];
}

无论在项目 A 下添加多少个子节点,此行为都应保持一致。

digraph {
   node [shape=box]

   A -> B;
   A -> C;
   A -> D;

   A [label="A (l=6, u=0)"];
   B [label="B (l=6, u=0)"];
   C [label="C (l=6, u=0)", fontcolor = "#00af00"];
   D [label="D (l=6, u=0)", fontcolor = "#00af00"];
}

在创建项目时创建限制覆盖似乎适得其反,因为整个注册默认值的目的,但不太可能通过将默认值指定为低于注册默认值来限制父项目。此行为与子限制总和可能超过父限制的要求保持一致,但任何一个子限制不得超过限制。

建议的服务器更改

Keystone 需要将此逻辑封装到新的执行模型中。理想情况下,此执行模型可以从统一限制 API 内部调用,以便在将其写入后端之前验证限制。

如果 keystone 配置为使用 strict-two-level 执行模型,并且 keystone 内当前的项目结构违反了两级项目约束,则 keystone 应该无法启动。在启动时,keystone 将扫描数据库以确保所有项目不超过两层层次结构,并且 keystone.conf [DEFAULT] max_project_tree_depth = 2。如果任一条件失败,keystone 将报告适当的错误消息并拒绝启动。

为了帮助操作员,我们可以开发一个 keystone-manage 命令,以检查部署中项目的层次结构,并在操作员使用新模型部署 keystone 时警告他们。这清楚地向操作员传达了运行时的项目结构。

建议的库更改和使用

oslo.limit 库需要知道何时根据 strict-two-level 模型执行使用量。它可以直接查询限制 API 以获取当前模型

请求: GET /v3/limits/model

响应

  • 200 - 确定

  • 401 - 确定

响应体

{
    "model": {
        "name": "strict-two-level",
        "description": "Strict usage enforcement for parent/child relationships."
     }
}

该库应公开一个用于声明的对象和一个上下文管理器,以便消耗的服务可以在其 API 业务逻辑中进行以下调用

from oslo_limit import limit
LIMIT_ENFORCER = limit.Enforcer()

 def create_foobar(self, context, foobar):

     claim = limit.ProjectClaim('foobars', context.project_id, quantity=1)
     callback = self.get_resource_usage_for_project
     with limit.Enforcer(claim, callback=callback):
         driver.create_foobar(foobar)

在上面的代码示例中,服务构建一个 ProjectClaim 对象,该对象描述了正在消耗的资源和项目。然后,将 claim 传递给 oslo.limit 上下文管理器,并由服务的回调方法补充。服务的回调方法负责计算项目树的资源使用量。oslo.limit 库可以使用上下文对象中的 project_id 从 keystone 获取限制信息,并使用回调计算项目树中的使用量。当实例化上下文管理器或执行 __enter__ 时,将执行项目层次结构的用法检查。默认情况下,退出上下文管理器将验证另一个请求是否超过了使用量,从而防止跨请求的竞争条件。可以使用以下方式显式禁用此功能

from oslo_limit import limit
LIMIT_ENFORCER = limit.Enforcer()

 def create_foobar(self, context, foobar):

     claim = limit.ProjectClaim('foobars', context.project_id, quantity=1)
     callback = self.get_resource_usage_for_project
     with limit.Enforcer(claim, callback=callback, verify=False):
         driver.create_foobar(foobar)

获取项目层次结构

(当前)默认策略阻止具有项目成员角色的用户检索整个项目层次结构。需要层次结构来计算使用量的库必须以项目管理员身份或使用服务用户令牌调用 API。

请求: GET /limits?show_hierarchy=true

请求过滤器

  • show_hierachy - 是否显示层次结构项目限制。

响应

层次结构项目限制列表。

响应代码

  • 200 - 确定

  • 404 - 未找到

响应体

{
    "limits":[
        {
            "id": "c1403b468a9443dcabf7a388234f3f68",
            "service_id": "e02156d4fa704d02ac11de4ddba81044",
            "region_id": null,
            "resource_name": "ram_mb",
            "resource_limit": 20480,
            "project_id": "fba8184f0b8a454da28a80f54d80b869",
            "limits": [
                {
                    "id": "7842e3ff904b48d89191e9b37c2d29af",
                    "project_id": "f7120b7c7efb4c2c8859441eafaa0c0f",
                    "region_id": null,
                    "resource_limit": 10240,
                    "resource_name": "ram_mb",
                    "service_id": "e02156d4fa704d02ac11de4ddba81044"
                },
                {
                    "id": "d2a6ebbc5b0141178c07951a10ff547c",
                    "project_id": "443aed1062884dd38cd3893089c3f109",
                    "region_id": null,
                    "resource_limit": 5120,
                    "resource_name": "ram_mb",
                    "service_id": "e02156d4fa704d02ac11de4ddba81044"
                },
                {
                    "id": "f8b7f4da96854c4cafe3d985acc5110f",
                    "project_id": "ca7e4b4cd7b849febb34f6cc137089d0",
                    "region_id": null,
                    "resource_limit": 2560,
                    "resource_name": "ram_mb",
                    "service_id": "e02156d4fa704d02ac11de4ddba81044"
                }
            ]
        }
    ]
}

以上是给定以下图表的示例响应,其中 ram_mb 的默认注册限制为 2560,适用于 D

digraph {
   node [shape=box]

   A -> B;
   A -> C;
   A -> D;

   A [label="A (l=20480)"];
   B [label="B (l=10240)"];
   C [label="C (l=5120)"];
   D [label="D (l=undefined)"];
}

备选方案

坚持使用扁平执行模型,要求操作员手动实现分层限制知识。

安全影响

通知影响

其他最终用户影响

性能影响

使用量缓存

与扁平执行相比,预计此模型的性能将不佳。导致预期性能损失的主要因素是树的使用量计算。 oslo.limit 库需要计算树中的每个项目的使用量,才能向服务提供有关请求的答案。

一种可能的解决方案来缓解性能问题是并行计算项目的使用量。

限制缓存

其他服务需要向 keystone 发出额外的调用,以检索限制信息,以便进行配额执行。这将为整体 API 调用性能增加一些开销,因为它需要往返 keystone。

值得注意的是,注册限制和项目限制预计不会经常更改。这意味着限制数据可能可以客户端在 oslo.limit 中缓存。但是,这会引入有关限制失效的问题。考虑以下示例

  • 客户端缓存 TTL 设置为从 keystone 获取限制信息的 1 小时

  • 一分钟后,管理员减少了特定项目的 cores 限制

  • 两分钟后,用户发出了一个请求,以在管理员刚刚设置的限制之上创建实例

  • 由于客户端缓存,服务认为项目在其限制范围内,并允许创建实例

  • 当前的使用量与管理员设置的限制不一致,服务在 TTL 过期后的 57 分钟内才会意识到这一点

客户端缓存将是一个非常特定的情况,需要小心处理,因为缓存失效策略将在服务之间分散。一种可能的缓解措施是让客户端缓存和 keystone 共享相同的缓存实例,从而更容易执行缓存失效。相反,这会提高管理员的操作门槛,并需要对底层基础设施进行假设。

在确定我们可以做出这些类型的假设或找到替代的缓存失效解决方案之前,应避免使用客户端缓存,以防止出现上述情况。在检索限制信息时,我们应以准确性为优先。

其他部署者影响

开发人员影响

强制执行库 oslo.limit 应基于 keystone 中实现的强制执行模型实现。

消费组件(例如 nova、neutron、cinder 等)应在未来添加从 keystone 获取配额限制的新方法。

实现

负责人

主要负责人

其他贡献者

工作项

  • 添加新的 API GET /limits/model

  • 将限制验证抽象成模型对象

  • strict-two-level 实现新的限制模型

  • oslo.limit 中实现 strict-two-level 强制执行

  • 为限制添加新的 show_hierachy 参数。

  • 添加 keystone 客户端对限制的支持。

未来工作

端点之间的限制和使用量感知

oslo.limit 和 keystone 服务器可以增强,以利用 etcd(或其他共享数据存储)来表示限制数据和跨端点配额使用量。这超出了本次规范的范围。应该注意的是,该模型应该能够消耗来自任何使用的存储的数据,而不局限于仅本地的数据存储。

优化的使用量计算

在设计此强制执行模型期间,各方提到在使用此模型处理具有许多项目的树时,与性能相关的担忧。例如,计算跨数百或数千个项目的 cores 的使用量。考虑以下树结构

digraph {
   node [shape=box]

   A -> B;
   A -> C;

   A [label="A"];
   B [label="B"];
   C [label="C"];
}

考虑到每个项目不仅具有 usagelimit 的概念,还具有称为 aggregate 的概念。 aggregate 是项目 usage 之和及其所有子项目的 aggregrate 计数之和。

例如,在 C 上声明两个 cores 时,C.usage=2C.aggregate=2。树根 A 在这种情况下也会更新,其中 A.aggregate=2。当随后在 B 上进行声明,将其使用量更新为 B.usage=2 时,使用量计算只需要检查父项目的 aggregate 使用量属性,或者项目树。

这简化了使用量计算过程,只需要查询父项或树根以获取其聚合使用量。与为每个项目查询其使用量并对父项存储的每个聚合结果求和相反。

以下说明了一个更极端的例子

digraph {
   node [shape=box]

   A -> B;
   A -> C;
   B -> D;
   B -> E;

   A [label="A"];
   B [label="B"];
   C [label="C"];
   D [label="D"];
   E [label="E"];
}

假设每个项目都具有 usage=0limit=10。以下可能是一种可能的情况:在 D.usage=4 上声明资源

  • 设置 D.usage=4 AND D.aggregate=4

  • 设置 B.aggregate=4,因为 B.usage=0 当前

  • 设置 A.aggregate=4,因为 A.usage=0 当前

  • 设置 C.usage=6 AND C.aggregate=6

  • 设置 A.aggregate=10,因为 A.usage=0 仍然

  • 设置 E.usage=2 失败

流程的最后一步失败,因为一旦 C 完成其声明,整个树对于 cores 已经达到了限制容量。优点是,为了计算 E 尝试进行声明时的使用量,我们只需要询问 A.aggregate,因为完成的声明会“冒泡”到树中。

请注意,这要求服务跟踪使用量和聚合使用量,如果这是期望的路径,则会提高采用门槛。

依赖项

  • 需要 API 来暴露配置的限制模型(请参阅 bug 1765193

  • 将模型抽象到 keystone 的自身区域,以使限制 CRUD 与强制执行模型隔离

文档影响

  • 必须记录受支持的限制模型和新的强制执行步骤。

参考资料

  • Queens 统一限制 规范

  • 统一限制的高级 描述

  • Rocky PTG 设计会话 etherpad

  • 早期 review 包含模型上下文

  • Adam 的 blog 关于配额