Gitlab为什么花了一个月的时间来消除 PostgreSQL 子事务 ?


自去年 6 月以来,我们注意到 GitLab.com 上的数据库会神秘地停止几分钟,这将导致用户在此期间看到 500 个错误。经过数周的艰苦调查,我们终于发现了造成这种情况的原因:在长事务正在进行时通过SAVEPOINTSQL 查询启动子事务可能会对数据库副本造成严重破坏。以下是发生的情况、我们如何发现问题以及我们采取了什么措施来解决问题。
 
什么是SAVEPOINT?
要理解SubtransControlLock(PostgreSQL 13 将其 重命名为SubtransSLRU),我们首先必须了解子事务在 PostgreSQL 中是如何工作的。在 PostgreSQL 中,事务可以通过BEGIN语句启动,子事务可以通过后续SAVEPOINT查询启动。当事务或子事务需要一个事务 ID(简称 XID)时,通常在客户端修改数据之前, PostgreSQL 会为每个事务分配一个事务 ID(简称 XID)。
例如,假设您经营一家在线商店并且客户下了订单。在订单完成之前,系统需要确保该用户的信用卡帐户存在。在 Rails 中,一个常见的模式是为订单启动一个事务并调用 find_or_create_by。例如:

Order.transaction do
  CreditAccount.transaction(requires_new: true) do
    CreditAccount.find_or_create_by(customer_id: customer.id)
  end
  # Fulfill the order
  # ...
rescue ActiveRecord::RecordNotUnique
  retry
end

如果两个订单大约在同一时间下达,您不希望创建重复帐户导致其中一个订单失败。相反,您希望系统说:“哦,刚刚创建了一个帐户;让我使用它。”
这就是子事务派上用场的地方:requires_new: true 如果应用程序已经在事务中,它告诉 Rails 开始一个新的子事务。上面的代码转换成几个 SQL 调用,如下所示:
--- Start a transaction
BEGIN
SAVEPOINT active_record_1
--- Look up the account
SELECT * FROM credit_accounts WHERE customer_id = 1
--- Insert the account; this may fail due to a duplicate constraint
INSERT INTO credit_accounts (customer_id) VALUES (1)
--- Abort this by rolling back
ROLLBACK TO active_record_1
--- Retry here: Start a new subtransaction
SAVEPOINT active_record_2
--- Find the newly-created account
SELECT * FROM credit_accounts WHERE customer_id = 1
--- Save the data
RELEASE SAVEPOINT active_record_2
COMMIT

在上面的第 7 行INSERT语句,如果客户帐户已经创建,则可能会失败,并且数据库唯一约束将防止重复条目。如果没有第一个SAVEPOINT和ROLLBACK块,整个事务就会失败。通过该子事务,事务可以正常重试并查找现有帐户。

这与长事务有什么关系?
长事务通常是不好的,因为它们会占用连接,但它们会在副本上导致一个微妙的不同问题。在副本上,SAVEPOINT长时间事务期间的单个快照会导致子快照溢出。请记住,在我们有超过 64 个子事务的情况下,这会降低性能。
  
解决办法共有三个选项:

  1. 完全消除SAVEPOINT。
  2. 消除所有长时间运行的事务。
  3. Andrey Borodin 的补丁应用到 PostgreSQL 并增加子事务缓存

我们选择第一个选项是因为可以很容易地删除子事务的大多数用途。我们采取了多种方法
  • 为什么不消除所有长时间运行的事务?

在我们的数据库中,消除所有长时间运行的事务是不切实际的,因为我们认为其中许多是通过数据库自动清理发生的,但我们还不能重现这一点。我们正在对表进行分区并对数据库进行分片,但这是一个比删除所有子事务更耗时的问题。
  • PostgreSQL 补丁怎么样?

尽管我们测试了 Andrey 的 PostgreSQL 补丁,但我们对偏离官方 PostgreSQL 版本感到不舒服。此外,在升级期间维护自定义补丁版本会给我们的基础架构团队增加重大的维护负担。我们的自我管理客户也不会受益,除非他们使用打补丁的数据库。
 
结论
自从删除所有SAVEPOINT查询后,我们再也没有看到以前问题。如果您使用只读副本运行 PostgreSQL,我们强烈建议您也删除所有子事务,直至另行通知。
PostgreSQL 是一个了不起的数据库,它的注释很好的代码使得理解它在不同配置下的局限性成为可能。