新的配额系统¶
https://blueprints.launchpad.net/cinder/+spec/count-resource-to-check-quota
Cinder 配额一直是运维人员和云用户的痛点。本规范提出一个新的配额系统,以解决配额使用跟踪方面的不一致性。
问题描述¶
Cinder 当前的配额系统基于预留和提交/回滚。当 API 接收到消耗配额的操作时,会验证请求并检查 Cinder 是否有足够的配额来执行该操作,然后创建预留以确保该配额不会被其他操作消耗。当操作完成后,这些预留将作为已用资源提交,或者如果操作失败则回滚。
当前资源的使用情况和预留(例如,卷的数量或千兆字节数)在数据库中作为 quota_usages 表中的计数器进行跟踪。因此,如果出于任何原因,此计数器与实际使用情况不匹配,则用户可能无法创建新资源,或者能够创建超出其项目限制的资源。
Cinder 中资源跟踪不同步的原因有很多,包括代码中的错误以及服务在操作期间崩溃。
作为配额不同步的解决方法,Cinder 服务具有代码,可以使预留经过一段时间后过期,并且还可以以一定的频率重新同步配额。
除了对运维人员和用户的影响外,当前的配额实现还会对开发人员产生影响,因为预留/提交和数据库中的资源计数系统缺点在于必须非常彻底才能始终跟踪所有内容,这使得配额系统成为一个非常手动且繁琐的编码过程,从而导致难以找到的错误,因为我们不知道计数出错的点。
当前配额系统的低级实现细节也存在于 Cinder 的所有地方,并且代码的几乎每个区域都需要了解低级实现细节,这使得代码非常冗长,并掩盖了高级逻辑。
例如,我们可以查看传输 API 中文件 cinder/transfer/api.py 中的 accept 方法的代码,在该文件中,截至撰写本文时,构成该方法的 106 行代码中有 70 行与配额相关!
用例¶
云管理员希望防止系统容量在没有通知的情况下耗尽,因此它会根据部署能力限制配额系统。
云管理员希望限制每个部门可以消耗的资源数量。
Cinder 贡献者希望添加一个创建或销毁资源的新功能,因此它需要编写代码来管理配额。
提议的变更¶
本规范提出一个新的配额系统,其中大部分低级配额细节将对开发人员隐藏,从而简化功能开发并降低引入新错误的风险。
此配额系统将支持 2 种不同的驱动程序
StoredQuotaDriver:这将类似于旧系统,使用数据库中的计数器,但不是对每次资源修改都进行预留和提交/回滚,而是仅对真正需要它来跟踪资源的操作进行预留。DynamicQuotaDriver:此驱动程序将不再将使用情况和资源跟踪存储在数据库表 (quota_usages) 中,而是根据数据库中存在的资源动态计算每次配额检查。计算将对资源进行计数(例如,
snapshots)或对大小求和(例如,gigabytes)。与
StoredQuotaDriver驱动程序一样,它将尽可能少地使用预留。
之所以有 2 个驱动程序而不是一个,是因为每个驱动程序都有权衡,默认驱动程序将是 DynamicQuotaDriver 驱动程序,原因如下文 性能影响 部分所述。
这些更改将尽可能地避免过度设计解决方案,重点关注 2 个新驱动程序和当前的 cinder,而不是解决未来可能永远不会出现的不同驱动程序的各种情况。
配额限制¶
在配额类 (quota_classes 数据库表) 的原始实现中,提到了支持不同类别的可能性,除了现有的 default 之外,并且能够通过上下文传递它,但在其实现超过 9 年后,Nova 和 Cinder 都未支持它,因此这个新的配额系统和驱动程序将只关注 default 配额类,它将被称为系统范围默认值、全局默认值、全局限制或只是默认值,因为该术语更容易理解。
来自 quotas 表的限制将被称为每个项目限制。
在考虑全局和每个项目限制后,适用于特定项目的有效配额限制将简单地称为配额限制。
配额限制的计算方式将保持不变
系统范围的配额限制默认值存储在
quota_classes表中,在class_name列中具有default值的记录下。替换系统范围限制的每个项目配额限制是可选的,并将存储在
quotas表中。hard_limit值为-1表示没有限制。
资源¶
新的配额系统不会引入或删除任何现有的配额资源,因此配额限制的可用资源仍然是:volumes、volumes_<volume-type>、snapshots、snapshots_<volume-type>、gigabytes、gigabytes_<volume-type>、backups、backup_gigabytes、groups 和 per_volume_gigabytes。配额使用情况将报告所有现有限制的已用和预留值,除了 per_volume_gigabytes,因为它不能有任何使用情况。
预留值将存储在 reservations 表的 delta 字段中,就像今天一样。
对于 DynamicQuotaDriver,这些值将动态添加,按 resource 对非删除行进行分组,这些行属于特定项目。另一方面,StoredQuotaDriver 将在 quota_usages 表的 reserved 字段中跟踪总和。
两个驱动程序在报告已用值时都将遵守以下规则
volumes和volumes_<volume-type>配额必须与volumes表中非删除行的数量相匹配,use_quota字段设置为true,再加上reservations表中正值的总和,其中resource匹配volumes或volumes_<volume-type>,在两种情况下,仅属于特定项目的值。snapshots和snapshots_<volume-type>配额必须与snapshots表中非删除行的数量相匹配,use_quota字段设置为true,并且属于特定项目。gigabytes和gigabytes_<volume_type>配额必须与可计量卷(如上所述)的size的总和相匹配,当配置选项no_snapshot_gb_quota设置为false(默认值)时,再加上可计量快照的volume_size值的总和,再加上reservations表中正值的总和,其中resource匹配gigabytes或gigabytes_<volume-type>,在两种情况下,仅属于特定项目的值。groups配额必须与groups表中非删除行的数量相匹配。backups配额必须与backups表中非删除行的数量相匹配,并且backup_gigabytes必须与它们的size值之和相匹配。per_volume_gigabytes是不需要任何计算的配额限制。
机制¶
新的配额系统将严重依赖数据库事务和使用 SQL 语句 SELECT FOR UPDATE 进行数据库行锁定,以控制并行操作并确保配额限制得到遵守,并且所有数据库更改都会发生或自动回滚。
这种机制的高层视图是
启动事务
获取当前配额限制,对这些行创建锁定
检查操作是否超出配额
在数据库中创建资源或进行预留
完成事务,释放锁定
锁定只会发生在我们要关注的资源的行上,允许对其他项目和资源的操作并行执行。例如,创建卷的配额检查将锁定 volumes、volumes_<volume-type>、gigabytes 和 gigabytes_<volume-type> 的行,因此 cinder 将能够检查创建备份的配额,因为这只需要 backup 和 backup_gigabytes。
新的系统将利用 Python 上下文管理器功能和 Cinder RequestContext (context.session) 中可用的 Oslo DB 事务上下文提供程序,以促进 Cinder 代码的不同区域之间共享事务/会话。
这将允许开发人员编写更简洁的代码,例如,在创建卷时,Cinder Volume Oslo 对象的 create 方法必须检查它是否可以创建 1 个卷,这将消耗额外的千兆字节,并且卷的大小是否超过允许的最大卷大小,因此代码将如下所示
with self.quota_check(self._context, self.volume_type.id,
vol_gbs=self.size,
vol_qty=1,
vol_size=self.size):
db_volume = db.volume_create(self._context, updates)
quota_check 是 Volume OVO 中的一个属性,它返回一个上下文管理器,以确保配额限制得到遵守。返回的上下文管理器取决于卷是否消耗配额,如果它不消耗配额,则返回一个空操作,如果它消耗配额,则返回配额驱动程序提供的上下文管理器。
配额驱动程序上下文管理器在提供的 context 中启动 DB 会话/事务,因此 volume_create 调用将使用相同的会话来创建卷记录,并且事务将在代码退出上下文管理器时完成,从而确保在创建卷之前,不会检查其他操作的配额。
从开发人员的角度来看,所有这些都将被隐藏,因为在更高的层次上,他们需要做的就是创建 Volume OVO,配额将自动检查。例如,这是创建卷流程中的代码 (cinder/volume/flows/api/create_volume.py)
volume = objects.Volume(context=context, **volume_properties)
volume.create()
为了抽象配额系统实现并隐藏其细节,直接与驱动程序交互的代码将不再使用诸如 gigabytes 和 volumes_<volume-type> 之类的资源名称,而是用于卷和快照上下文管理器检查器的参数是
vol_qty:将在检查器上下文管理器内消耗的卷数量的增量。配额系统内部对此的名称是数据库中的volumes。vol_type_vol_qty:特定卷类型的卷数量增量,将在检查器上下文管理器内消耗。默认值为vol_qty的值,因为这是最常见的情况。此配额系统在数据库中的内部名称为volumes_<volume-type>。vol_gbs:将在检查器上下文管理器内消耗的卷千兆字节数量增量。此配额系统在数据库中的内部名称为gigabytes。vol_type_gbs:特定卷类型的卷千兆字节数量增量,将在检查器上下文管理器内消耗。默认值为vol_gbs,因为这是最常见的情况。此配额系统在数据库中的内部名称为gigabytes_<volume-type>。snap_qty:将在检查器上下文管理器内消耗的快照数量增量。此配额系统在数据库中的内部名称为snapshots。snap_type_qty:特定卷类型的快照数量增量,将在检查器上下文管理器内消耗。默认值为snap_qty的值,因为这是最常见的情况。此配额系统在数据库中的内部名称为snapshots_<volume-type>。snap_gbs:将在检查器上下文管理器内消耗的快照千兆字节数量增量。最终将使用配额系统内部名称gigabytes,如果配置选项no_snapshot_gb_quota设置为false(默认值),或者如果设置为true则将被忽略。snap_type_gbs:特定卷类型的快照千兆字节数量增量,将在检查器上下文管理器内消耗。默认值为snap_gbs的值,因为这是最常见的情况。如果配置选项no_snapshot_gb_quota设置为false,则此配额系统在数据库中的内部名称为gigabytes_<volume_type>,或者如果设置为true则将被忽略。vol_size:创建或扩展卷时的总卷大小。在内部,配额系统使用per_volume_gigabytes配额限制来检查此值。
此更改可能看起来毫无价值,但它具有其价值,因为它抽象了快照和卷共享相同配额大小限制的实现细节,从而提供了
更简洁的代码,因为快照创建或带有快照的卷的传输不需要了解
no_snapshot_gb_quota配置选项。如果将来我们要添加特定于快照的配额限制 -
snapshot_gigabytes和snapshot_gigabytes_<volume-type>- 我们将能够这样做,而不会影响 Cinder 代码的任何部分,除了配额驱动程序本身。
预留¶
对于新的配额系统,预留提交和回滚操作将被分组到一个上下文管理器中,该管理器处理这两种情况。提交和回滚预留对于这两种驱动程序具有不同的含义。
对于 DynamicQuotaDriver,这些是 *noop* 操作,因为检查每次都使用数据库值,并且数据库已经在删除预留的同一事务中被修改。另一方面,StoredQuotaDriver 需要根据操作相应地修改 quota_usages 表中的 in_use 和 reserved 计数器。
如前所述,预留仅对特定操作是必需的,确切地说是在 3 个操作上:扩展、传输和重新类型化。
这些操作中的每一个都有不同原因需要预留
扩展:在操作完成之前,数据库中卷的
size字段必须保持不变,以反映其真实值,但我们需要为gigabytes和gigabytes_<volume_type>配额预留额外的千兆字节,在操作期间,这样我们才不会因其他并发操作而超出配额。如果操作成功完成,卷的大小将增加,并且预留将被提交。传输:在正常情况下,接受传输不需要使用预留,因为我们应该能够在同一事务中检查配额并进行数据库更改以接受传输。不幸的是,SolidFire 和 VMDK 驱动程序需要在传输时对其后端进行一些更改,因此卷服务必须进行驱动程序调用。
我们不能在驱动程序调用完成时锁定数据库,因为它可能需要一些时间,并且我们不想阻止 API 处理其他操作。
这就是为什么在调用驱动程序之前会创建预留,并在接受资源后清除它们的原因。
对于预留,传输对于
StoredQuotaDriver来说很复杂,因为在完成一个传输时,它需要修改 2 个不同的项目。一个用于增加计数器,另一个用于减少计数器,因此更高级别的系统需要对 2 个不同的项目进行 2 个不同的调用,一个带有正数,一个带有负数,并且负数应该忽略配额使用情况和限制。在存储带有快照的卷的预留时,它们必须单独存储,以防在更改
no_snapshot_gb_quota_toggled配置选项后有人重新启动服务,如vol_snap_check_and_reserve_cm方法中详细所述。重新类型化:在进行重新类型化时,API 需要在操作完成之前预留
gigabytes_<dest-volume-type>和volumes_<dest-volume-type>,以及为gigabytes_<source-volume-type和volumes_<source-volume-type>创建负预留。出于以下原因,这会消耗两种类型的卷和千兆字节,直到操作完成
如果重新类型化失败,我们将继续消耗源卷类型上的卷和千兆字节,但是如果我们从操作开始时“释放”了该使用量,我们可能会发现没有足够的配额可用于该卷停留在那里。这是主要原因。
即使重新类型化成功,Cinder 也不知道云管理员设置配额限制的原因,因此在重新类型化开始时释放源千兆字节和卷意味着,如果在重新类型化期间创建了源类型的新的卷,Cinder 将超出该卷类型的配额。
这是唯一可能发生竞争条件的操作,尽管这是一个极端情况。如果我们在添加新的配额限制(全局或每个项目)到卷类型资源(例如
volume_<volume_type>)时,该资源在数据库中没有限制,同时我们正在将卷重新类型化到相同的卷类型,则可能会发生这种情况。这种竞争应该在合理的预期范围内,因为有人可能会认为限制是在重新类型化通过配额检查后才添加的。
在对资源进行操作时,代码流程可能会以意想不到的方式不完成,从而在数据库中留下未清理的预留,例如
Cinder 中的编码错误导致卷处于意外状态。
服务终止。
节点重启或关机。
对于这些情况,新的配额系统将在卷上的 os-reset_status REST API 操作中添加代码,以自动清除卷可能拥有的任何预留,当状态更改时(例如,当卷卡在 extending、retyping 等状态时)。这样,无需等待预留过期,操作员可以通过无需其他 API 调用轻松地进行清理。
在删除卷时,代码也会清除该卷上的任何现有预留。
为了便于清理这些预留,卷的 ID 将用作所有预留的 uuid 字段,而不是创建随机 ID,无论 reservations 表中 resource 字段的值如何。
两个驱动程序将以相同的方式创建预留,以便于在不使使用量数字不同步的情况下切换驱动程序。
更改配置¶
有两个 Cinder 配置选项对于新的配额系统正确运行至关重要:quota_driver 和 no_snapshot_gb_quota。
配置选项 no_snapshot_gb_quota 用于确定是否应将快照计入卷配额,因此我们不希望在某些地方计数,而在其他地方不计数;我们希望在 所有 Cinder 服务中都具有一致的行为,这意味着它们必须具有相同的值。
当前 Cinder 没有办法强制 no_snapshot_gb_quota 具有相同的值,更糟糕的是,它甚至无法知道当前配额计算何时变得无效,因为此配置选项已更改(Bug #1952635)。
这是我们绝对不希望在配额系统中看到的,并且有了新的配额系统,我们面临更大的问题,因为不仅 no_snapshot_gb_quota 可以更改,而且 quota_driver 也可以更改,并且更改配额驱动程序意味着配额系统可能需要重新计算某些内容以确保它从正确的配额假设开始运行。例如,从 DynamicQuotaDriver 更改为 StoredQuotaDriver,数据库中的所有计数器都将不正确,因此 StoredQuotaDriver 需要在开始工作之前计算计数器,否则整个配额系统将无法正常运行。
这些配置选项不是经常更改的类型,并且我们预计大多数部署永远不需要更改它们,但是 Cinder 仍然应该提供一种安全更改它们的方法,因为我们期望的情况之一是部署超出 DynamicQuotaDriver 的实用性并遇到性能问题。在这种情况下,他们将希望切换到 StoredQuotaDriver。
为了支持更改配置选项,新的配额系统需要能够执行 3 件事
检测配置选项的更改。
向驱动程序发出信号,
no_snapshot_gb_quota配置选项已更改,并让驱动程序对该更改做出反应。向驱动程序发出信号,它们不是上次启动时正在运行的配额驱动程序,并且它们应该查看是否需要进行一些计算。
为了检测这些配置选项的更改,将创建一个新的 global_data 表来存储当前使用的配置值。此表将用于向配额驱动程序发出信号,当事情发生变化时。
系统管理员必须遵循以下步骤才能更改这两个配置选项之一
停止所有 Cinder 服务。
在运行 Cinder 服务的所有节点上更改 cinder.conf 文件。
运行 cinder-manage 命令以应用更改的选项。
重新启动 Cinder 服务。
cinder-manage 命令不仅会触发配额系统重新计算,还会对 global_data 表进行适当的数据库更改,以反映生效的新配置选项。
由于我们不能允许 Cinder 服务以配置选项不匹配的状态运行,如果数据库中的配额配置选项与服务拥有的配置选项不匹配,它们将无法启动。这将防止系统管理员犯错,并且只有在整个系统出现疯狂的配额之后才意识到错误。
请参阅 更改配置替代方案,了解其他可能的机制,以替代此处提出的方案。
接口¶
这是新配额系统驱动程序提出的接口
名称¶
标识驱动程序的唯一字符串,最大长度为 254 个 ASCII 字符。
__init__¶
def __init__(self, driver_switched, no_snapshot_gb_quota_toggled):
配额驱动程序的初始化方法,其中 driver_switched 参数指示上次运行是否使用了相同的配额驱动程序,或者是否使用了不同的驱动程序,并且这是使用该驱动程序的第一次运行。
这一点很重要,因为切换到 StoredQuotaDriver 而不是 DynamicQuotaDriver 意味着需要重新计算 in-use 和 reserved 计数器,因为它们可能不同步或完全缺失。
这项工作将仅支持这两种配额驱动程序,并避免不必要的复杂性,因为如果我们要支持其他基于 Cinder 数据库之外的驱动程序,则需要添加更复杂的机制,因为 Cinder 代码需要通知驱动程序何时在数据库中更改了限制,并且需要有一种方法让 Cinder 从旧配额驱动程序请求信息,例如当前预留,在切换时。
如果未来的配额驱动程序发现它不足,可以增强该接口。
参数 no_snapshot_gb_quota_toggled 指示该选项自上次运行以来是否已更改。这对于需要重新计算 in-use 和 reserved 计数器的 StoredQuotaDriver 而言非常重要。目前这部分功能无法正常工作。
驱动程序可以在驱动程序已切换或快照配额配置选项已切换时,在同步时阻塞 Cinder 数据库,因为驱动程序仅会在部署中的单个服务上使用任何参数设置为 True 的情况下被调用,并且此时配额不会被任何其他服务使用。
resync¶
def resync(self, context, project_id):
这仅与 StoredQuotaDriver 相关,旨在允许 cinder-manage 命令请求针对特定项目或整个部署重新计算配额。
set_defaults¶
def set_defaults(self, context, **defaults):
设置系统范围内的默认限制。
关键字参数 defaults 的键采用配额系统的内部形式,也就是说,它们将是 gigabytes 而不是 vol_gbs。
这将是基于数据库的配额驱动程序的常见实现,它会修改记录(如果存在)或创建记录(如果不存在)。
set_limits¶
def set_limits(self, context, project_id=None, **limits):
设置项目特定的限制。
关键字参数 limits 的键采用配额系统的内部形式,也就是说,它们将是 gigabytes 而不是 vol_gbs。
这将是基于数据库的配额驱动程序的常见实现,它会修改记录(如果存在)或创建记录(如果不存在)。
clear_limits_and_defaults_cm¶
def clear_limits_and_defaults_cm(self, context,
project_id=None, type_name=None):
此上下文管理器在退出时删除所有现有的项目级别限制,用于项目删除时,或删除所有类型特定的全局默认值和项目级别限制,用于类型删除时。
参数 project_id 和 type_name 将用作删除的过滤器。因此,如果仅提供 project_id,则仅删除项目级别的条目(在数据库驱动程序中,来自 quotas 表),如果仅提供 type_name,则仅删除 gigabytes_<type-name>、volume_<type-name> 和 snapshots_<type-name> 资源,但针对项目级别(quotas 数据库表)和全局(quota_classes 表)。
如果上下文管理器内部发生错误,则不应清除限制和默认值。
这将是基于数据库的配额驱动程序的常见实现。
type_name_change_cm¶
def type_name_change_cm(self, context, old_name, new_name,
project_id=None):
上下文管理器,用于在进入时对系统范围内的默认值和项目级别限制进行必要的修改,以考虑卷类型的名称更改。
这将把 gigabytes_<old_name>、volume_<old_name> 和 snapshots_<old_name> 重命名为 gigabytes_<new_name>、volume_<new_name> 和 snapshots_<new_name>,在所有表中分别进行。
数据库对卷类型名称的更改在此上下文管理器内调用,以确保配额默认值和限制与卷类型名称保持同步,并且我们不会更改一个而没有更改另一个。
这将是基于数据库的配额驱动程序的常见实现。
get_defaults¶
def get_defaults(self, context, project_id=None):
返回配额限制的系统范围内的默认值。如果 project_id 不为 None,则卷类型配额资源(volumes_<volume-type>、gigabytes_<volume-type> 和 snapshots_<volume-type>)将根据项目对卷类型的可见性进行过滤,如果 project_id 为 None,则将返回所有默认值,而不管卷类型的 is_public 值如何。
就卷类型可见性而言,项目可以查看所有公共卷类型以及具有权限的私有卷类型(在 volume_type_projects 表中的条目)。
系统范围内的默认值存储在数据库的 quota_classes 表中,在 class_name 上具有 default 值。
返回的数据是一个字典,将资源映射到其硬限制,并且必须包含所有卷类型资源,即使数据库中没有记录。
在以下返回数据示例中,gigabytes_lvmdriver-1、volumes_lvmdriver-1 和 snapshots_lvmdriver-1 不存在于数据库中
{
"per_volume_gigabytes": -1,
"volumes": 10,
"gigabytes": 1000,
"snapshots": 10,
"backups": 10,
"backup_gigabytes": 1000,
"groups": 10,
"gigabytes___DEFAULT__": -1,
"volumes___DEFAULT__": -1,
"snapshots___DEFAULT__": -1,
"gigabytes_lvmdriver-1": -1,
"volumes_lvmdriver-1": -1,
"snapshots_lvmdriver-1": -1
}
这将是基于数据库的配额驱动程序的常见实现。
get_limits_and_usage¶
def get_limits_and_usage(self, context, project_id, usages=True):
获取特定项目的有效配额限制,以及可选的配额使用情况,用于特定项目。如果 project_id 为 None,则将使用 context 中的项目 ID。
卷类型配额资源(volumes_<volume-type>、gigabytes_<volume-type> 和 snapshots_<volume-type>)将根据项目对卷类型的可见性进行过滤。
项目可以查看所有公共卷类型以及具有权限的私有卷类型(在 volume_type_projects 表中的条目)。
在 quotas 表中定义的配额限制值会覆盖来自 quota_classes 的全局值。
返回的数据始终是一个字典(或 defaultdict),但内容取决于我们是否获取配额使用情况。与 get_defaults 方法一样,即使数据库中没有记录,此方法也会返回所有卷类型资源。
{
"per_volume_gigabytes": -1,
"volumes": 8,
"gigabytes": 1000,
"snapshots": 10,
"backups": 10,
"backup_gigabytes": 1000,
"groups": 10,
"gigabytes___DEFAULT__": -1,
"volumes___DEFAULT__": -1,
"snapshots___DEFAULT__": -1,
"gigabytes_lvmdriver-1": -1,
"volumes_lvmdriver-1": -1,
"snapshots_lvmdriver-1": -1
}
如果返回配额使用情况,则值如下所示
{
'per_volume_gigabytes': {'limit': -1, 'in_use': 0, 'reserved': 0},
'volumes': {'limit': 8, 'in_use': 1, 'reserved': 0},
'gigabytes': {'limit': 1000, 'in_use': 1, 'reserved': 0},
'snapshots': {'limit': 10, 'in_use': 0, 'reserved': 0},
'backups': {'limit': 10, 'in_use': 0, 'reserved': 0},
'backup_gigabytes': {'limit': 1000, 'in_use': 0, 'reserved': 0},
'groups': {'limit': 10, 'in_use': 0, 'reserved': 0},
'gigabytes___DEFAULT__': {'limit': -1, 'in_use': 0, 'reserved': 0},
'volumes___DEFAULT__': {'limit': -1, 'in_use': 0, 'reserved': 0},
'snapshots___DEFAULT__': {'limit': -1, 'in_use': 0, 'reserved': 0},
'gigabytes_lvmdriver-1': {'limit': -1, 'in_use': 1, 'reserved': 0},
'volumes_lvmdriver-1': {'limit': -1, 'in_use': 1, 'reserved': 0},
'snapshots_lvmdriver-1': {'limit': -1, 'in_use': 0, 'reserved': 0}
}
group_check_cm¶
def group_check_cm(self, context, qty=1, project_id=None):
上下文管理器,用于在进入上下文时检查组配额。
如果添加 qty 个新组后配额使用量将超过配额限制,则引发 QuotaError。
有效配额限制基于项目的配额限制(groups 资源在 quotas 表中的 hard_limit)定义,或者全局默认值(在 quota_classes 表中)确定。
项目由 project_id 参数确定,或者如果可选的 project_id 参数值为 None,则由 context 的 project_id 确定。
上下文管理器必须确保在节点内的不同线程和进程以及跨不同节点之间没有与并发调用 group_check_cm 相关的竞争条件。
对于数据库驱动程序,这可以通过在 groups 配额限制上使用 SELECT FOR UPDATE 来实现,这会阻止其他请求,直到上下文管理器退出。
此上下文管理器的用户应尝试将上下文管理器内的代码保持在最低限度,以允许更高的并发性。
对于 DB 驱动程序,上下文管理器将启动数据库事务/会话,使其在提供的 context 的 session 属性中可用,如果上下文管理器所包含的代码成功完成,则将提交此事务,但如果在所包含的代码中引发异常,则将回滚事务。因此,此上下文管理器不仅检查配额,还提供事务上下文。
在 Group Oslo Versioned 对象 create 方法中使用此上下文管理器的示例
with quota.driver.group_check_cm(self._context, qty=1):
db_groups = db.group_create(self._context,
updates,
group_snapshot_id,
source_group_id)
group_free¶
def group_free(self, context, gbs, qty=1, project_id=None):
上下文管理器,用于在退出上下文时释放组配额。组的数据库行软删除将包含在此调用中。
这仅与需要减少其计数器的 StoredQuotaDriver 相关。
backup_check_cm¶
def backup_check_cm(self, context, gbs, qty=1, project_id=None):
上下文管理器,用于在进入上下文时检查备份配额。
如果添加 qty 个备份或 gbs 备份千兆字节后配额使用量将超过配额限制,则引发 QuotaError。
有效配额限制基于项目的配额限制(backups 和 backup_gigabytes 资源在 quotas 表中的 hard_limit)定义,或者全局默认值(在 quota_classes 表中)确定。
项目由 project_id 参数确定,或者如果可选的 project_id 参数值为 None,则由 context 的 project_id 确定。
上下文管理器必须确保在节点内的不同线程和进程以及跨不同节点之间没有与并发调用 backup_check_cm 相关的竞争条件。
对于数据库驱动程序,这可以通过在 backups 和 backup_gigabytes 配额限制上使用 SELECT FOR UPDATE 来实现,这会阻止其他请求,直到上下文管理器退出。
此上下文管理器的用户应尝试将上下文管理器内的代码保持在最低限度,以允许更高的并发性。
对于 DB 驱动程序,上下文管理器将启动数据库事务/会话,使其在提供的 context 的 session 属性中可用,如果上下文管理器所包含的代码成功完成,则将提交此事务,但如果在所包含的代码中引发异常,则将回滚事务。因此,此上下文管理器不仅检查配额,还提供事务上下文。
在 Backup Oslo Versioned 对象 create 方法中使用此上下文管理器的示例
with quota.driver.backup_check_cm(self._context, qty=1, gbs=self.size):
db_backup = db.backup_create(self._context, updates)
backup_free¶
def backup_free(self, context, gbs, qty=1, project_id=None):
在上下文退出时释放备份配额的管理程序。备份的数据库行软删除将包含在此调用中。
这仅与需要减少其计数器的 StoredQuotaDriver 相关。
vol_snap_check_and_reserve_cm¶
def vol_snap_check_and_reserve_cm(self, context, type_id, type_name=None,
project_id=None,
*,
uuid=None,
vol_gbs=0, vol_qty=0,
vol_type_gbs=None, vol_type_vol_qty=None,
snap_gbs=0, snap_qty=0,
snap_type_gbs=None, snap_type_qty=None,
vol_size=0):
上下文管理器,在进入时检查卷和快照配额,并可选地进行预留。
卷和快照是紧密耦合的资源,因为快照不能没有父卷而存在,所以它们的配额检查在同一个方法中联合处理。
如果在使用提供的资源后配额使用量超过配额限制,则会引发 QuotaError
vol_qty要预留的卷数量。vol_gbs额外的卷千兆字节。vol_type_vol_qty指定卷类型的卷数量。默认为vol_qty的值。vol_type_gbs指定卷类型的额外卷千兆字节。默认为vol_gbs的值。snap_qty快照数量。snap_gbs额外的快照千兆字节。snap_type_qty指定卷类型的快照数量。默认为snap_qty的值。snap_type_gbs指定卷类型的额外快照千兆字节。默认为snap_gbs的值。
与 vol_gbs、vol_type_gbs、snap_gbs 和 snap_type_gbs 参数不同,vol_size 不是对现有消耗的增量,而是表示卷总大小的绝对值。并且,如果它大于 per_volume_gigabytes 限制,则上下文管理器还会引发 QuotaError 异常。
有效的配额限制是根据项目配额限制 volumes、volumes_<volume-type>、snapshots、snapshots_<volume-type>、gigabytes、gigabytes_<volume_type> 和 per_volume_gigabytes 确定,如果定义在 quotas 表中,或者在 quota_classes 表中定义了全局默认值。
项目由 project_id 参数确定,或者如果可选的 project_id 参数值为 None,则由 context 的 project_id 确定。
为了执行配额检查,需要卷类型名称 (type_name),但该方法可以根据 type_id 查询此信息。由于当前的 Cinder 行为(即使项目拥有卷,类型也可以更改为私有),因此配额驱动程序需要确认项目仍然可以访问它。
卷和快照是目前可以进行预留的唯一资源,并且该方法在提供 uuid 时会自动创建它们。此 uuid 必须是操作的主要资源,也就是说,如果我们正在传输带有所有快照的卷,则预留将传递卷的 uuid。
由于 no_snapshot_gb_quota_toggled 配置选项可能会更改并且服务在接受传输之前重新启动,因此两个驱动程序必须使用不同的条目进行卷和快照千兆字节预留,并且 StoredQuotaDriver 需要在重新计算(如果驱动程序已更改)和传输接受时做出决定。
此上下文管理器必须确保在节点内以及不同节点之间,不同线程和进程中的并发调用与 vol_snap_check_and_reserve_cm 没有竞争条件。
对于数据库驱动程序,这可以通过在 volumes、volumes_<volume-type>、snapshots、snapshots_<volume-type>、gigabytes 和 gigabytes_<volume_type> 配额限制上使用 SELECT FOR UPDATE 来实现,这会阻止其他卷和快照请求,直到上下文管理器退出。
此上下文管理器的用户应尝试将上下文管理器内的代码保持在最低限度,以允许更高的并发性。
在创建预留时,上下文管理器必须确保如果在上下文管理器内引发异常,则会清理它们。对于 DB 驱动程序,上下文管理器将启动数据库事务/会话,使其在提供的 context 中可用,并在正常上下文管理器退出时提交所有内容,并在引发异常时回滚所有内容,包括预留。
在 Volume Oslo Versioned Object 的 create 方法中使用此上下文管理器的示例
with self.quota_check(self._context, self.volume_type.id,
volume_type and volume_type.name,
vol_gbs=self.size,
vol_qty=1,
vol_size=self.size):
db_volume = db.volume_create(self._context, updates)
其中 quota_check 是一个属性,它会考虑卷是否使用配额。
@property
def quota_check(self):
if self.get('use_quota', True):
return quota.driver.vol_snap_check_and_reserve
return self.nullcontext
vol_snap_free¶
def vol_snap_free(self, context, type_id, type_name=None, project_id=None,
*,
vol_gbs=0, vol_qty=0,
vol_type_gbs=None, vol_type_vol_qty=None,
snap_gbs=0, snap_qty=0,
snap_type_gbs=None, snap_type_qty=None):
上下文管理器,在上下文退出时释放卷和快照配额。
这仅与需要减少其计数器的 StoredQuotaDriver 相关。
reservations_clean_cm¶
def reservations_clean_cm(self, context, resource_uuid, commit=True):
上下文管理器,在退出时清理给定 uuid 的所有预留,提交或回滚。
uuid 是操作的“主要”uuid,它不会是为每个已预留的资源不同的 uuid。例如,在接受带有快照的卷传输时,所有预留都将使用卷的 uuid。
对于 DynamicQuotaDriver,这主要是从数据库中删除条目,但对于 StoredQuotaDriver,它需要调整 in-use 和 reserved 计数器。
这些计数器可能来自不同的项目,用于卷的传输,因此 context 的 project_id 将被忽略。
DynamicQuotaDriver 驱动程序还必须在提交传输时考虑 no_snapshot_gb_quota_toggled 配置选项,因为快照预留存储在不同的行条目中,以防该选项在接受传输之前更改并且服务重新启动。
差异¶
新系统和旧系统之间存在一些值得强调的差异
本规范的资源部分中声明的资源消耗规则是绝对的,因此,即使由于调度失败或驱动程序在卷服务上的调用失败,卷变为
error状态,也无关紧要。如果存在可计量的数据库记录,则它将计入配额。在重新调整卷类型时创建的负预留不会计入使用量计算,因为如前所述,我们希望
volumes_<volume-type>和gigabytes_<volume-type>源类型仍然被消耗,因为我们不知道是否会成功,如果失败,我们需要再次消耗它们。新的配额系统放弃了 Cinder 可以支持多个 ORM 系统的幻想,并接受了 Cinder 与 SQLAlchemy 和 MySQL/InnoDB 紧密耦合的事实(这并非新事物,已经有一个补丁提出要删除中间层,因此,所有配额代码都不会在
cinder/db/sqlalchemy/api.py中,而将在cinder/quota下,包括所有数据库查询。这种方法缺点是在多个地方有 DB 代码,可能存在代码重复,但另一方面,它具有将配额代码包含在更少的文件中并使用更少的内存的优点(当前标准配额驱动程序文件始终加载,即使未实例化)。
所有部署都将使用默认配额类,而不是支持已经弃用的配置文件配额限制。
新的配额系统修复了许多现有错误,因此存在一些不希望的行为会发生变化
现在列出配额限制和配额使用量不会显示项目无法访问的私有类型(bug #1576717)。
如果项目拥有我们不再可以访问的类型(因为它在资源创建后被设为私有),则会显示 0 的限制(相关 bug #1952456)。这也会发生在管理员为项目创建私有类型的卷时,而项目无法访问该卷。
限制¶
规范针对这两个驱动程序,因此添加其他驱动程序可能不容易。但如果这两个驱动程序正常工作,则不应该有问题。
在并发代码执行中存在瓶颈,因为代码锁定在系统范围内的默认值上,这些默认值对所有项目都是通用的。因此,即使由检查上下文管理器包围的关键部分代码非常小,它仍然会限制每次部署的给定配额限制只有一个上下文进入。
在预留部分中解释的 retype 操作上的竞争条件。
备选方案¶
充分利用我们所拥有的¶
一些替代方案包括
仔细检查 Cinder 代码,查找潜在的错误原因并修复它们。
重构现有的配额代码,将部分 Quota Python 逻辑移动到数据库查询中。
重构现有代码,以减少配额系统实现细节在整个代码中的溢出,并减少仅在严格必要的情况下使用预留。
这些替代方案与当前实现具有相同的基础问题,即很难确定我们是否解决了所有问题,并且在部署中遇到另一个不同步案例时,我们将再次处于无法确定如何到达该点的境地。
统一限制¶
另一种替代方案是使用 KeyStone 统一限制。乍一看,这可能看起来是一个完美的解决方案,因为它
允许在所有 OpenStack 中使用统一的限制系统(一旦所有项目都实施了它)。
支持不同的实施模型,包括层次结构。
但仔细检查后,它并非没有缺点
虽然 Glance 和 Nova 在 Yoga 版本中实施了它的使用,但由于用户没有足够的时间来评估它,因此它仍然不能被认为是经过验证的解决方案。
统一限制系统没有防止并发操作之间的竞争条件的机制。因此,我们需要实施自己的机制,该机制需要在所有 Cinder 服务中工作。可以使用 DLM、一些数据库锁定,或者 Nova 将要使用的方式,即检查限制、提交声明,然后再次检查限制,并在超过使用量时回滚。Nova 机制意味着我们始终进行双重检查,有时进行回滚,并且由于两次检查上的双重竞争条件(两个并发请求通过初始检查,然后在确认检查中都失败,而单独的请求中的一个本来会成功),我们甚至可能获得错误的失败。
如果未在 KeyStone 中先前注册限制,oslo.limit 项目将失败限制检查,这与我们当前的配额系统行为相反,因为它假定无限制(-1)。这意味着 Cinder 将不得不管理在创建或销毁卷类型、向项目授予对卷类型的访问权限、卷类型的公共状态更改等时限制的注册,或者强制操作员自行管理所有这些。更合理的替代方案是修改 oslo.limit 项目以支持对未定义值使用替代行为。
由于我们需要调用外部服务 KeyStone 进行限制检查,而 KeyStone 需要检查发起调用的用户、访问数据库等,因此速度会变慢。并且,每个被检查的资源 都需要向 KeyStone 发送自己的 REST API 调用
可以在 Keystone 中改进,以允许同时进行多个检查。
使用严格的两级强制机制的层次支持 尚未在 oslo.limit 中实现
修复瓶颈¶
如前所述,在提议的配额系统中,由于 DB 锁,并发代码执行存在瓶颈,因为它使用 quotas 表中的条目进行锁定,而这些条目在所有项目之间共享。
为了解决使用 DB 锁造成的此瓶颈,我们需要复制系统范围内的默认值。这些条目可以复制到 quotas 表或 quota_classes 表中。
如果它们复制到 quotas 表中,则需要添加一列 (is_default) 来标记内容是默认值还是非默认值。因为当 quota_classes 表中的全局默认值发生更改时,需要更改 quotas 表中具有默认值的记录,而无需更改具有相同值的显式设置的记录。
如果使用 quota_classes 表,则我们将 project_id 存储到 name 列中,这意味着如果将来我们想完全实现配额类概念,将会遇到问题。尽管考虑到该表创建的时间以及该概念从未实现,这种情况不太可能发生。
在设置全局默认配额限制时,我们需要删除对 name 列的限制,即在使用 quota_classes 时该列不能为 default。如果使用 quotas 表,则除了对 default quota_classes 记录的查询之外,还需要进行额外的查询,因为我们需要更新 quotas 表中具有 is_default 值为 true 的非删除记录。
另一个需要考虑的是,Cinder 事先不知道存在哪些项目,因此在首次对项目进行操作时,如果它们不存在,它还需要动态复制全局默认配额记录。这可以有效地完成,只需在第一次请求项目时进行额外的查询并进行锁定,并假设值存在,只有在结果缺失时才复制全局默认值。
这种动态复制也很棘手,因为我们不希望与全局配额限制更新请求或其他触发相同复制的操作发生冲突。可以使用 SELECT ... FOR UPDATE 在 quota_classes 表上防止这两种冲突。
在撰写本文时,我们希望瓶颈并不重要到需要付出额外的努力来消除它。如果时间证明我们错了,我们可以实施这些或其他解决方案。
更改配置选项¶
更改配置 部分的 提议的更改 部分介绍了选择的机制来更改 quota_driver 和 no_snapshot_gb_quota 配置选项,但这些并不是唯一的可能性。
本节介绍了 2 种更改选项并确保 所有 Cinder 服务都使用相同的配置选项值。
构建一个复杂的系统来协调正在运行的服务上的更改:向所有 Cinder 服务发出更改信号,并确保它们在发出信号给配额驱动程序需要进行重新计算之前完成正在进行的配额操作,然后向服务发出信号,它们必须重新加载配额驱动程序,最后继续其操作。
实施起来非常复杂,首先是确保没有服务错过通知有多困难:服务可能与 RabbitMQ 或 DB 暂时失去连接。
我们还面临着暂停所有正在运行的操作的困难等问题。
仅允许在所有服务关闭时更改配置选项。Cinder 服务足够智能,可以在启动时检测到配置已更改,并确认它是当前正在运行的唯一服务,然后才能继续告诉配额驱动程序需要进行重新计算。
当尝试使 Cinder 足够智能以检测没有其他服务运行时,我们遇到了多个挑战。
我们无法知道 Cinder API 服务是否正在运行,因为它们不会发出 DB 心跳,也不会通过 RabbitMQ 接收任何 RPC 调用。我们可以让它们发出 DB 心跳,甚至可以让他们在 RabbitMQ 消息队列上列出。
当同时启动所有 Cinder 服务时,它们需要避免争夺告诉驱动程序重新计算配额。这可以通过锁定更新 DB 行来解决,以防止竞争,并仅允许一个服务进行计算。
即使所有服务都已关闭,服务的 DB 心跳也不会在一段时间后超时,因此 Cinder 将不得不等到这些心跳超时。这给 Cinder 重启引入了不必要的延迟。
可能发生的瞬时/临时网络问题可能会导致 Cinder 认为没有正在运行的服务。这实际上是最大的问题。
由于更改配额配置选项不是经常更改的事情,我们预计最多只会更改一次,将正确执行的责任交给系统管理员似乎是最好的选择。
无论如何,这并不是一成不变的。如果此行为成为一个真正的问题,我们可以在将来进行更改。
数据模型影响¶
新的配额系统将更加重视数据库查询并减少 Python 代码,因此数据库需要准备好有效地执行计数查询。
确保效率的主要更改是为资源表添加适当的索引。我们需要拥有的索引是 project_id 和 deleted 列的索引,用于以下表(当前没有这些索引):
volumessnapshotsbackupsgroups
这些更改将为 Cinder 服务带来额外的好处,因为目前在具有许多项目或许多已删除资源的部署上列出资源效率不高,因为数据库必须遍历所有资源才能过滤掉属于特定项目的非删除行 (bug #1952443)。
现有的 quota_usages 表将不再使用,并且将在新配额系统推出后的下一个版本中删除。
reservations 表将继续使用,尽管仅用于几个操作。
此外,我们需要跟踪当前正在使用的配额驱动程序以及 no_snapshot_gb_quota 配置选项,以便能够告诉配额驱动程序它们是否已更改。
为此,建议创建一个新的表来存储全局 Cinder 信息。
表 global_data 将具有以下字段:
created_at:创建此键值对的时间updated_at:上次更新此键值对的时间key:描述该值的字符串值。例如no_snapshot_gb_quota或quota_driver。value:键的值的字符串。例如true或StoredQuotaDriver。
REST API 影响¶
不会有 REST API 影响,因为我们当前仅通过列出 API(quota_usages 表)公开使用情况和预留,我们仍然可以使用当前的配额驱动程序接口提供该 API,并且我们仍然有预留(即使使用的操作更少),因此这些信息仍然相关,我们不需要从响应中删除它们。
安全影响¶
无。
Active/Active HA 影响¶
无。
通知影响¶
没有通知影响,因为没有添加或删除任何新操作。
其他最终用户影响¶
Cinder 服务的最终用户不应受到重大影响,除了配额的计算方式之外。
当前行为在配额计算方式上可能不稳定,因为它将取决于事情失败的方式和位置,因此我们可能会有处于 ERROR 状态的卷被计入配额,而其他卷则没有。
有了这种新的方法,配额消耗规则将非常简单明了,用户/管理员只需要列出资源并将所有具有设置为 consumes_quota 字段的资源 true 的资源相加,即可检查使用情况是否正确。
这种稳定的行为,无论服务如何关闭,都可以对部署产生积极影响,因为最终用户将无法超出其允许的配额并被迫清理失败的资源,而是可以放任它们。
性能影响¶
为了评估不同的配额驱动程序,对卷创建和获取使用情况操作进行了初步的代码原型设计:旧的、新的 StoredQuotaDriver 和新的 DynamicQuotaDriver。
结果表明,新的 StoredQuotaDriver 系统在这两个操作中比旧代码快两倍,而 DynamicQuotaDriver 比 StoredQuotaDriver 慢,正如预期的那样,但直到每个项目有大约 26000 个资源时才比旧代码更快。
因此,DynamicQuotaDriver 不太可能与现实不同步,因为它不存储固定值,但 StoredQuotaDriver 具有更好的性能,这就是为什么将实施这两个驱动程序的原因,以便让系统管理员决定哪个更适合他们。
默认驱动程序将是 DynamicQuotaDriver,以优先考虑使用值始终保持同步,而大型部署或寻求最佳性能的部署可以使用 StoredQuotaDriver。
部署甚至可以从一个配额系统开始,然后在必要时切换到另一个系统。
其他部署者影响¶
一旦部署了新代码并执行,将使用新的配额系统,将不会对旧配额代码提供向后兼容性支持。
使用自定义外部配额驱动程序的部署将无法启动。这不应该成为问题,因为我们认为没有人使用自定义驱动程序。
在滚动升级期间,配额系统将比平时更脆弱,用户可能会超出配额。
新的配额系统将不再具有内部的配额清理机制,将使用卷状态更改 API 来清理预留,并且
cinder-manage quota sync命令将用于StoredQuotaDriver,因此以下配置选项将被弃用,并且将不再生效:reservation_expire、reservation_clean_interval、until_refresh和max_age。配置选项
use_default_quota_class将被弃用,因为所有部署都将使用默认配额类,而不是支持已弃用的配置文件配额限制 (相关 bug #1609937)。
开发人员影响¶
对于 Cinder 开发人员来说,应该会有积极的影响,因为代码应该更易于阅读,没有配额代码干扰高级逻辑,并且添加新代码不需要手动触摸配额。
新代码可能会破坏 cinderlib 项目,因此项目也需要进行更改。
实现¶
负责人¶
- 主要负责人
Gorka Eguileor (geguileo)
- 其他贡献者
Rajat Dhasmana (whoami-rajat)
工作项¶
正如在 PTG/年中周期讨论的那样,这项工作可能会分为 2 个阶段,这些阶段可能在不同的版本中实施
阶段 1:DynamicQuotaDriver¶
弃用配置选项并记录使用自定义配额驱动程序的部署的警告。
将所需的索引添加到
volumes、snapshots、backups和groups表。将缺失的
backup和backup_gigabytes默认配额限制添加到quota_classes表。从
quota_classes、quotas、quota_usages和reservations表中删除已弃用的consistencygroups资源。编写
DynamicQuotaDriver数据库配额驱动程序。使以下操作使用新的配额驱动程序
创建卷
删除卷
管理卷
扩展卷
重新调整卷类型
转移卷
创建快照
删除快照
管理快照
备份创建
备份恢复
组创建
组删除
删除旧配额驱动程序的代码。
使
cinder-manage quota sync和check命令变为noop。编写
DynamicQuotaDriver数据库配额驱动程序的单元测试。更新现有的单元测试。
编写初始文档,并说明未来将推出更高效的驱动程序。
阶段 2:StoredQuotaDriver¶
编写
StoredQuotaDriver数据库配额驱动程序。编写
StoredQuotaDriver数据库配额驱动程序的单元测试。更新
cinder-manage quota sync和check命令。添加
cinder-manage quota change命令。对旧的和新的配额系统进行基本的性能手动比较。
使用一个 *noop* 配额驱动程序,并将其作为
quota_driver配置选项的默认值,为cinderlib添加对新配额系统的支持。更新文档。
依赖项¶
由于数据库引擎无法锁定不存在的行,因此新代码需要数据库为
quota_classes表中的所有基本资源保留默认配额限制记录。因此,此新代码依赖于我们确保backups和backup_gigabytes记录存在于数据库中,并且我们还应该删除consistencygroups,因为它们已经很长时间没有被使用了(bug #1952420)。
测试¶
除了将执行一些手动测试以对旧的和新的配额系统进行基本的性能比较之外,大部分测试将集中在 SQL 查询的测试上。
当前支持的数据库引擎是 InnoDB 和 SQLite,后者有一些限制和怪癖,可能会使测试某些查询变得困难或不可能,因此某些单元测试将在 SQLite 上跳过。
我们可能会探索运行一个 tempest 作业,该作业在完成 tempest 运行后检查配额使用情况,并报告是否已不同步的可能性。
文档影响¶
Cinder 配额文档将更新,以反映现在将如何跟踪资源,包含不同配额驱动程序的描述和用例,以及更改配额驱动程序和 no_snapshot_gb_quota_toggled 配置选项的程序。
参考资料¶
在 Yoga PTG 中,大家一致认为配额系统在滚动升级期间可能会出现故障。
相关 bug
备份创建配额警告在日志中:bug #1952420。
删除
default_quota_class配置选项:bug #1609937在没有在
quota_classes中具有值的类型上创建卷和快照时出现警告:bug #1435807。资源列表效率低下:bug #1952443
显示私有类型的配额:bug #1576717。
私有卷类型的限制不正确:bug #1952456
更改
no_snapshot_gb_quota时的使用不正确:bug #1952635