多数据库迁移
本文档描述了如何为使用多个数据库的解耦 GitLab 应用正确编写数据库迁移。 更多信息请参见多数据库。
多数据库的设计(Geo 数据库除外)假设所有解耦的数据库都具有相同的结构(例如 schema),但每个数据库中的数据是不同的。这意味着某些表在每个数据库中不包含数据。
操作
根据使用的构造,我们可以将迁移分为以下几类:
- 修改结构(DDL - Data Definition Language)(例如
ALTER TABLE)。 - 修改数据(DML - Data Manipulation Language)(例如
UPDATE)。 - 执行其他查询(例如
SELECT),在我们的迁移中被视为 DML。
使用 Gitlab::Database::Migration[2.0] 要求迁移必须始终具有单一目的。
迁移不能混合 DDL 和 DML 的更改,因为应用程序要求所有解耦数据库的结构
(由 db/structure.sql 描述)必须完全相同。
数据定义语言 (DDL)
DDL 迁移包括所有:
- 创建或删除表(例如
create_table)。 - 添加或删除索引(例如
add_index、add_concurrent_index)。 - 添加或删除外键(例如
add_foreign_key、add_concurrent_foreign_key)。 - 添加或删除列,带或不带默认值(例如
add_column)。 - 创建或删除触发器函数(例如
create_trigger_function)。 - 将触发器附加或分离到表(例如
track_record_deletions、untrack_record_deletions)。 - 准备或不准备异步索引(例如
prepare_async_index、unprepare_async_index_by_name)。 - 清空表(例如使用
truncate_tables!辅助方法)。
因此 DDL 迁移不能:
- 通过 SQL 语句或 ActiveRecord 模型以任何形式读取或修改数据。
- 更新列值(例如
update_column_in_batches)。 - 计划后台迁移(例如
queue_background_migration_jobs_by_range_at_intervals)。 - 读取功能标志的状态,因为它们存储在
main:中(features和feature_gates表)。 - 读取应用程序设置(因为设置存储在
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示例:添加新表存储在单个数据库中
-
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 -
在 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 迁移包括所有:
- 通过 SQL 语句读取数据(例如
SELECT * FROM projects WHERE id=1)。 - 通过 ActiveRecord 模型读取数据(例如
User < MigrationRecord)。 - 通过 ActiveRecord 模型创建、更新或删除数据(例如
User.create!(...))。 - 通过 SQL 语句创建、更新或删除数据(例如
DELETE FROM projects WHERE id=1)。 - 批量更新列(例如
update_column_in_batches(:projects, :archived, true))。 - 计划后台迁移(例如
queue_background_migration_jobs_by_range_at_intervals)。 - 访问应用程序设置(例如,如果为
main:数据库运行,则ApplicationSetting.last)。 - 如果为
main:数据库运行,则读取和修改功能标志。
DML 迁移不能:
- 对 DDL 进行任何更改,因为这会破坏保持
structure.sql在所有解耦数据库中一致性的规则。 - 从另一个数据库读取数据。
要指示 DML 迁移类型,迁移必须在迁移类中使用 restrict_gitlab_migration gitlab_schema: 语法。
这会将给定的迁移标记为 DML 并限制对其的访问。
示例:仅在包含给定 gitlab_schema 的数据库上下文中执行 DML
示例迁移更新 projects 的 archived 列,该迁移仅对包含 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
endgitlab_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),
则每个迁移为每个数据库配置运行:
- DDL 迁移将所有结构更改应用到所有数据库。
- DML 迁移仅在包含给定
gitlab_schema:的数据库上下文中运行。 - 如果 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_config。
gitlab:db:validate_config 通过检查每个底层数据库配置的数据库标识符来验证 database_tasks: 的正确性。
共享数据库的配置需要设置 database_tasks: false。gitlab: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
endSelect/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混合 DDL 和 DML 的迁移根据操作的顺序会引发上述异常之一。
多数据库迁移的即将到来的变化
使用 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_migration 或 gitlab_shared 之外查询另一个数据库。
如果您需要查询另一个数据库,请分离迁移。
因为后台迁移的实际迁移逻辑(不是排队步骤)在 Sidekiq worker 中运行, 所以逻辑可以对任何数据库中的表执行 DML 查询,就像任何普通的 Sidekiq worker 一样。
如何确定给定表的 gitlab_schema
请参见数据库字典。