首页 > 解决方案 > Sidekiq 幂等性、N+1 查询和死锁

问题描述

在 Sidekiq wiki 中,它谈到了作业需要具有幂等性和事务性。从概念上讲,这对我来说很有意义,而且这个 SO 答案看起来像是一种小规模的有效方法。但这并不完美。作业可能会在运行过程中消失。我们注意到某些工作是不完整的,当我们查看日志时,他们会在工作中途中断,就好像工作刚刚蒸发一样。可能是由于服务器重新启动或其他原因,但它通常找不到回到队列中的方式。super_fetch试图解决这个问题,但它在重复工作方面犯了错误。有了这个,我们看到很多工作最终会同时运行两次。如果两个事务同时开始,拥有一个数据库事务并不能保护我们免于重复工作。我们需要锁定来防止这种情况。

但是,除了事务之外,当我们想要批量做事情时,我还没有找到一个优雅的解决方案。例如,假设我需要发送 1000 封电子邮件。我能想到的选项:

  1. 生成 1000 个工作,每个工作单独启动事务、更新记录并发送电子邮件。这似乎是默认的,并且在幂等性方面相当不错。但它具有创建分布式 N+1 查询、向数据库发送垃圾邮件并导致用户面临速度变慢和超时的副作用。

  2. 在一笔大交易中处理所有电子邮件,并接受电子邮件可能会发送多次或根本不发送,具体取决于结构。例如:

    User.transaction do
      users.update_all(email_sent: true)
      users.each { |user| UserMailer.notification(user).deliver_now }
    end
    

    在上述场景中,如果UserMailer循环由于错误或服务器重新启动而在中间停止,则事务回滚并且作业回到队列中。但是任何已经发送的电子邮件都无法撤回,因为它们独立于交易。所以会有一部分电子邮件被重新发送。如果出现代码错误并且作业不断重新排队,则可能会出现多次。

  3. 以小批量处理电子邮件,例如 100 封,并接受最多 100 封可能不止一次发送,或者根本不发送,具体取决于上面的结构。

我错过了哪些替代方案?

任何基于事务的方法的另一个问题是 PostgreSQL 中的死锁风险。当用户在我们的系统中做某事时,我们可能会产生几个需要以不同方式更新记录的进程。在过去,我们使用事务的次数越多,死锁错误就越多。自从我们走这条路以来已经有几年了,所以也许更新的 PostgreSQL 版本可以更好地处理死锁问题。我们尝试更进一步并锁定记录,但随后我们开始在用户端出现超时,因为 Web 进程与后台作业竞争锁定。

是否有任何系统的方式来处理优雅地处理这些问题的工作?我是否只需要接受分布式 N+1 并在更多缓存中分层来处理它?鉴于我们需要使用数据库来确保幂等性,这让我想知道我们是否应该改用delayed_jobwithactive_record,因为它在内部处理自己的锁定。

标签: ruby-on-railsrubypostgresqlsidekiqidempotent

解决方案


这是一个非常复杂/加载的问题,因为架构确实取决于更多的因素,而不是简单的问题/答案格式可以简明地描述。但是,我可以给出一般性的建议。

处理与交付分开

开始交易、更新记录和发送电子邮件

将这些步骤分开。最好避免在事务中同时进行数据库更新和电子邮件发送,无论是否批处理。

与电子邮件发送分开,在交易中执行所有逻辑和记录更新。如果速度足够快,可以单独或批量执行它们,甚至可以在原始 Web 请求中执行它们。如果将结果保存到数据库,则可以使用事务来回滚失败。如果将结果保存为 args 到电子邮件发送作业,请确保在对批处理进行排队之前成功处理整个批处理。您现在拥有灵活性 b/c 这是一个纯数据转换。

为每个数据转换排队电子邮件发送作业。这些工作必须很少甚至没有逻辑和处理!让它们保持简单,没有数据库写入——所有处理都应该已经完成​​。仅将值传递给电子邮件模板并发送。这是关键的 b/c 这种外部影响不能包含在事务中。使电子邮件发送作业对您的系统是只读的(它“写入”电子邮件,在您的系统外部)也为您提供了灵活性——您可以缓存、从副本中读取等。

通过这样做,您可以将用于电子邮件处理的数据库负载与电子邮件发送分开,现在将它们分开处理。电子邮件处理中的错误不会影响电子邮件发送。电子邮件发送失败不会影响电子邮件处理。

关于行锁定和死锁

根本不需要锁定行——围绕处理的事务足以让数据库引擎处理它。也不应该有任何死锁,因为没有两个作业在读取和写入相同的行。

回应:中途死亡的工作

假设在事务完成后但在电子邮件发出之前,该作业被终止。

我通过与电子邮件发送分开处理事务,并使电子邮件发送尽可能简单,从而尽可能地降低了发生这种情况的可能性。一旦事务提交,就没有更多的处理要做,唯一失败的事情是通常不在你控制范围内的系统(Redis、Sidekiq、数据库、你的托管服务、互联网连接等)。

响应:重复作业

同一作业的两个副本可能会从队列中拉出,在将其设置为“处理”之前都会检查一些标志

您正在使用 Sidekiq 而不是编写自己的异步作业系统,因此您需要考虑作业系统故障超出您的范围。剩下的是您的工作绩效特征和工作系统配置。如果你得到重复的工作,我猜你的工作比配置的工作超时时间要长。你的工作花费了很长时间,以至于 Sidekiq 认为它已经死了(因为它还没有报告成功/失败),然后产生了另一次尝试。加速或中断作业,使其在配置的超时时间内成功或失败,这将停止发生(99.99% 的时间)。

与 Web 请求不同,另一端没有人来决定是否在异步作业系统中重试。这就是为什么您的工作绩效概况需要可预测的原因。一旦系统变得足够大,我希望基于以下差异将作业队列和工作人员完全分开:

  • 预期作业运行时间
  • 预期作业 CPU/内存/磁盘使用率
  • 预期的作业数据库或其他 I/O 使用情况
  • 工作只读?只写?两个都?
  • 影响外部服务的工作
  • 工作用户正在积极等待

推荐阅读