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

添加新的 Redis 实例

GitLab 可以使用多个 Redis 实例。 这些实例在功能上是分区的,例如,我们可以将 CI 追踪块 存储在一个 Redis 实例中,而将会话存储在另一个实例中。

我们可能需要不时地添加新的 Redis 实例。通常,这会是将现有实例(如缓存或共享状态)进行功能分区拆分。本文档描述了一种基于先前示例的添加新 Redis 实例的方法,该实例处理现有数据:

本文档不详细介绍准备和配置新 Redis 实例的操作方面,但示例 epic 中包含关于先前方法的信息。

步骤 1:支持配置新实例

在将任何功能切换到使用新实例之前,我们必须支持在代码库中配置和引用它。我们必须支持主要的安装类型:

回退实例

在应用程序代码中,我们需要定义一个回退实例,以防新实例未配置。例如,如果一个 GitLab 实例已经配置了单独的共享状态 Redis,并且我们正在从共享状态 Redis 中分区数据,那么当新实例配置不存在时,新实例的配置应默认为共享状态 Redis 的配置。否则,一旦新实例可用,我们可能会破坏未配置新 Redis 实例的实例。

您可以在 Gitlab::Redis::Wrapper(所有 Redis 实例的基类)中定义一个 .config_fallback 方法, 定义此实例,如果当前实例未配置,则使用该实例。如果我们正在添加一个应回退到 SharedStateFoo 实例,我们可以这样做:

module Gitlab
  module Redis
    class Foo < ::Gitlab::Redis::Wrapper
      # 我们存储在 Foo 上的数据以前存储在 SharedState 上。
      def self.config_fallback
        SharedState
      end
    end
  end
end

我们还应添加类似于 trace_chunks_spec.rb 中的规范,以确保此回退功能正常工作。

步骤 2:支持写入和读取新实例

在迁移到新实例时,我们必须考虑数据可能位于以下情况之一:

  • ‘旧’(原始)实例。
  • 我们刚刚添加支持的新实例。

因此,我们可能需要根据某些条件支持从两个实例读取和写入数据。

使用的确切条件取决于要迁移的数据。对于上述的追踪块情况,已经有一个数据库列指示数据存储的位置(因为除了 Redis 还有其他存储选项)。

如果数据的生命周期非常短(最多几分钟)且不关键,则此步骤可能不适用。在这种情况下,我们可能决定可以承受少量数据损失,仅通过配置进行切换。

如果没有更自然的方式来标记数据存储位置,使用 功能标志 可能会很方便:

  • 它不需要应用程序重启即可生效。
  • 它同时应用于所有应用程序实例(Sidekiq、API、web 等)。
  • 它支持增量发布 - 理想情况下按参与者(项目、组、用户等)- 这样我们可以监控错误并轻松回滚。

步骤 3:迁移数据

然后,我们需要为 GitLab.com 的生产和暂存环境配置新实例。希望能够在暂存环境中有效测试此更改,至少确保基本功能继续工作。

完成后,我们可以将更改发布到生产环境。理想情况下,这将采用增量方式,遵循功能标志的标准增量发布文档。

当我们在生产环境中 100% 使用新实例一段时间且没有问题时,我们可以继续。

建议的解决方案:使用 MultiStore 和回退策略迁移数据

我们需要一种方法来将用户迁移到新的 Redis 存储,而不会从用户体验角度造成任何不便。 我们还希望能够在新实例出现问题时回退到"旧"Redis 实例。

迁移要求:

  • 无停机时间。
  • 在数据存储的 TTL 过期之前,不会丢失存储的数据。
  • 使用功能标志或环境变量或两者组合进行部分发布。
  • 监控切换过程。
  • 设置 Prometheus 指标。
  • 如果新实例或逻辑行为不符合预期,可以轻松回滚而无需停机。

这有点类似于零停机时间的数据库表重命名。 我们需要将数据写入两个 Redis 实例(旧 + 新)。 我们从新实例读取数据,但当从失败的新专用 Redis 实例预取时,我们需要回退到旧实例。 我们需要记录新实例的任何问题或异常,但仍回退到旧实例。

建议的迁移策略是实现和使用 MultiStore。 我们使用此方法添加了用于会话键的新专用 Redis 实例。 MultiStore 还带有相应的 规范

MultiStore 看起来像一个 redis-rb ::Redis 实例。

在您在步骤 1中添加的新 Redis 实例类中,改为继承自 ::Gitlab::Redis::MultiStoreWrapper,并覆盖 multistore 类方法来定义 MultiStore。

module Gitlab
  module Redis
    class Foo < ::Gitlab::Redis::MultiStoreWrapper
      ...
      def self.multistore
        MultiStore.create_using_pool(self.pool, config_fallback.pool, store_name)
      end
    end
  end
end

MultiStore 通过提供新的 Redis 连接池作为主池,以及旧(回退实例)连接池作为辅助池来初始化。 第三个参数是 store_name,用于日志、指标和功能标志名称,以防我们同时使用 MultiStore 实现不同的 Redis 存储。

默认情况下,MultiStore 仅从默认的 Redis 存储读取和写入。 默认的 Redis 存储是 secondary_store(旧的回退实例)。 这允许我们引入 MultiStore 而不改变默认行为。

MultiStore 使用两个功能标志来控制实际迁移:

  • use_primary_and_secondary_stores_for_[store_name]
  • use_primary_store_as_default_for_[store_name]

例如,如果我们的新 Redis 实例名为 Gitlab::Redis::Foo,我们可以通过执行以下命令创建两个功能标志:

bin/feature-flag use_primary_and_secondary_stores_for_foo
bin/feature-flag use_primary_store_as_default_for_foo

通过启用 use_primary_and_secondary_stores_for_foo 功能标志,我们的 Gitlab::Redis::Foo 将使用 MultiStore 写入新的 Redis 实例和旧(回退实例)。所有读取命令仅在默认存储上执行,该默认存储使用 use_primary_store_as_default_for_foo 功能标志控制。通过启用 use_primary_store_as_default_for_foo 功能标志,MultiStore 使用 primary_store(新实例)作为默认 Redis 存储。

对于 pipelined 命令(pipelinedmulti),我们在两个存储中执行整个操作,然后比较结果。如果它们不同,我们会发出 Gitlab::Redis::MultiStore:PipelinedDiffError 错误,并在 gitlab_redis_multi_store_pipelined_diff_error_total Prometheus 计数器中跟踪它。

在新存储填充一段时间后,我们可以执行外部验证来比较两个存储的状态。 在验证结果令人满意后,我们可能可以安全地将流量转移到新的 Redis 存储。我们可以禁用 use_primary_and_secondary_stores_for_foo 功能标志。 这将允许 MultiStore 仅从主 Redis 存储(新存储)读取和写入,将所有流量转移到新的 Redis 存储。

一旦我们将所有流量转移到主存储,数据迁移就完成了。 我们可以安全地移除 MultiStore 实现,并继续使用新引入的 Redis 存储实例。

实现细节

MultiStore 分别实现读取和写入 Redis 命令。

读取命令

读取命令在 Gitlab::Redis::MultiStore::READ_COMMANDS 常量 中定义。

写入命令

写入命令在 Gitlab::Redis::MultiStore::WRITE_COMMANDS 常量 中定义。

pipelined 命令

传递给这些命令的 Ruby 块将执行两次,每个存储执行一次。 因此,除了执行的 Redis 操作外,块应该是幂等的。

  • pipelined
  • multi

当使用支持列表之外的命令时,method_missing 将其传递给旧的 Redis 实例并跟踪它。 这确保任何意外行为都像以前一样。在开发或测试环境中,会引发错误以便早期检测。

通过跟踪 gitlab_redis_multi_store_method_missing_total 计数器和 Gitlab::Redis::MultiStore::MethodMissingError, 开发人员需要在继续迁移之前为缺失的 Redis 命令添加实现。

不建议在 pipelinedmulti 块中进行变量赋值,因为块应该是幂等的。请参考修复性修复 MR,它移除了在迁移期间导致应用程序行为不正确的非幂等块。

错误
error message
Gitlab::Redis::MultiStore::PipelinedDiffError pipelined 命令在两个存储上执行成功,但结果不同。
Gitlab::Redis::MultiStore::MethodMissingError 方法缺失。回退到在 Redis 辅助存储上执行方法。
指标
Metrics name Type Labels Description
gitlab_redis_multi_store_pipelined_diff_error_total Prometheus Counter command, instance_name Redis MultiStore pipelined 命令在存储之间的差异
gitlab_redis_multi_store_method_missing_total Prometheus Counter command, instance_name 客户端 Redis MultiStore 方法缺失总数

步骤 4:迁移后清理

我们可以选择保留或删除迁移路径,具体取决于我们是否期望 GitLab 自托管实例执行此迁移。 gitlab-com/gl-infra/scalability#1131 包含关于追踪块功能标志的此主题讨论。在那种情况下,我们可能会决定支持迁移代码的维护成本高于允许自托管实例无缝执行此迁移的好处,如果我们期望自托管实例在没有此功能分区的情况下能够应对。

如果我们决定保留迁移代码:

  • 我们应该记录迁移步骤。
  • 如果我们使用了功能标志,我们应该确保它是操作类型功能标志,因为这些是长期存在的标志。

否则,我们可以删除标志并结束项目。