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
endUntil Executed
此策略在任务添加到队列时获取锁,并在任务完成后移除该锁。 它可以防止任务多次同时运行。
module Ci
class BuildTraceChunkFlushWorker
include ApplicationWorker
deduplicate :until_executed
idempotent!
# ...
end
end此外,您可以传递 if_deduplicated: :reschedule_once 选项,在
当前运行的任务完成后且至少发生一次去重时重新运行一次任务。
这确保即使发生竞争条件,也能始终产生最新结果。
有关更多信息,请参阅 此问题。
调度未来的任务
GitLab 不会跳过未来调度的任务,因为我们假设
在任务计划执行时状态已经改变。
对于 until_executed 和 until_executing 策略,
都可以对未来调度的任务进行去重。
如果您确实想要对未来调度的任务进行去重,
可以在定义去重策略时通过传递 including_scheduled: true 参数
在 Worker 上指定:
module AuthorizedProjectUpdate
class UserRefreshOverUserRangeWorker
include ApplicationWorker
deduplicate :until_executing, including_scheduled: true
idempotent!
# ...
end
end设置去重生存时间 (TTL)
去重依赖于存储在 Redis 中的幂等键。这通常 由配置的去重策略清除。
然而,在某些情况下,键可能会保留直到其 TTL,例如:
-
使用了
until_executing,但在 Sidekiq 客户端中间件运行后,任务从未被排队或执行。 -
使用了
until_executed,但由于重试耗尽,任务未能完成, 被中断了最大次数,或者丢失了。
默认值是 6 小时。在此期间,即使第一个任务从未执行或完成, 任务也不会被排队。
TTL 可以通过以下方式配置:
class ProjectImportScheduleWorker
include ApplicationWorker
idempotent!
deduplicate :until_executing, ttl: 5.minutes
end当 TTL 达到时,可能会发生重复任务,因此请确保您只对 可以容忍一定重复的任务降低此值。
为幂等任务保留最新的 WAL 位置
去重总是考虑最新的二进制复制指针,而不是第一个。 这是因为我们丢弃了第二次调度的相同任务,并且预写日志(WAL)丢失了。 这可能导致比较旧的 WAL 位置并从过时的副本读取。
为了同时支持去重和通过负载平衡维护数据一致性, 我们在 Redis 中为幂等任务保留最新的 WAL 位置。 这样我们总是比较最新的二进制复制指针, 确保我们从完全同步的副本中读取。