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

Sidekiq worker 属性

Worker类可以定义某些属性来控制其行为并添加元数据。

从其他worker继承的子类也会继承这些属性,因此只有当你想覆盖它们的值时才需要重新定义。

作业紧急程度

作业可以设置urgency属性,该属性可以是:high:low:throttled。这些属性的对应目标如下:

紧急程度 队列调度目标 执行延迟要求
:high 10秒 10秒
:low(默认) 1分钟 5分钟
:throttled 5分钟

要设置作业的紧急程度,请使用urgency类方法:

class HighUrgencyWorker
  include ApplicationWorker

  urgency :high

  # ...
end

对延迟敏感的作业

如果大量后台作业同时被调度,作业可能在等待worker节点可用时发生排队。这是正常的,它通过允许系统优雅地处理流量峰值来提供弹性。然而,有些作业比其他作业更敏感于延迟。

通常,对延迟敏感的作业执行的操作是用户合理期望同步发生的操作,而不是在后台worker中异步进行。一个常见的例子是在某个操作后进行的写入。这类作业的示例包括:

  1. 在向分支推送后更新合并请求的作业。
  2. 在向分支推送后使项目已知分支的缓存失效的作业。
  3. 在权限更改后重新计算用户可见的组和项目的作业。
  4. 在流水线中的作业状态变更后更新CI流水线状态的作业。

当这些作业被延迟时,用户可能会将延迟视为bug:例如,他们可能推送一个分支,然后尝试为该分支创建合并请求,但在UI中被告知该分支不存在。我们认为这些作业属于urgency :high

我们会额外努力确保这些作业在调度后被非常短的时间内启动。然而,为了确保吞吐量,这些作业也有非常严格的执行时长要求:

  1. 作业的中位执行时间应小于1秒。
  2. 99%的作业应在10秒内完成。

如果一个worker无法满足这些预期,那么它不能被视为urgency :high的worker:考虑重新设计worker,或将工作拆分为两个不同的worker,一个具有快速执行的urgency :high代码,另一个具有urgency :low,后者没有执行延迟要求(但也具有较低的调度目标)。

更改队列的紧急程度

在GitLab.com上,我们在多个分片中运行Sidekiq,每个分片代表一种特定类型的工作负载。

当更改队列的紧急程度或添加新队列时,我们需要考虑新分片的预期工作负载。如果我们正在更改现有队列,这也会影响旧分片,但这总是减少工作量。

为此,我们希望计算新分片的总执行时间和RPS(吞吐量)的预期增加量。我们可以从以下位置获取这些值:

  • 队列详情仪表板包含队列本身的值。对于新队列,我们可以查找具有相似模式或在类似情况下调度的队列。
  • 分片详情仪表板包含总执行时间和吞吐量(RPS)。分片利用率面板显示此分片当前是否有任何剩余容量。

然后,我们可以计算队列的RPS * 平均运行时间(为新作业估计)以查看我们对新分片预期的RPS和执行时间的相对增加量:

new_queue_consumption = queue_rps * queue_duration_avg
shard_consumption = shard_rps * shard_duration_avg

(new_queue_consumption / shard_consumption) * 100

如果我们预计增加量少于5%,则无需进一步操作。

否则,请在合并请求中ping @gitlab-com/gl-infra/data-access/durability 并请求审核。

具有外部依赖的作业

GitLab应用中的大多数后台作业会与其他GitLab服务进行通信,例如PostgreSQL、Redis、Gitaly和对象存储。这些被视为作业的“内部”依赖项。

然而,有些作业的成功完成依赖于外部服务。一些示例包括:

  1. 调用用户配置的Web钩子的作业。
  2. 将应用程序部署到用户配置的Kubernetes集群的作业。

这些作业具有“外部依赖项”。这对于后台处理集群的操作至关重要,原因如下:

  1. 大多数外部依赖项(如Web钩子)不提供SLO(服务等级目标),因此我们无法保证这些作业的执行延迟。由于无法保证执行延迟,我们也无法保证吞吐量。因此,在高流量环境中,我们需要确保具有外部依赖的作业与高优先级作业分离,以保证这些队列的吞吐量。
  2. 具有外部依赖的作业的错误告警阈值更高,因为错误的根源很可能是外部的。
class ExternalDependencyWorker
  include ApplicationWorker

  # 声明此工作器依赖于第三方外部服务以成功完成
  worker_has_external_dependencies!

  # ...
end

一个作业不能同时具有高优先级和外部依赖。

受CPU和内存限制的工作器

受CPU或内存资源限制的工作器应使用worker_resource_boundary方法进行标注。

大多数工作器倾向于大部分时间处于阻塞状态,等待来自其他服务(如Redis、PostgreSQL和Gitaly)的网络响应。由于Sidekiq是多线程环境,这些作业可以以高并发调度。

然而,有些工作器会在CPU上花费大量时间运行Ruby逻辑。Ruby MRI不支持真正的多线程——它依靠GIL(全局解释器锁)来极大简化应用开发,即无论托管进程的机器有多少核心,一次只允许进程中的一段Ruby代码运行。对于IO密集型工作器来说,这不是问题,因为大多数线程在底层库中阻塞(这些库不受GIL约束)。

如果许多线程试图同时运行Ruby代码,这会导致GIL上的竞争,从而减慢所有进程的速度。

在高流量环境中,知道工作器是CPU密集型的,我们可以将其放在不同的集群中以较低的并发运行。这样可以确保最佳性能。

同样,如果一个工作器使用了大量内存,我们可以将其放在定制的低并发、高内存集群中运行。

内存绑定的工作器会产生繁重的GC(垃圾回收)负载,导致10-50毫秒的暂停。这对工作器的延迟要求有影响。因此,标记为memory绑定且urgency :high的作业是不被允许的,并且会触发CI失败。一般来说,memory绑定的工作器不被建议使用,应考虑替代的处理方式。

如果一个工作器需要大量的内存和CPU时间,则应标记为内存绑定,因为上述对高优先级内存绑定工作器的限制。

将作业声明为CPU密集型

此示例展示了如何将作业声明为CPU密集型。

class CPUIntensiveWorker
  include ApplicationWorker

  # 声明此工作器将在CPU上执行大量计算。
  worker_resource_boundary :cpu

  # ...
end

确定工作器是否为CPU密集型

我们采用以下方法来确定工作器是否为CPU密集型:

  • 在Sidekiq结构化JSON日志中,聚合工作器的durationcpu_s字段。
  • duration指作业的总执行时长,单位为秒。
  • cpu_s源自Process::CLOCK_THREAD_CPUTIME_ID计数器,是作业在CPU上所花时间的度量。
  • cpu_s除以duration得到在CPU上花费的时间百分比。
  • 如果该比率超过33%,则认为该工作器是CPU密集型的,并应相应地标注。
  • 这些值不应在小样本量上使用,而应在相当大的聚合数据集上使用。

功能类别

所有Sidekiq工作器必须定义已知的功能类别

作业数据一致性策略

在 GitLab 13.11 及更早版本中,Sidekiq 工作进程始终向主数据库节点发送数据库查询,无论是读取还是写入。这确保了数据的完整性和即时性,因为在单节点场景下,即使工作进程读取自己的写入操作,也不会遇到过时的读取。

然而,如果一个工作进程向主库写入,但从副本读取,由于副本可能落后于主库,读取过时记录的可能性不为零。

当依赖数据库的作业数量增加时,确保即时数据一致性会给主数据库服务器带来不可持续的负载。因此,我们添加了支持 Sidekiq 工作进程的数据库负载均衡 的功能。通过配置工作进程的 data_consistency 字段,我们可以让调度器根据以下概述的几种策略将读取指向副本。

以牺牲即时性换取减少主库负载

我们要求 Sidekiq 工作进程明确决定是否需要对所有读写操作使用主数据库节点,或者是否可以从副本提供读取服务。这是通过 RuboCop 规则强制执行的,该规则确保设置了 data_consistency 字段。

在引入 data_consistency 之前,默认行为模仿 :always。由于作业现在与当前数据库 LSN(日志序列号)一起入队,副本(对于 :sticky:delayed)保证能追上该点,否则作业会重试或使用主库。这意味着数据至少会与作业入队时的状态保持一致。

下表显示了 data_consistency 属性及其值,按它们对副本的偏好程度和等待副本追上的顺序排列:

数据一致性 说明 指南
:always 该作业要求对所有查询使用主数据库。(已弃用) 已弃用 仅适用于遇到主库粘性问题边缘情况的作业。
:sticky 该作业优先选择副本,但在写入或遇到复制延迟时切换到主库。 这是首选选项。应适用于需要尽可能快速执行的作业。副本保证能追上作业入队时的点。
:delayed 该作业优先选择副本,但写入时切换到主库。在作业开始前遇到复制延迟时,会重试一次。如果下次重试时副本仍未更新,则切换到主库。 应适用于进一步延迟执行通常无关紧要的作业,例如缓存过期或 Web 钩子执行。不应用于禁用重试的作业,例如定时任务。

在所有情况下,工作进程要么从完全追上的副本读取,要么从主节点读取,因此总能保证数据一致性。

要为工作进程设置数据一致性,请使用 data_consistency 类方法:

class DelayedWorker
  include ApplicationWorker

  data_consistency :delayed

  # ...
end

为分解后的数据库覆盖数据一致性

GitLab 使用多个分解后的数据库。Sidekiq 工作进程对这些数据库的使用可能偏向某个特定数据库。例如,PipelineProcessWorkerci 数据库的写入流量高于 main 数据库。在涉及主库粘性问题的边缘情况下,为每个数据库定义独立的数据一致性允许工作进程更高效地使用读副本。

如果设置了 overrides 关键字参数,Gitlab::Database::LoadBalancing::SidekiqServerMiddleware 会加载负载均衡策略,使用最优先选择副本的数据一致性。

偏好程度的递增顺序为::always:sticky,然后是 :delayed

仅当 GitLab 实例使用多个数据库或 Gitlab::Database.database_mode == Gitlab::Database::MODE_MULTIPLE_DATABASES 时,覆盖才生效。

要为工作进程设置数据一致性,请使用带有 overrides 关键字参数的 data_consistency 类方法:

class MultipleDataConsistencyWorker
  include ApplicationWorker

  data_consistency :always, overrides: { ci: :sticky }

  # ...
end

feature_flag 属性

feature_flag 属性允许你切换作业的 data_consistency,这使你可以安全地为特定作业切换负载均衡功能。当 feature_flag 禁用时,作业默认为 :always,这意味着该作业始终使用主数据库。

feature_flag 属性不允许使用 基于参与者的特性门控。这意味着特性门控不能仅为特定项目、组或用户切换,但你可以安全地使用 按时间百分比部署。由于我们在 Sidekiq 客户端和服务器上都检查特性门控,因此以 10% 的时间进行部署,可能导致仅 1%(0.1 [来自客户端] * 0.1 [来自服务器])的有效作业使用副本。

示例:

class DelayedWorker
  include ApplicationWorker

  data_consistency :delayed, feature_flag: :load_balancing_for_delayed_worker

  # ...
end

当使用带有 overridesfeature_flag 属性时,所有数据库连接的作业默认为 always。当特性门控启用时,配置的数据一致性会独立应用于每个数据库。对于以下示例,当标志启用时,main 数据库连接将使用 :always 数据一致性,而 ci 数据库连接将使用 :sticky 数据一致性。

class DelayedWorker
  include ApplicationWorker

  data_consistency :always, overrides: { ci: :sticky }, feature_flag: :load_balancing_for_delayed_worker

  # ...
end

幂等作业的数据一致性

对于声明 :sticky:delayed 数据一致性的 幂等作业,我们正在 保留最新的 WAL 位置,同时去重,确保我们从完全跟上的副本中读取数据。

作业暂停控制

通过 pause_control 属性,你可以有条件地暂停作业处理。如果策略处于活动状态,作业将被存储在一个单独的 ZSET 中,并在策略变为非活动状态时重新入队。PauseControl::ResumeWorker 是一个 cron 工作器,用于检查是否必须重启任何已暂停的作业。

若要使用 pause_control,你可以:

  • 使用 lib/gitlab/sidekiq_middleware/pause_control/strategies/ 中定义的策略之一。
  • lib/gitlab/sidekiq_middleware/pause_control/strategies/ 中定义自定义策略,并将该策略添加到 lib/gitlab/sidekiq_middleware/pause_control.rb

例如:

module Gitlab
  module SidekiqMiddleware
    module PauseControl
      module Strategies
        class CustomStrategy < Base
          def should_pause?
            ApplicationSetting.current.elasticsearch_pause_indexing?
          end
        end
      end
    end
  end
end
class PausedWorker
  include ApplicationWorker

  pause_control :custom_strategy

  # ...
end

如果你想为工作器移除中间件,请将策略设置为 :deprecated 以禁用它,并等待必要的停止后再完全移除。这样可以确保所有暂停的作业正确恢复。

并发限制

通过 concurrency_limit 属性,你可以限制工作器的并发量。它会将超出此限制的作业放入单独的 LIST 中,并在低于限制时重新入队。ConcurrencyLimit::ResumeWorker 是一个 cron 工作器,用于检查是否应重新入队任何被节流的作业。

第一个越过定义的并发限制的作业会启动此类所有其他作业的节流过程。在此之前,作业会按常规调度和执行。

当节流开始后,新调度的和执行的作业会被添加到 LIST 的末尾,以确保执行顺序得以保留。一旦 LIST 再次变空,节流过程结束。

暴露了 Prometheus 指标来监控使用并发限制中间件的工作器:

  • sidekiq_concurrency_limit_deferred_jobs_total
  • sidekiq_concurrency_limit_queue_jobs
  • sidekiq_concurrency_limit_queue_jobs_total
  • sidekiq_concurrency_limit_max_concurrent_jobs
  • sidekiq_concurrency_limit_current_concurrent_jobs_total

如果有持续的负载超过限制,LIST 将不断增长,直到限制被禁用或负载降至限制以下。

你应该使用 lambda 来定义限制。如果它返回 nil0,则不会应用限制。负数会暂停执行。

class LimitedWorker
  include ApplicationWorker

  concurrency_limit -> { 60 }

  # ...
end
class LimitedWorker
  include ApplicationWorker

  concurrency_limit -> { ApplicationSetting.current.elasticsearch_concurrent_sidekiq_jobs }

  # ...
end

跳过在 Geo 次级节点上执行工作器

在 Geo 次级节点上,数据库写入被禁用。 如果这些工作器在 Geo 次级节点上入队,你必须跳过那些尝试从 Geo 次级节点进行数据库写入的工作器的执行。 幸运的是,大多数工作器不会在 Geo 次级节点上入队,因为大多数非 GET HTTP 请求会被代理到 Geo 主节点,并且因为 Geo 次级节点禁用了大部分 Sidekiq-Cron 任务。 如果你不确定,请咨询 Geo 工程师。 要跳过执行,在工作器类前添加 ::Geo::SkipSecondary 模块。

class DummyWorker
  include ApplicationWorker
  prepend ::Geo::SkipSecondary

 # ...
end