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

多数据库迁移

本文档描述了如何为使用多个数据库的解耦 GitLab 应用正确编写数据库迁移。 更多信息请参见多数据库

多数据库的设计(Geo 数据库除外)假设所有解耦的数据库都具有相同的结构(例如 schema),但每个数据库中的数据是不同的。这意味着某些表在每个数据库中不包含数据。

操作

根据使用的构造,我们可以将迁移分为以下几类:

  1. 修改结构(DDL - Data Definition Language)(例如 ALTER TABLE)。
  2. 修改数据(DML - Data Manipulation Language)(例如 UPDATE)。
  3. 执行其他查询(例如 SELECT),在我们的迁移中被视为 DML

使用 Gitlab::Database::Migration[2.0] 要求迁移必须始终具有单一目的。 迁移不能混合 DDLDML 的更改,因为应用程序要求所有解耦数据库的结构 (由 db/structure.sql 描述)必须完全相同。

数据定义语言 (DDL)

DDL 迁移包括所有:

  1. 创建或删除表(例如 create_table)。
  2. 添加或删除索引(例如 add_indexadd_concurrent_index)。
  3. 添加或删除外键(例如 add_foreign_keyadd_concurrent_foreign_key)。
  4. 添加或删除列,带或不带默认值(例如 add_column)。
  5. 创建或删除触发器函数(例如 create_trigger_function)。
  6. 将触发器附加或分离到表(例如 track_record_deletionsuntrack_record_deletions)。
  7. 准备或不准备异步索引(例如 prepare_async_indexunprepare_async_index_by_name)。
  8. 清空表(例如使用 truncate_tables! 辅助方法)。

因此 DDL 迁移不能

  1. 通过 SQL 语句或 ActiveRecord 模型以任何形式读取或修改数据。
  2. 更新列值(例如 update_column_in_batches)。
  3. 计划后台迁移(例如 queue_background_migration_jobs_by_range_at_intervals)。
  4. 读取功能标志的状态,因为它们存储在 main: 中(featuresfeature_gates 表)。
  5. 读取应用程序设置(因为设置存储在 main: 中)。

由于 GitLab 代码库中的大多数迁移都是 DDL 类型, 这也是默认的操作模式,无需对迁移文件进行进一步更改。

示例:在所有数据库上执行 DDL

示例迁移添加一个并发索引,该索引被视为结构更改(DDL), 在所有配置的数据库上执行。

class AddUserIdAndStateIndexToMergeRequestReviewers < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!

  INDEX_NAME = 'index_on_merge_request_reviewers_user_id_and_state'

  def up
    add_concurrent_index :merge_request_reviewers, [:user_id, :state], where: 'state = 2', name: INDEX_NAME
  end

  def down
    remove_concurrent_index_by_name :merge_request_reviewers, INDEX_NAME
  end
end

示例:添加新表存储在单个数据库中

  1. 将表添加到 db/docs/ 中的数据库字典

    table_name: ssh_signatures
    description: Description example
    introduced_by_url: Merge request link
    milestone: Milestone example
    feature_categories:
    - Feature category example
    classes:
    - Class example
    gitlab_schema: gitlab_main
  2. 在 schema 迁移中创建表:

    class CreateSshSignatures < Gitlab::Database::Migration[2.1]
      def change
        create_table :ssh_signatures do |t|
          t.timestamps_with_timezone null: false
          t.bigint :project_id, null: false, index: true
          t.bigint :key_id, null: false, index: true
          t.integer :verification_status, default: 0, null: false, limit: 2
          t.binary :commit_sha, null: false, index: { unique: true }
        end
      end
    end

数据操作语言 (DML)

DML 迁移包括所有:

  1. 通过 SQL 语句读取数据(例如 SELECT * FROM projects WHERE id=1)。
  2. 通过 ActiveRecord 模型读取数据(例如 User < MigrationRecord)。
  3. 通过 ActiveRecord 模型创建、更新或删除数据(例如 User.create!(...))。
  4. 通过 SQL 语句创建、更新或删除数据(例如 DELETE FROM projects WHERE id=1)。
  5. 批量更新列(例如 update_column_in_batches(:projects, :archived, true))。
  6. 计划后台迁移(例如 queue_background_migration_jobs_by_range_at_intervals)。
  7. 访问应用程序设置(例如,如果为 main: 数据库运行,则 ApplicationSetting.last)。
  8. 如果为 main: 数据库运行,则读取和修改功能标志。

DML 迁移不能

  1. 对 DDL 进行任何更改,因为这会破坏保持 structure.sql 在所有解耦数据库中一致性的规则。
  2. 从另一个数据库读取数据

要指示 DML 迁移类型,迁移必须在迁移类中使用 restrict_gitlab_migration gitlab_schema: 语法。 这会将给定的迁移标记为 DML 并限制对其的访问。

示例:仅在包含给定 gitlab_schema 的数据库上下文中执行 DML

示例迁移更新 projectsarchived 列,该迁移仅对包含 gitlab_main schema 的数据库执行。

class UpdateProjectsArchivedState < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!

  restrict_gitlab_migration gitlab_schema: :gitlab_main

  def up
    update_column_in_batches(:projects, :archived, true) do |table, query|
      query.where(table[:archived].eq(false)) # rubocop:disable CodeReuse/ActiveRecord
    end
  end

  def down
    # no-op
  end
end

示例:使用 ActiveRecord

使用 ActiveRecord 类执行数据操作的迁移必须使用 MigrationRecord 类。 该类保证在给定迁移的上下文中提供正确的连接。

在底层,MigrationRecord == ActiveRecord::Base,因为一旦 db:migrate 运行, 它会切换 ActiveRecord::Base.establish_connection :ci 的活动连接。 为了避免使用 ActiveRecord::Base 的混淆,需要使用 MigrationRecord

这意味着 DML 迁移被禁止从其他数据库读取数据。 例如,在 ci: 上下文中运行迁移并从 main: 读取功能标志, 因为没有建立到另一个数据库的连接。

class UpdateProjectsArchivedState < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!

  restrict_gitlab_migration gitlab_schema: :gitlab_main

  class Project < MigrationRecord
  end

  def up
    Project.where(archived: false).each_batch of |batch|
      batch.update_all(archived: true)
    end
  end

  def down
  end
end

gitlab_shared 的特殊用途

gitlab_schema 中所述, gitlab_shared 表允许在所有数据库中包含数据。这意味着此类迁移应该在所有数据库上运行以修改结构(DDL)或修改数据(DML)。

因此,访问 gitlab_shared 的迁移不需要使用 restrict_gitlab_migration gitlab_schema:, 没有限制的迁移会在所有数据库上运行,并允许在每个数据库上修改数据。 如果指定了 restrict_gitlab_migration gitlab_schema:,则 DML 迁移仅在包含给定 gitlab_schema 的数据库上下文中运行。

示例:在所有数据库上运行 DML gitlab_shared 迁移

示例迁移更新 loose_foreign_keys_deleted_records 表, 该表在 lib/gitlab/database/gitlab_schemas.yml 中被标记为 gitlab_shared

此迁移在所有配置的数据库上执行。

class DeleteAllLooseForeignKeyRecords < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!

  def up
    execute("DELETE FROM loose_foreign_keys_deleted_records")
  end

  def down
    # no-op
  end
end

示例:仅在包含给定 gitlab_schema 的数据库上运行 DML gitlab_shared

示例迁移更新 loose_foreign_keys_deleted_records 表, 该表在 db/docs/loose_foreign_keys_deleted_records.yml 中被标记为 gitlab_shared

由于此迁移配置了对 gitlab_ci 的限制,因此仅在包含 gitlab_ci schema 的数据库上下文中执行。

class DeleteCiBuildsLooseForeignKeyRecords < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!

  restrict_gitlab_migration gitlab_schema: :gitlab_ci

  def up
    execute("DELETE FROM loose_foreign_keys_deleted_records WHERE fully_qualified_table_name='ci_builds'")
  end

  def down
    # no-op
  end
end

跳过迁移的行为

唯一被跳过的迁移是执行 DML 更改的迁移。 DDL 迁移总是无条件地执行。

实现的解决方案 使用 database_tasks: 作为指示哪些额外的数据库配置 (在 config/database.yml 中)共享同一个主数据库的方式。 标记为 database_tasks: false 的数据库配置被免除为这些数据库配置执行 db:migrate

如果数据库配置不共享数据库(所有都有 database_tasks: true), 则每个迁移为每个数据库配置运行:

  1. DDL 迁移将所有结构更改应用到所有数据库。
  2. DML 迁移仅在包含给定 gitlab_schema: 的数据库上下文中运行。
  3. 如果 DML 迁移不符合运行条件,它将被跳过。它仍然在 schema_migrations 中被标记为已执行。 在运行 db:migrate 时,被跳过的迁移会输出 Current migration is skipped since it modifies 'gitlab_ci' which is outside of 'gitlab_main, gitlab_shared'

为了防止在配置 database_tasks: false 时丢失迁移,使用了专用的 Rake 任务 gitlab:db:validate_configgitlab:db:validate_config 通过检查每个底层数据库配置的数据库标识符来验证 database_tasks: 的正确性。 共享数据库的配置需要设置 database_tasks: falsegitlab:db:validate_config 总是在 db:migrate 之前运行。

验证

验证本质上使用 pg_query 来分析每个查询, 并根据 db/docs/ 中的信息对表进行分类。 如果指定的 gitlab_schema 不在给定数据库连接管理的 schema 列表之外,则迁移会被跳过。

Gitlab::Database::Migration[2.0] 包含 Gitlab::Database::MigrationHelpers::RestrictGitlabSchema, 它扩展了 #migrate 方法。在迁移期间,安装了一个专用的查询分析器 Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas,它接受由 restrict_gitlab_migration: 定义的允许 schema 列表。 如果执行的查询在允许的 schema 之外,则会引发异常。

异常

根据对 restrict_gitlab_migration 的误用或缺失,可能会引发各种异常, 作为迁移运行的一部分,并阻止迁移完成。

异常 1:在 DDL 模式下运行的迁移执行 DML 选择

class UpdateProjectsArchivedState < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!

  # 缺少:
  # restrict_gitlab_migration gitlab_schema: :gitlab_main

  def up
    update_column_in_batches(:projects, :archived, true) do |table, query|
      query.where(table[:archived].eq(false)) # rubocop:disable CodeReuse/ActiveRecord
    end
  end

  def down
    # no-op
  end
end
在 DDL(结构)模式下不允许使用 Select/DML 查询(SELECT/UPDATE/DELETE)
使用 'SELECT * FROM projects...'' 修改 'projects' (gitlab_main)

当前迁移未使用 restrict_gitlab_migration。缺少它表示迁移在DDL模式下运行, 但执行的有效负载似乎正在从 projects 读取数据。

解决方案是添加 restrict_gitlab_migration gitlab_schema: :gitlab_main

异常 2:在 DML 模式下运行的迁移更改结构

class AddUserIdAndStateIndexToMergeRequestReviewers < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!

  # 如果定义了 restrict_gitlab_migration 则表示 DML,应该删除
  restrict_gitlab_migration gitlab_schema: :gitlab_main

  INDEX_NAME = 'index_on_merge_request_reviewers_user_id_and_state'

  def up
    add_concurrent_index :merge_request_reviewers, [:user_id, :state], where: 'state = 2', name: INDEX_NAME
  end

  def down
    remove_concurrent_index_by_name :merge_request_reviewers, INDEX_NAME
  end
end
在 Select/DML (SELECT/UPDATE/DELETE) 模式下不允许使用 DDL(结构)查询。
使用 'CREATE INDEX...'' 修改 'merge_request_reviewers'

当前迁移确实使用了 restrict_gitlab_migration。存在它表示DML模式, 但执行的有效负载似乎在进行结构更改(DDL)。

解决方案是删除 restrict_gitlab_migration gitlab_schema: :gitlab_main

异常 3:在 DML 模式下运行的迁移访问另一个 schema 中的表

class UpdateProjectsArchivedState < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!

  # 由于它修改 `projects`,应该使用 `gitlab_main`
  restrict_gitlab_migration gitlab_schema: :gitlab_ci

  def up
    update_column_in_batches(:projects, :archived, true) do |table, query|
      query.where(table[:archived].eq(false)) # rubocop:disable CodeReuse/ActiveRecord
    end
  end

  def down
    # no-op
  end
end
Select/DML 查询 (SELECT/UPDATE/DELETE) 访问了 'projects' (gitlab_main) " \
它不在允许的 schema 列表中:'gitlab_ci'

当前迁移确实将迁移限制在 gitlab_ci,但似乎正在修改 gitlab_main 中的数据。

解决方案是更改 restrict_gitlab_migration gitlab_schema: :gitlab_ci

异常 4:混合 DDL 和 DML 模式

class UpdateProjectsArchivedState < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!

  # 无论规范如何,此迁移都无效,因为它不能同时修改结构和数据
  restrict_gitlab_migration gitlab_schema: :gitlab_ci

  def up
    add_concurrent_index :merge_request_reviewers, [:user_id, :state], where: 'state = 2', name: 'index_on_merge_request_reviewers'
    update_column_in_batches(:projects, :archived, true) do |table, query|
      query.where(table[:archived].eq(false)) # rubocop:disable CodeReuse/ActiveRecord
    end
  end

  def down
    # no-op
  end
end

混合 DDLDML 的迁移根据操作的顺序会引发上述异常之一。

多数据库迁移的即将到来的变化

使用 gitlab_schema:restrict_gitlab_migration 被视为此功能的第一迭代, 用于根据上下文有选择地运行迁移。有可能对 DML 仅迁移添加额外的限制 (因为结构一致性可能会保持不变,直到进一步通知)以限制它们的运行时机。

一个可能的扩展是限制仅在特定环境中运行 DML 迁移:

restrict_gitlab_migration gitlab_schema: :gitlab_main, gitlab_env: :gitlab_com

后台迁移

当您使用:

  • track_jobs 设置为 true 的后台迁移,或
  • 批量后台迁移

迁移必须写入一个 jobs 表。所有后台迁移使用的 jobs 表都被标记为 gitlab_shared。您可以在迁移任何数据库中的表时使用这些迁移。

但是,在排队批次时,您必须根据迭代的表设置 restrict_gitlab_migration。 例如,如果您更新所有 projects,则设置 restrict_gitlab_migration gitlab_schema: :gitlab_main。 但是,如果您更新所有 ci_pipelines,则设置 restrict_gitlab_migration gitlab_schema: :gitlab_ci

与所有 DML 迁移一样,您不能在 restrict_gitlab_migrationgitlab_shared 之外查询另一个数据库。 如果您需要查询另一个数据库,请分离迁移。

因为后台迁移的实际迁移逻辑(不是排队步骤)在 Sidekiq worker 中运行, 所以逻辑可以对任何数据库中的表执行 DML 查询,就像任何普通的 Sidekiq worker 一样。

如何确定给定表的 gitlab_schema

请参见数据库字典