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

Sidekiq 跨版本兼容性

Sidekiq 任务的参数在等待执行时存储在队列中。在在线更新期间,这可能导致几种可能的情况:

  1. 旧版本的应用程序发布了一个任务,由升级后的 Sidekiq 节点执行。
  2. 任务在升级前被排队,但在升级后执行。
  3. 任务由运行较新版本应用程序的节点排队,但在运行较旧版本应用程序的节点上执行。

添加新的 worker

在 GitLab.com 上,我们目前没有在 canary 阶段部署 Sidekiq。 这意味着可以从 HTTP 端点调度的新的 worker 可能会从 canary 调度,但直到完整的生产部署完成后才会在 Sidekiq 上运行。这可能比调度任务晚几个小时。对于一些 worker 来说,这不是问题。但对于其他 worker - 特别是延迟敏感的任务 - 这将导致糟糕的用户体验。

这仅适用于首次引入的新 worker 类。由于我们推荐使用功能标志作为通用开发流程,最好使用功能标志来控制整个变更(包括新 Sidekiq worker 的调度)。

更改 worker 的参数

任务需要在应用程序的连续版本之间保持向后和向前兼容。添加或删除参数可能会导致问题。

在任何部署期间,都会有一段时间,其中一些应用程序节点已更新,而其他节点尚未更新。 如果更新的节点使用新参数排队任务,但较旧的 Sidekiq 节点处理它,任务将因参数不匹配而失败。

对于 GitLab.com,如果在同一里程碑中有多次部署,可能会发生这种情况。大多数自管理部署在每个发布周期中按顺序更新所有节点,因此我们需要将变更分布在多个版本中。

弃用并移除参数

在从 perform_asyncperform 方法中删除参数之前,请先弃用它们。以下示例展示了如何弃用然后从 perform_async 方法中移除 arg2

  1. 提供一个默认值(通常是 nil),并使用注释将参数标记为将在下一个次要版本中弃用。(版本 M)

    class ExampleWorker
      # 为向后兼容保留 arg2 参数。
      def perform(object_id, arg1, arg2 = nil)
        # ...
      end
    end
  2. 一个次要版本后,停止在 perform_async 中使用该参数。(版本 M+1)

    ExampleWorker.perform_async(object_id, arg1)
  3. 在下一个主要版本中,从 worker 类中移除该值。(下一个主要版本)

    class ExampleWorker
      def perform(object_id, arg1)
        # ...
      end
    end

添加参数

有两种方法可以安全地向 Sidekiq worker 添加新参数:

  • 设置一个多步骤发布,首先将新参数添加到 worker 中。考虑使用参数哈希以获得未来的灵活性。
  • 如果 worker 已经使用参数哈希来存储额外参数,则在哈希中传递新参数。尚未使用参数哈希的 worker 需要通过多步骤发布来首先添加它。

多步骤发布

这种方法需要多个版本。

  1. 使用默认值将参数添加到 worker 中(版本 M)。

    class ExampleWorker
      def perform(object_id, new_arg = nil)
        # ...
      end
    end
  2. 将新参数添加到 worker 的所有调用中(版本 M+1)。

    ExampleWorker.perform_async(object_id, new_arg)
  3. 移除默认值(版本 M+2)。

    class ExampleWorker
      def perform(object_id, new_arg)
        # ...
      end
    end

参数哈希

如果现有的 worker 已经使用参数哈希,这种方法不需要多个版本。

  1. 在 worker 中使用参数哈希以允许未来的灵活性。

    class ExampleWorker
      def perform(object_id, params = {})
        # ...
      end
    end

移除 worker 类

要移除 worker 类,请在三个次要版本中遵循以下步骤:

在次要版本 M 中

  1. 移除任何排队任务的代码。

    例如,如果有一个 UI 组件或 API 端点,用户与之交互会导致 worker 实例被排队,请确保这些表面区域要么被移除,要么以不再排队 worker 实例的方式进行更新。

    这确保了与 worker 类相关的实例不再被排队。

  2. 确保前端和后端代码不再依赖于 worker 以前完成的工作。

  3. 在相关的 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。

为确保应用程序连续版本之间的向后和向前兼容性,请在三个次要版本中遵循以下步骤:

  1. 创建新命名的 worker,并让旧的 worker 调用新 worker 的 #perform 方法。引入一个功能标志来控制我们何时开始调度新 worker。(版本 M)

    任何仍在队列中的旧 worker 任务将委托给新 worker。当此版本部署后,调度哪个版本的任务或哪个 Sidekiq 处理它不再重要,旧 Sidekiq 将使用旧 worker 的完整实现,新 Sidekiq 将委托给新 worker。

  2. 为 GitLab.com 启用功能标志,之后准备一个 MR 以默认启用它。(版本 M+1)

  3. 移除旧的 worker 类和功能标志。(版本 M+2)