Sidekiq 跨版本兼容性
Sidekiq 任务的参数在等待执行时存储在队列中。在在线更新期间,这可能导致几种可能的情况:
- 旧版本的应用程序发布了一个任务,由升级后的 Sidekiq 节点执行。
- 任务在升级前被排队,但在升级后执行。
- 任务由运行较新版本应用程序的节点排队,但在运行较旧版本应用程序的节点上执行。
添加新的 worker
在 GitLab.com 上,我们目前没有在 canary 阶段部署 Sidekiq。 这意味着可以从 HTTP 端点调度的新的 worker 可能会从 canary 调度,但直到完整的生产部署完成后才会在 Sidekiq 上运行。这可能比调度任务晚几个小时。对于一些 worker 来说,这不是问题。但对于其他 worker - 特别是延迟敏感的任务 - 这将导致糟糕的用户体验。
这仅适用于首次引入的新 worker 类。由于我们推荐使用功能标志作为通用开发流程,最好使用功能标志来控制整个变更(包括新 Sidekiq worker 的调度)。
更改 worker 的参数
任务需要在应用程序的连续版本之间保持向后和向前兼容。添加或删除参数可能会导致问题。
在任何部署期间,都会有一段时间,其中一些应用程序节点已更新,而其他节点尚未更新。 如果更新的节点使用新参数排队任务,但较旧的 Sidekiq 节点处理它,任务将因参数不匹配而失败。
对于 GitLab.com,如果在同一里程碑中有多次部署,可能会发生这种情况。大多数自管理部署在每个发布周期中按顺序更新所有节点,因此我们需要将变更分布在多个版本中。
弃用并移除参数
在从 perform_async 和 perform 方法中删除参数之前,请先弃用它们。以下示例展示了如何弃用然后从 perform_async 方法中移除 arg2:
-
提供一个默认值(通常是
nil),并使用注释将参数标记为将在下一个次要版本中弃用。(版本 M)class ExampleWorker # 为向后兼容保留 arg2 参数。 def perform(object_id, arg1, arg2 = nil) # ... end end -
一个次要版本后,停止在
perform_async中使用该参数。(版本 M+1)ExampleWorker.perform_async(object_id, arg1) -
在下一个主要版本中,从 worker 类中移除该值。(下一个主要版本)
class ExampleWorker def perform(object_id, arg1) # ... end end
添加参数
有两种方法可以安全地向 Sidekiq worker 添加新参数:
- 设置一个多步骤发布,首先将新参数添加到 worker 中。考虑使用参数哈希以获得未来的灵活性。
- 如果 worker 已经使用参数哈希来存储额外参数,则在哈希中传递新参数。尚未使用参数哈希的 worker 需要通过多步骤发布来首先添加它。
多步骤发布
这种方法需要多个版本。
-
使用默认值将参数添加到 worker 中(版本 M)。
class ExampleWorker def perform(object_id, new_arg = nil) # ... end end -
将新参数添加到 worker 的所有调用中(版本 M+1)。
ExampleWorker.perform_async(object_id, new_arg) -
移除默认值(版本 M+2)。
class ExampleWorker def perform(object_id, new_arg) # ... end end
参数哈希
如果现有的 worker 已经使用参数哈希,这种方法不需要多个版本。
-
在 worker 中使用参数哈希以允许未来的灵活性。
class ExampleWorker def perform(object_id, params = {}) # ... end end
移除 worker 类
要移除 worker 类,请在三个次要版本中遵循以下步骤:
在次要版本 M 中
-
移除任何排队任务的代码。
例如,如果有一个 UI 组件或 API 端点,用户与之交互会导致 worker 实例被排队,请确保这些表面区域要么被移除,要么以不再排队 worker 实例的方式进行更新。
这确保了与 worker 类相关的实例不再被排队。
-
确保前端和后端代码不再依赖于 worker 以前完成的工作。
-
在相关的 worker 类中,将
perform方法的内容替换为空操作,同时保持任何参数不变。例如,如果您正在处理以下
ExampleWorker:class ExampleWorker def perform(object_id) SomeService.run!(object_id) end end实现空操作可能如下所示:
class ExampleWorker def perform(object_id); end end通过实现这个空操作,一旦任何仍在排队的已弃用任务最终被处理,您就可以避免不必要的周期。
在 M+1 版本中
添加一个使用 sidekiq_remove_jobs 的迁移(不是部署后迁移):
class RemoveMyDeprecatedWorkersJobInstances < Gitlab::Database::Migration[2.1]
# 使用 `sidekiq_remove_jobs` 方法时,始终使用 `disable_ddl_transaction!`,
# 因为由于 `idle-in-transaction` 超时,我们遇到了多次生产事故。
disable_ddl_transaction!
DEPRECATED_JOB_CLASSES = %w[
MyDeprecatedWorkerOne
MyDeprecatedWorkerTwo
]
def up
Gitlab::SidekiqSharding::Validator.allow_unrouted_sidekiq_calls do
# 如果任务已通过 `sidekiq-cron` 调度,我们还必须使用在 config/initializers/1_settings.rb 中定义 cron 调度时使用的键
# 从计划 worker 集合中移除它。
job_to_remove = Sidekiq::Cron::Job.find('my_deprecated_worker')
# 任务可能被完全移除:
job_to_remove.destroy if job_to_remove
# 任务可能被禁用:
job_to_remove.disable! if job_to_remove
end
# 从 Sidekiq 队列中移除计划的任务实例
sidekiq_remove_jobs(job_klasses: DEPRECATED_JOB_CLASSES)
end
def down
# 此迁移移除了任何已弃用 worker 的实例,无法撤销。
end
end在 M+2 版本中
删除 worker 类文件,并遵循我们Sidekiq 队列文档中关于运行 Rake 任务来重新生成/更新相关文件的指导。
重命名队列
出于与移除 worker 相同的危险原因,重命名队列时应谨慎。
重命名队列时,在部署后迁移中使用 sidekiq_queue_migrate 辅助迁移方法:
class MigrateTheRenamedSidekiqQueue < Gitlab::Database::Migration[2.1]
restrict_gitlab_migration gitlab_schema: :gitlab_main
disable_ddl_transaction!
def up
sidekiq_queue_migrate 'old_queue_name', to: 'new_queue_name'
end
def down
sidekiq_queue_migrate 'new_queue_name', to: 'old_queue_name'
end
end您必须在部署后迁移中重命名队列,而不是在标准迁移中。否则,它会在所有调度这些任务的 worker 停止运行之前过早运行。另请参阅其他示例。
重命名 worker 类
我们应该将此处理类似于添加新的 worker。这意味着我们只在 Sidekiq 部署完成后才开始调度新命名的 worker。
为确保应用程序连续版本之间的向后和向前兼容性,请在三个次要版本中遵循以下步骤:
-
创建新命名的 worker,并让旧的 worker 调用新 worker 的
#perform方法。引入一个功能标志来控制我们何时开始调度新 worker。(版本 M)任何仍在队列中的旧 worker 任务将委托给新 worker。当此版本部署后,调度哪个版本的任务或哪个 Sidekiq 处理它不再重要,旧 Sidekiq 将使用旧 worker 的完整实现,新 Sidekiq 将委托给新 worker。
-
为 GitLab.com 启用功能标志,之后准备一个 MR 以默认启用它。(版本 M+1)
-
移除旧的 worker 类和功能标志。(版本 M+2)