无锁配额实现¶
https://blueprints.launchpad.net/nova/+spec/lock-free-quota-management
实现一种无锁配额管理算法,移除数据库 API 中 SELECT FOR UPDATE 的使用。
问题描述¶
在 Nova 启动单个实例时,可能会对 Nova、Neutron、Glance 和/或 Cinder 数据库进行超过 120 次数据库查询。 在这些查询中,很大一部分涉及配额管理任务——检查现有配额、检查现有项目和用户使用记录、声明在预留中使用的资源,以及在启动序列成功或失败后提交或回滚这些预留。
在 Nova 中,SELECT FOR UPDATE SQL 结构在几个地方使用,以确保没有两个并发线程尝试更新数据库中的相同行。 当线程使用 SELECT FOR UPDATE 选择记录时,该线程会声明它打算修改从表中读取的记录——这称为写意图锁。 如果另一个线程想要更新相同的记录集,它将发出 SELECT FOR UPDATE 调用,并且此调用将等待直到第一个线程完成事务并发出 COMMIT 或 ROLLBACK。
在 PostgreSQL 或 MySQL 等传统 RDBMS 系统中,对 SELECT FOR UPDATE 的调用本质上会降低可扩展性,因为在任何给定时间,只有单个线程可以持有表中相同行的写意图锁。 所有其他线程都必须等待单个写入线程完成正在执行的操作。 在 Nova 的情况下,SELECT FOR UPDATE 的使用主要集中在两个领域:配额管理和 nova-network 的 IPAM 层中空闲 IP 地址的分配。
除了 SELECT FOR UPDATE 本身固有的可扩展性问题外,MySQL 的一种流行的复制变体,称为 MySQL Galera,不支持 SELECT FOR UPDATE 所需的写意图锁。 实际上,对于使用 MySQL Galera 的 Nova 部署者来说,这意味着当两个线程同时尝试更改相同的记录集时,MySQL 客户端偶尔会返回死锁错误。 从传统意义上讲,实际上并没有发生死锁,但 Galera 会引发 InnoDB 锁等待超时(已发生死锁)的错误代码,这是在称为认证失败时发生的情况。 认证失败发生在两个线程写入 Galera 集群中的两个不同节点时,尝试在相同的时间间隔内 UPDATE 相同表中的相同记录集时。 与让 MySQL 包含不一致的数据(两个节点对底层数据有不同的看法)相比,Galera 只是导致两个线程都失败,从而发出包含事务的 ROLLBACK。 这与标准 MySQL 的行为不同,在标准 MySQL 中,类似的情况会导致一个线程在收到锁等待超时错误后 ROLLBACK,而另一个线程的 UPDATE 会成功。 造成这种“死锁”实际上不是 Galera 案例中的死锁的原因是,整个过程在没有任何实际等待或超时循环的情况下发生。 每个冲突的线程只是被发送一个错误,并且该线程发出当前 SQL 事务的 ROLLBACK。
由于 MySQL Galera 是目前运营商生态系统中迄今为止最流行的可用性数据库部署选项,因此需要在配额管理代码中进行一些更改,以替换对 SELECT FOR UPDATE 的使用,采用无锁实现,既不会产生可扩展性问题,也不会产生 Galera 特定的准死锁问题。
用例¶
部署者通常使用像 Tempest 或 Rally 这样的系统来提供其 OpenStack 部署的“验收测试”。 这两个系统都通过尝试生成和对许多实例采取操作,使用许多用户和项目来强调 Nova 系统。 这两个测试都可靠地产生错误场景,从 Nova 日志文件中可以看出,部署者可以看到正在使用死锁重试机制来处理 MySQL Galera 集群未能认证配额使用更新记录的情况。 这种死锁重试机制过于粗暴,显然会影响 Nova 的性能和可扩展性。 部署者希望 Nova 日志中没有重试消息,并希望看到整个系统的吞吐量得到改善。
项目优先级¶
这与 Kilo 列出的任何项目优先级无关。
提议的变更¶
建议的解决方案是借鉴无阻塞和无锁算法设计,并使用“比较并交换”方法,允许打算更改用户或项目的配额使用记录的线程发出针对这些记录的标准 SELECT 语句,并且当该线程转而更新这些记录时,它首先检查记录的状态是否与线程先前知道的存在相同。 UPDATE 语句将包含一个 WHERE 条件,以确保仅当当前行值与线程先前在读取行时认为的值相同时,才在表中更新行。 线程将检查 UPDATE 语句影响的行数。 如果受影响的行数为 0,则将命中随机指数退避循环,并且读取然后使用 WHERE 条件 UPDATE 的过程将重复,直到尝试了预定义的次数为止。
该算法是无锁的,因为在配额管理事务的任何时候都不会采取任何类型的记录锁。
备选方案¶
无
数据模型影响¶
无
REST API 影响¶
无
安全影响¶
无
通知影响¶
无
其他最终用户影响¶
无
性能影响¶
我们将与 Rally 开发团队合作,确定一些预先和事后基准测试,这些基准测试应该证明在 MySQL 和 PostgreSQL 部署下,此无锁实现具有更好的并发性。
其他部署者影响¶
无
开发人员影响¶
无
实现¶
我们将完全在 quota_reserve()、quota_rollback() 和 quota_commit() DB API 方法中实现无锁算法。
将从 nova.db.sqlalchemy.api 中 _get_project_user_quota_usages() 方法的查询对象构造中删除 with_lockmode(‘update’) 的使用。 在 quota_reserve()、quota_commit() 和 quota_rollback() 中,我们将从这个简化的 quota_reserve() 伪代码中更改算法
start_transaction:
usage_records = get_and_lock_usage_records()
reservations = []
for resource, amount in requested_resource_changes:
reservation = reservation_record_create(resource, amount)
reservations.append(reservation)
commit_transaction
return reservations
为以下代码
current_usage_records = get_usage_records()
for resource, amount in requested_resource_changes:
while num_attempts < max_attempts:
if usage_records_update(resource, amount,
current_usage_records):
break
num_attempts++
current_usage_records = get_usage_records()
return requested_resource_changes
其中 usage_records_update() 方法看起来像这样,同样,伪代码
def usage_records_update(resource, amount, current_records):
sql = "UPDATE quota_usage SET used = used + amount
WHERE resource = $resource
AND used = $current_records.used"
execute_sql()
return num_affected_rows() > 0
重要的是要注意,上述实现建议删除围绕检索和更新配额使用记录的事务容器。 这是由于需要将每个 UPDATE SQL 语句发生在自己的事务容器中。 否则,REPEATABLE_READ 隔离级别的语义将意味着事务将看不到其他事务的任何更改。
负责人¶
- 主要负责人
AlexFrolov <afrolov@mirantis.com> pkholkin <pkholkin@mirantis.com>
工作项¶
添加一个新的配额驱动程序 nova.quota.ConcurrentDbDriver 类,我们可以使用它来隔离此新功能的测试,并确保现有的配额 DB 驱动程序可以保持默认配额驱动程序,同时完成这项工作。
添加新的 DB API 方法,执行更新单个资源使用记录的操作,该操作将受影响的行数返回给调用者。 UPDATE 语句应构造一个 WHERE 条件,其中包括预期的使用量。
添加一个新的 DB API 方法,该方法返回项目的配额使用记录集,但不调用 lock_mode(‘update’)。
更改新配额 DB 驱动程序的 reserve() 方法,以调用新的 DB API 方法来检索使用记录,循环遍历每个使用记录,为每个资源预留调用新的比较和交换更新使用量 DB API 方法。 该方法应实现一个 while 循环,该循环检测何时未更新使用记录并重试更新,并在读取更新的使用记录信息后。 该方法应跟踪成功更新的记录。 如果在任何资源的任何重试循环中检测到配额超出,则应更新成功更新的使用记录以撤消预留。
对新配额 DB 驱动程序的 commit() 方法进行类似实现。
对新配额 DB 驱动程序的 rollback() 方法进行类似实现。
在开发人员参考文档中提供关于并发配额 DB 驱动程序的可靠参考文档。
依赖项¶
无
测试¶
我们应该添加一个测试,以检查 Nova 日志中死锁重试的发生情况,并验证在此修复之后,我们不再看到任何发生情况。
文档影响¶
无
参考资料¶
关于重试策略的相关邮件列表主题
http://lists.openstack.org/pipermail/openstack-dev/2014-November/050935.html http://lists.openstack.org/pipermail/openstack-dev/2014-November/051300.html