Cinder Volume Active/Active 支持 - API 竞争¶
https://blueprints.launchpad.net/cinder/+spec/cinder-volume-active-active-support
目前 cinder-volume 服务只能以 Active/Passive HA 方式运行。
其中一个原因是 c-api 中非原子状态转换,可能导致竞争条件。
本规范提出了一种使用比较和交换在 Cinder API 中进行原子数据库更改的方法,以防止这些情况发生。
问题描述¶
当前的 Cinder API 服务代码包含一些部分,这些部分首先从数据库中检索内容,然后检查其有效性。如果所有检查都通过,并且操作可以继续,则相应地更改数据库。
检索和数据库更改之间经过的时间取决于验证的性质。在某些情况下,它只是简单地检查“status”字段,而在其他情况下,它需要检查其他表,例如确认卷未附加或没有快照。
API 处理检查和更改的方式为代码竞争的发生创造了机会。当在检索数据库内容之后对数据库进行更改时,就会发生这些竞争。这会导致我们根据过时的数据做出决策,不仅使用过时的引用数据更改数据库,而且在大多数情况下还会错误地调用卷管理器服务来执行操作。
这些 API 竞争的一个例子是 cinder/volume/api.py 中的 extend 方法,我们首先检查状态
if volume[‘status’] != ‘available’
然后稍后更改状态
self.update(context, volume, {‘status’: ‘extending’})
最后,我们发出 RPC 请求来执行操作
self.volume_rpcapi.extend_volume(context, volume, new_size, reservations)
用例¶
操作员希望避免云中的意外行为,此外,由于这些竞争条件可能导致存储后端的数据损坏。而且,这些情况在可以处理更高工作负载的 Active/Active 配置中发生的可能性更大。
提议的变更¶
建议的解决方案是使用比较和交换更新,并在发生死锁时进行重试,以确保仅在满足所有必需条件时才更新数据库。
基本上,比较和交换只是一个复杂的数据库查询,该查询在更新查询中添加了一个条件,以确保仅当满足这些条件时才对数据库进行修改。这些条件可以是简单的条件,例如 status == ‘available’,也可以是引用其他表的更复杂的条件。
之所以选择此解决方案而不是其他替代方案,是因为 性能数据 在测试中表明,即使在多主集群数据库配置中最极端的情况下,它们也能产生出色的性能结果。
我们的目标是使 Cinder API 服务行为尽可能接近原始代码,因此我们将会在相同的条件下失败和成功。即使报告的错误会略有不同,但唯一的区别在于我们之前会遇到竞争条件,并且可能导致数据损坏,而现在 API 调用者会收到肯定响应,而其中一个会收到失败,就像两个操作是顺序执行的一样。
我们报告错误的细微差异源于我们不再知道哪个条件触发了失败。因此,为了提高效率和代码清晰度,我们将返回一个通用的错误,说明某些必需条件未满足,因此无法完成请求的操作。
备选方案¶
一种替代方案是在失败时返回通用错误,该方案由于其复杂性而被拒绝。我们可以从数据库中获取更新的数据,并将其提供给验证方法,该方法将检查哪个条件未满足,并引发带有正确错误消息的正确异常。例如,此方法可以检查卷的 status,如果它不是 available,则引发消息,然后检查该卷是否没有快照,如果有,则引发不同的异常或带有不同消息的相同异常。
过程的框架
result = resource.conditional_update(values, conditions)
if not result:
resource = db.get_resource(resource.id)
raise_right_validation_error(resource)
rpc_call(resource)
但是,这里存在潜在的竞争。我们可能会请求一个条件更新,该更新失败(因为数据库中的条件未满足),当我们从数据库中检索数据时,它刚刚更改(现在将满足更新的条件),因此当我们调用负责引发正确验证错误的该方法时,它将找不到任何无法执行操作的原因,因此它将只返回控制权给调用者。
在这种情况下,由于我们没有引发错误,我们将继续进行 rpc 调用而没有更改数据库,这是一个问题。这就是为什么我们需要循环进行条件更新,以确保我们成功更新数据库或无论发生什么竞争条件都引发错误。
更新后的过程框架
while not resource.conditional_update(values, conditions):
resource = db.get_resource(resource.id)
raise_right_validation_error(resource)
rpc_call(resource)
在此实现中,最好拥有最大重试次数,而不是一直循环直到我们更新数据库或正确引发错误。这样,如果我们更新条件或验证代码中存在错误,我们将不会陷入无限循环。
还探索并放弃了多种比较和交换的替代方案,因为它们效率较低——即速度较慢或需要更多查询数据库。
被放弃的替代方案是
SELECT … FOR UPDATE,并在发生死锁时进行重试,尽管它适用于多主配置,但它比建议的解决方案有更多的死锁重试。
使用具有不同后端的分布式锁定管理器来强制对资源的独占访问。
有关测试的更多信息,请参见 性能数据。
数据模型影响¶
无
REST API 影响¶
无
安全影响¶
无
通知影响¶
无
其他最终用户影响¶
无
性能影响¶
性能影响可以忽略不计,甚至可能获得更好的性能,因为只会对数据库进行一次查询,而不是多次查询。例如,在卷删除时,我们现在有一个查询来查看状态,另一个查询来获取该卷的快照计数。使用新的比较和交换,我们将只有 1 个查询,如果状态正确且不存在快照,则将更新数据库。
其他部署者影响¶
无
开发人员影响¶
对 Cinder API 方法进行更改后,添加的所有新的 API 方法都需要符合这种新的比较和交换方式来更新数据库,以防止新的竞争条件进入代码库。
实现¶
负责人¶
- 主要负责人
Gorka Eguileor (geguileo)
- 其他贡献者
欢迎任何人提供帮助
工作项¶
数据库原子更新方法。
此方法需要某些功能
基本比较:status = ‘available’ status != ‘available’
比较多个值:status in [‘available’, ‘error’] status not in [‘attaching’, ‘detaching’]
处理 None 值,就像 Python 所做的那样:在对非 None 值进行否定比较时,它也应返回 None 值。因此,检查 ‘migration_status’ != ‘migrating’ 也会返回 ‘migration_status’ 设置为 None 的值。
使用其他复杂的过滤器:在某些情况下,例如检查附件,可能需要更复杂的查询。
根据其他字段设置更新值。例如,在分离卷时,如果该卷没有更多的卷附件,则必须将状态设置为“available”,如果还有更多的附件(它是多附件的),则必须将其设置为“in-use”。
根据数据库中的字段设置更新值:previous_status = status, status = ‘retyping’ size = size + 10
版本化对象中的原子更新方法。
除了公开上述数据库条件更新项目提供的功能外,它还需要
自动将资源的 ID 添加到条件更新中。
如果未提供任何条件,则必须假设我们希望数据库记录保持不变。
它必须允许与更新一起保存脏属性。
它必须能够使用写入数据库的数据更新版本化对象,即使是条件值。
更新 Cinder API 服务方法以使用比较和交换。
依赖项¶
没有 openstack 依赖项,但它具有 SQLAlchemy 依赖项,版本为 1.0.10,其中包括参数排序更新 (issue #3541)。
在某些数据库中,更新方法是顺序相关的,因此它们根据值的顺序表现不同,例如在状态为“available”的卷上
UPDATE volumes SET previous_status=status, status=’retyping’ WHERE id=’44f284f9-877d-4fce-9eb4-67a052410054’;
将导致状态为“retyping”且 previous_status 为“available”的卷,在 SQLite 和 MariaDB 上都是如此,但是
UPDATE volumes SET status=’retyping’, previous_status=status WHERE id=’44f284f9-877d-4fce-9eb4-67a052410054’;
将产生 SQLite 中的相同结果,但在 MariaDB 中将导致状态和 previous_status 都设置为“retyping”的卷,这不是我们想要的,因此必须考虑顺序。
测试¶
将为原子更新方法添加单元测试。
文档影响¶
无