Help us learn about your current experience with the documentation. Take the survey.

Sidekiq 幂等工作

众所周知,任务可能会因多种原因失败。例如,网络中断或程序错误。 为了解决这个问题,Sidekiq 内置了一个重试机制, GitLab 中的大多数 Worker 默认都会使用该机制。

我们期望任务在失败后可以再次运行,且不会对应用程序或用户产生重大副作用, 这就是为什么 Sidekiq 鼓励任务应该是 幂等和事务性的

一般来说,如果 Worker 满足以下条件,就可以被认为是幂等的:

  • 可以使用相同的参数安全地运行多次。
  • 应用程序的副作用只发生一次 (或者第二次运行的副作用不会产生影响)。

一个很好的例子是缓存过期 Worker。

当幂等 Worker 的任务被调度时,如果队列中已存在相同参数的未开始任务, 该任务会被 去重

确保 Worker 是幂等的

使用以下共享示例来查看运行两次任务的效果。

it_behaves_like 'an idempotent worker'

该共享示例需要定义 job_args。如果没有提供,它会 不带参数调用任务。

当共享示例运行时,不应该有任何模拟会避免任务的副作用。 例如,允许 Worker 调用服务而不模拟其 execute 方法。 这样,我们可以断言任务确实是幂等的。

共享示例包含一些基本测试。您可以在共享示例块中添加更多 针对特定 Worker 的幂等性测试。

it_behaves_like 'an idempotent worker' do
  it '检查多次调用的副作用' do
    # `perform_idempotent_work` 将调用任务的 perform 方法 2 次
    perform_idempotent_work

    expect(model.state).to eq('state')
  end
end

声明 Worker 为幂等

class IdempotentWorker
  include ApplicationWorker

  # 声明一个 Worker 是幂等的,可以
  # 安全地运行多次。
  idempotent!

  # ...
end

建议只在最顶层的 Worker 类中调用 idempotent!,即使 perform 方法定义在另一个类或模块中。

如果 Worker 类没有标记为幂等,代码检查(cop)会失败。 如果您不确定您的任务是否可以安全地运行多次,可以考虑跳过该检查。

去重

当幂等 Worker 的任务被排队时,如果另一个 未开始的任务已经在队列中,GitLab 会丢弃第二个 任务。这项工作被跳过,因为相同的工作将由 第一个调度的任务完成;当第二个任务执行时, 第一个任务将不会做任何事。

策略

GitLab 支持两种去重策略:

  • until_executing,这是默认策略
  • until_executed

更多 去重策略已被建议。 如果您正在实现一个可以从不同策略中受益的 Worker, 请在问题中发表评论。

Until Executing

此策略在任务添加到队列时获取锁,并在任务开始前移除该锁。

例如,AuthorizedProjectsWorker 接受用户 ID。当 Worker 运行时,它会重新计算用户的授权。GitLab 在每次 可能更改用户授权的操作时都会调度此任务。 如果同一时间将同一用户添加到两个项目中,如果第一个任务尚未 开始,第二个任务可以被跳过,因为当第一个任务运行时,它会为两个项目创建 授权。

module AuthorizedProjectUpdate
  class UserRefreshOverUserRangeWorker
    include ApplicationWorker

    deduplicate :until_executing
    idempotent!

    # ...
  end
end

Until Executed

此策略在任务添加到队列时获取锁,并在任务完成后移除该锁。 它可以防止任务多次同时运行。

module Ci
  class BuildTraceChunkFlushWorker
    include ApplicationWorker

    deduplicate :until_executed
    idempotent!

    # ...
  end
end

此外,您可以传递 if_deduplicated: :reschedule_once 选项,在 当前运行的任务完成后且至少发生一次去重时重新运行一次任务。 这确保即使发生竞争条件,也能始终产生最新结果。 有关更多信息,请参阅 此问题

调度未来的任务

GitLab 不会跳过未来调度的任务,因为我们假设 在任务计划执行时状态已经改变。 对于 until_executeduntil_executing 策略, 都可以对未来调度的任务进行去重。

如果您确实想要对未来调度的任务进行去重, 可以在定义去重策略时通过传递 including_scheduled: true 参数 在 Worker 上指定:

module AuthorizedProjectUpdate
  class UserRefreshOverUserRangeWorker
    include ApplicationWorker

    deduplicate :until_executing, including_scheduled: true
    idempotent!

    # ...
  end
end

设置去重生存时间 (TTL)

去重依赖于存储在 Redis 中的幂等键。这通常 由配置的去重策略清除。

然而,在某些情况下,键可能会保留直到其 TTL,例如:

  1. 使用了 until_executing,但在 Sidekiq 客户端中间件运行后,任务从未被排队或执行。

  2. 使用了 until_executed,但由于重试耗尽,任务未能完成, 被中断了最大次数,或者丢失了。

默认值是 6 小时。在此期间,即使第一个任务从未执行或完成, 任务也不会被排队。

TTL 可以通过以下方式配置:

class ProjectImportScheduleWorker
  include ApplicationWorker

  idempotent!
  deduplicate :until_executing, ttl: 5.minutes
end

当 TTL 达到时,可能会发生重复任务,因此请确保您只对 可以容忍一定重复的任务降低此值。

为幂等任务保留最新的 WAL 位置

去重总是考虑最新的二进制复制指针,而不是第一个。 这是因为我们丢弃了第二次调度的相同任务,并且预写日志(WAL)丢失了。 这可能导致比较旧的 WAL 位置并从过时的副本读取。

为了同时支持去重和通过负载平衡维护数据一致性, 我们在 Redis 中为幂等任务保留最新的 WAL 位置。 这样我们总是比较最新的二进制复制指针, 确保我们从完全同步的副本中读取。