防止竞态条件的机制

https://blueprints.launchpad.net/manila/+spec/eliminate-race-conditions

本提案旨在开发一种通用的防止竞态条件的解决方案,该方案适用于所有服务,以及在具有相同服务多个副本的部署中(通常称为主动/主动 HA 部署)。

重点是将所有状态保存在数据库中,并使用短暂持有的锁来保护数据库状态的更改。 此外,互斥的并发操作应尽早以有用的错误代码失败,以简化上层逻辑的重试逻辑。

问题描述

在 Manila 中,某些操作不应并行进行,因为一个操作的结果会阻止另一个操作成功完成。

例如,对共享快照的获取不能与删除该共享同时发生。 快照必须先发生,从而阻止删除 – 或者删除必须先发生,从而阻止快照。 不幸的是,数据库中存储的状态不足以防止这些操作相互竞争,因此在实践中,两个 API 调用都可以通过 API 服务到达共享管理器,最终会发生错误,一个或两个操作都会神秘地失败。

存在多种类似上述情况,导致未定义行为。 本规范不尝试枚举所有这些情况,因为目标是描述一种修复这些类型问题的机制,而不是显式修复所有此类问题。 一般来说,竞态条件应被视为错误,但到目前为止,Manila 缺乏可靠地修复这些错误的工具。

用例

具体案例

  • 两个快照操作不能同时发生。 一个必须在第二个开始之前完成。 这确保了快照以已知的顺序发生,并防止了拥有 2 个相同快照的无用情况。

  • 对共享进行快照应阻止删除该共享。

  • 无论现有访问规则的状态如何,对访问规则的有效更改应始终被接受。 虽然规则是异步地应用于后端,但以比系统可以应用它们的速度更快地添加多个规则,并期望 Manila 赶上进度是有效的。

一般案例

  • 这些保证必须可以使用以集群配置运行的数据库来强制执行,例如 Galera。 这可以防止显而易见的解决方案,例如依赖于 DB 行级锁定。

  • 这些保证必须在运行多个 Manila 服务副本时强制执行,包括多个 API、调度器和共享管理器服务。 这可以防止显而易见的解决方案,例如进程内锁定。

  • 这些保证必须在运行分布式配置时强制执行,其中协同服务位于不同的节点上(物理机、虚拟机或容器)。 这可以防止我们使用文件锁的现有方法。 即使存在基于网络的基于文件锁解决方案,它们也代表了单点故障,并且在正确分布的环境中不可接受。

  • Manila 服务应能够自动且优雅地从崩溃和其他计划外停机中恢复。 这意味着应避免隐式状态,因为长时间持有的锁是隐式状态,应避免使用它们,而应使用短暂持有的锁和显式状态。

提议的变更

将添加更多过渡状态,以便可以显式地通过查看共享状态来检测可能与其他操作冲突的操作。 本规范仅提出一个特定的新状态来解决涉及快照的竞争,但更一般地提供了一个框架来解决发现的类似竞争。

状态之间的转换始终在持有分布式锁(由分布式锁管理器 (DLM) 实现的锁)的情况下完成。 使用分布式锁可确保所有服务都看到相同的锁定状态,即使服务在不同的节点上运行,甚至在节点故障和网络分区等瞬态故障中也是如此。 锁仅在数据库测试和设置操作期间持有,以最大程度地减少锁争用。

在共享管理器到共享驱动程序的调用期间,不会持有任何锁。 驱动程序调用之间的互斥将通过状态检查来实现。

在 RPC 调用或广播中不会持有任何锁。

备选方案

Cinder 使用的依赖于复杂的 SQL 调用来比较和交换字段的方法已被考虑过,但由于以下原因而被拒绝

  • Cinder 中的代码无法与 Manila 共享,因为它依赖于 OVO(Oslo Versioned Objects)

  • 了解其工作原理的人数不足,因此维护起来可能很困难。

  • Cinder 的比较和交换方法限制了可以进行的状态更改类型,因为原子更新多个表是不可能的。 锁不受此限制。

数据模型影响

将添加新的状态

digraph share_states { label="共享状态" // Transitional States creating[shape=hexagon]; manage_starting[shape=hexagon]; deleting[shape=hexagon]; snapshotting[shape=hexagon,color=gold4, fontcolor=gold4]; migrating[shape=hexagon]; shrinking[shape=hexagon]; extending[shape=hexagon]; unmanage_starting[shape=hexagon]; replication_change[shape=hexagon]; // Error states error[color=red4, fontcolor=red4]; shrinking_error[color=red4, fontcolor=red4]; shrinking_possible_data_loss_error[color=red4, fontcolor=red4]; extending_error[color=red4, fontcolor=red4]; unmanage_error[color=red4, fontcolor=red4]; manage_error[color=red4, fontcolor=red4]; error_deleting[color=red4, fontcolor=red4]; // Other states new[color=blue, fontcolor=blue]; available[color=darkgreen, fontcolor=darkgreen]; deleted[shape=box, color=navy, fontcolor=navy]; unmanaged[shape=box, color=navy, fontcolor=navy]; // User requested transitions new -> creating[label="创建"]; new -> manage_starting[label="管理"]; available -> deleting[label="删除"]; available -> snapshotting[label="创建快照", color=gold4, fontcolor=gold4]; available -> migrating[label="迁移"]; available -> shrinking[label="缩小"]; available -> extending[label="扩展"]; available -> unmanage_starting[label="取消管理"]; available -> replication_change[label="添加副本"]; // Automatic transitions creating -> available[label="成功", color=darkgreen, fontcolor=darkgreen]; deleting -> deleted[label="成功", color=darkgreen, fontcolor=darkgreen]; snapshotting -> available[label="成功", color=darkgreen, fontcolor=darkgreen]; manage_starting -> available[label="成功", color=darkgreen, fontcolor=darkgreen]; unmanage_starting -> unmanaged[label="成功", color=darkgreen, fontcolor=darkgreen]; extending -> available[label="成功", color=darkgreen, fontcolor=darkgreen]; shrinking -> available[label="成功", color=darkgreen, fontcolor=darkgreen]; replication_change -> available[label="成功", color=darkgreen, fontcolor=darkgreen]; // Reset transitions error -> available[label="重置"]; shrinking_error -> available[label="重置"]; extending_error -> available[label="重置"]; unmanage_error -> available[label="重置"]; manage_error -> available[label="重置"]; error_deleting -> available[label="重置"]; // Error transitions creating -> error[label="失败", color=red4, fontcolor=red4]; migrating -> error[label="失败", color=red4, fontcolor=red4]; shrinking -> shrinking_error[label="失败", color=red4, fontcolor=red4]; shrinking -> shrinking_possible_data_loss_error[label="失败", color=red4, fontcolor=red4]; extending -> extending_error[label="失败", color=red4, fontcolor=red4]; unmanage_starting -> unmanage_error[label="失败", color=red4, fontcolor=red4]; manage_starting -> manage_error[label="失败", color=red4, fontcolor=red4]; snapshotting -> error[label="失败", color=red4, fontcolor=red4]; deleting -> error_deleting[label="失败", color=red4, fontcolor=red4]; }

REST API 影响

新的状态将通过任何显示状态的 API 可见。 此外,我们将检测竞争更早并直接报告它们,因此新的错误条件将成为可能。

与锁定相关的行为更改将不会进行微版本控制,因为在实施更改后,模拟旧行为将是不可能的或不可取的。 但是,如果添加了新的状态,这些更改将进行微版本控制,以便依赖于新状态的客户端可以检测到服务器支持它们。

驱动程序影响

安全影响

通知影响

其他最终用户影响

性能影响

分布式锁定预计会适度减慢状态更改的速度。 此外,添加更多状态更改会减慢需要它们的运算的速度。

其他部署者影响

部署和配置合适的 Tooz 后端的必要性。 由于 Manila 将依赖于 tooz 以确保正确性,因此未能满足 API 合同的 tooz 后端将不适用。

开发人员影响

这将非常重要。 开发人员需要遵循新的模型来处理涉及状态更改的所有功能。 此外,需要小心使用锁来避免死锁情况。 限制锁的持有时间将有助于避免死锁,但在同时持有 2 个锁的情况下,需要通过建立锁顺序来防止死锁。

实现

负责人

bswartz

工作项

  • 添加快照状态

  • 完成 tooz 集成

  • 使用 tooz 锁包装状态更改

依赖项

Tooz

测试

现有测试将有助于确保没有回归,但要检测竞态条件,我们需要 rally 测试或类似的高并发测试。

文档影响

管理员指南 - 需要记录 tooz 要求。

开发人员参考 - 需要记录状态机和锁定协议

参考资料

访问规则规范: https://review.openstack.org/#/c/399049/

Tooz 集成: https://review.openstack.org/#/c/318336/