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

多数据库架构

为支持 GitLab 进一步扩展,我们将 GitLab 应用数据库拆分为多个数据库。主要数据库包括 maincisec。GitLab 支持使用一个、两个或三个数据库运行。在 GitLab.com 上,我们使用独立的 maincisec 数据库。

为构建 Cells 架构,我们进一步拆分数据库,引入了另一个数据库 gitlab_main_clusterwide

GitLab 模式

为正确发现不同数据库间的允许模式,GitLab 应用实现了数据库字典

数据库字典将表虚拟分类为 gitlab_schema,概念上类似于 PostgreSQL Schema。在使用数据库模式更好地隔离 CI 拆分功能的决策中,由于迁移程序复杂,我们决定不使用 PostgreSQL 模式,而是实现了应用层分类的概念。GitLab 的每个表都需要分配一个 gitlab_schema

Schema 描述 备注
gitlab_main 参见 Cells / Organizations schemas
gitlab_main_cell 参见 Cells / Organizations schemas
gitlab_main_cell_setting 参见 Cells / Organizations schemas
gitlab_main_clusterwide 参见 Cells / Organizations schemas
gitlab_main_cell_local 参见 Cells / Organizations schemas
gitlab_ci 存储在 ci: 数据库中的所有 CI 表(例如 ci_pipelinesci_builds
gitlab_ci_cell_local 参见 Cells / Organizations schemas
gitlab_geo 存储在 geo: 数据库中的所有 Geo 表(例如 project_registrysecondary_usage_data
gitlab_shared 包含所有拆分数据库间数据的应用表(例如继承自 Gitlab::Database::SharedModel 的模型对应的 loose_foreign_keys_deleted_records
gitlab_internal Rails 和 PostgreSQL 的所有内部表(例如 ar_internal_metadataschema_migrationspg_*
gitlab_pm 存储 package_metadata 的所有表 gitlab_main 的别名,未来将替换为 gitlab_sec
gitlab_sec 存储在 sec: 数据库中的所有安全与漏洞功能表 拆分进行中

更多模式将随额外拆分数据库引入

模式的使用强制要求使用基类:

  • gitlab_main/gitlab_main_cell 使用 ApplicationRecord
  • gitlab_ci 使用 Ci::ApplicationRecord
  • gitlab_geo 使用 Geo::TrackingBase
  • gitlab_shared 使用 Gitlab::Database::SharedModel
  • gitlab_pm 使用 PackageMetadata::ApplicationRecord
  • gitlab_sec 使用 SecApplicationRecord

为所有单元本地表定义分片键

此内容已移至新位置

gitlab_schema 的影响

gitlab_schema 的使用对应用有显著影响。其主要目的是引入不同数据访问模式间的屏障。

这主要用作以下分类的来源:

gitlab_shared 的特殊用途

gitlab_shared 是一个特例,描述按设计包含所有拆分数据库数据的表或视图。此分类描述应用定义的表(如 loose_foreign_keys_deleted_records)。

注意:使用 gitlab_shared 需谨慎,因为它在访问数据时需要特殊处理。由于 gitlab_shared 共享结构和数据,应用必须按顺序遍历所有数据库中的所有数据来编写代码:

Gitlab::Database::EachDatabase.each_model_connection([MySharedModel]) do |connection, connection_name|
  MySharedModel.select_all_data...
end

因此,修改 gitlab_shared 表数据的迁移需要在所有拆分数据库中运行。

gitlab_internal 的特殊用途

gitlab_internal 描述 Rails 定义的表(如 schema_migrationsar_internal_metadata)以及内部 PostgreSQL 表(例如 pg_attribute)。其主要目的是支持其他数据库,如 Geo,这些数据库可能缺少某些应用定义的 gitlab_shared 表(如 loose_foreign_keys_deleted_records),但仍是有效的 Rails 数据库。

gitlab_pm 的特殊用途

gitlab_pm 存储描述公共仓库的包元数据。这些数据用于许可证合规性和依赖扫描产品类别,由组合分析组维护。它是 gitlab_main 的别名,旨在简化未来路由到不同数据库的操作。

迁移

请阅读多数据库迁移指南

CI 和 Sec 数据库

配置单数据库

默认情况下,GDK 配置为使用多个数据库运行。

不建议在同一开发实例中在单数据库和多数据库间切换。在单数据库模式下,cisec 数据库中的任何数据都将无法访问。单数据库应使用独立的开发实例。

配置 GDK 使用单数据库:

  1. 在 GDK 根目录运行:

    gdk config set gitlab.rails.databases.ci.enabled false
    gdk config set gitlab.rails.databases.sec.enabled false
  2. 重新配置 GDK:

    gdk reconfigure

要切换回使用多个数据库,将 gitlab.rails.databases.<db_name>.enabled 设置为 true 并运行 gdk reconfigure

移除 main 表与非 main 表的连接

跨数据库连接的查询会引发错误。在 GitLab 14.3 引入,仅适用于新查询。现有查询不会引发错误。

由于 GitLab 可使用多个独立数据库运行,无法在单个查询中同时引用 main 表和非 main 表。因此,SQL 查询中使用任何类型的 JOIN 都将无效。

移除跨数据库连接的建议

以下是一些被识别为跨数据库连接的实际案例及可能的修复方案:

移除代码

我们多次见到的最简单解决方案是移除未使用的现有作用域。这是最容易修复的案例。因此第一步是调查代码是否未被使用,然后移除它。以下是一些实际案例:

可能存在更多使用代码的案例,但我们可以评估是否需要它或功能是否应如此实现。在通过添加新列和表使问题复杂化之前,考虑是否可以简化方案并仍满足要求。一个正在评估的案例涉及更改某些 UsageData 的计算方式,以移除 https://gitlab.com/gitlab-org/gitlab/-/issues/336170 中的连接查询。这是一个很好的评估候选,因为 UsageData 对用户不关键,可能通过更简单的方法获得同样有用的指标。或者我们可能发现无人使用这些指标,因此可以移除它们。

使用 preload 替代 includes

Rails 中的 includespreload 方法都是避免 N+1 查询的方式。includes 方法使用启发式方法判断是否需要连接表,或是否可以在单独查询中加载所有记录。此方法假设如果需要查询其他表的列则需要连接,但有时判断错误,在不必要时执行连接。在这种情况下,使用 preload 在单独查询中显式加载数据可避免连接,同时避免 N+1 查询。

实际案例参考:https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67655

移除冗余连接

有时查询会执行多余的(或冗余的)连接。

常见场景:查询通过具有外键的表 BA 连接到 C。当只需统计 C 中的行数,且 B 中的外键具有 NOT NULL 约束时,统计 B 中的行可能就足够了。例如,在 MR 71811 中,原代码执行 project.runners.count,生成如下查询:

select count(*) from projects
inner join ci_runner_projects on ci_runner_projects.project_id = projects.id
where ci_runner_projects.runner_id IN (1, 2, 3)

通过将代码更改为 project.runner_projects.count 避免跨连接,生成相同结果的查询:

select count(*) from ci_runner_projects
where ci_runner_projects.runner_id IN (1, 2, 3)

另一个常见冗余连接是连接到另一张表后按主键过滤,而实际上可以通过外键过滤。案例参考 MR 71614。原代码 joins(scan: :build).where(ci_builds: { id: build_ids }) 生成:

select ...
inner join security_scans
inner join ci_builds on security_scans.build_id = ci_builds.id
where ci_builds.id IN (1, 2, 3)

security_scans 已有外键 build_id,可改为 joins(:scan).where(security_scans: { build_id: build_ids }),生成相同结果的查询:

select ...
inner join security_scans
where security_scans.build_id IN (1, 2, 3)

这两种移除冗余连接的案例不仅消除了跨连接,还生成了更简单快速的查询。

限制性 pluck 后跟 find

除非返回的数组大小有保证,否则不建议使用 pluckpick 获取 id 数组。通常适用于结果最多为 1 的情况,或内存中已有等长的 id(或用户名)列表需要映射到另一列表的情况。在一对多关系中映射 id 列表时不适用,因为结果无界。然后可以使用返回的 id 获取相关记录:

allowed_user_id = board_user_finder
  .where(user_id: params['assignee_id'])
  .pick(:user_id)

User.find_by(id: allowed_user_id)

实际案例参考:https://gitlab.com/gitlab-org/gitlab/-/merge_requests/126856

有时将连接转换为 pluck 看起来简单,但通常会导致加载无界的 id 到内存中,然后在后续查询中重新序列化回 PostgreSQL。这些方案无法扩展,建议尝试其他选项。可能想到对 pluck 数据应用 limit 以限制内存,但这会引入不可预测的用户结果,尤其对最大客户(包括我们自己)影响最大,因此不建议这样做。

反规范化外键到表中

反规范化指向表中添加冗余的预计算(重复)数据以简化查询或提高性能。当涉及三张表的连接(通过中间表)时可能有用。

通常建模数据库架构时,优先使用"规范化"结构,原因如下:

  • 重复数据占用额外存储
  • 重复数据需保持同步

有时规范化数据性能较低,反规范化是 GitLab 长期用于提高数据库查询性能的常用技术。当满足以下条件时,上述问题得到缓解:

  1. 数据量不大(例如仅整数列)
  2. 数据不常更新(例如大多数表的 project_id 几乎从不更新)

我们发现的案例是 terraform_state_versions 表。该表有外键 terraform_state_versions.ci_build_id 可连接到构建,因此可连接到项目:

select projects.* from terraform_state_versions
inner join ci_builds on terraform_state_versions.ci_build_id = ci_builds.id
inner join projects on ci_builds.project_id = projects.id

问题是 ci_builds 与其他两张表位于不同数据库。

解决方案是向 terraform_state_versions 添加 project_id 列。这不会占用太多额外存储,且由于这些功能的工作方式,它从不更新(构建不会移动项目)。简化后的查询:

select projects.* from terraform_state_versions
inner join projects on terraform_state_versions.project_id = projects.id

这也提高了性能,因为无需通过额外表连接。

实现案例参考:https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66963。该 MR 还反规范化了 pipeline_id 以修复类似查询。

反规范化到额外表

有时前一种反规范化(添加额外列)不适用于特定场景。可能因为数据不是 1:1 关系,或目标表已过宽(例如 projects 表不应添加更多列)。

此时可决定将额外数据存储在单独表中。

一个使用此方法的案例是实现 Project.with_code_coverage 作用域。此作用域用于将项目列表缩小到曾使用过代码覆盖功能的项目。查询(简化版):

select projects.* from projects
inner join ci_daily_build_group_report_results on ci_daily_build_group_report_results.project_id = projects.id
where ((data->'coverage') is not null)
and ci_daily_build_group_report_results.default_branch = true
group by projects.id

此工作仍在进行中,当前计划引入名为 projects_with_ci_feature_usage 的新表,包含两列 project_idci_feature。当项目首次为代码覆盖创建 ci_daily_build_group_report_results 时会写入此表。新查询:

select projects.* from projects
inner join projects_with_ci_feature_usage on projects_with_ci_feature_usage.project_id = projects.id
where projects_with_ci_feature_usage.ci_feature = 'code_coverage'

上述示例为简化使用文本列,但可能应使用 enum 节省空间。

新设计的缺点是可能需要更新(如果删除 ci_daily_build_group_report_results 则需删除)。但根据领域不同,这可能不必要,因为删除是边缘情况或不可能,或在列表页看到项目对用户影响不大。也可以在需要时实现删除这些行的逻辑。

最后,此反规范化新查询也提高了性能,因为它减少了连接和过滤。

has_onehas_many through: 关系使用 disable_joins

有时使用 has_one ... through:has_many ... through: 跨不同数据库表时会导致连接查询。有时可通过添加 disable_joins:true 解决。这是 Rails 功能,我们已向后移植。我们还扩展了该功能以允许使用 lambda 语法通过功能标志启用 disable_joins。如果使用此功能,建议使用功能标志,因为如果出现严重性能回归可降低风险。

实际案例参考:https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66709/diffs

更改数据库查询时,分析比较变更前后的 SQL 至关重要。disable_joins 可能根据 has_manyhas_one 关系的实际逻辑引入性能极差的代码。关键点是检查用于构建最终结果集的任何中间结果集是否加载了无界数据。最佳方式是查看生成的 SQL 并确认每个查询都有限制(例如 LIMIT 1 子句或基于唯一列的 WHERE 子句)。任何无界中间数据集都可能导致加载过多 ID 到内存中。

可能看到极差性能的示例代码:

class Project
  has_many :pipelines
  has_many :builds, through: :pipelines
end

class Pipeline
  has_many :builds
end

class Build
  belongs_to :pipeline
end

def some_action
  @builds = Project.find(5).builds.order(created_at: :desc).limit(10)
end

上述 some_action 将生成:

select * from builds
inner join pipelines on builds.pipeline_id = pipelines.id
where pipelines.project_id = 5
order by builds.created_at desc
limit 10

但如果将关系改为:

class Project
  has_many :pipelines
  has_many :builds, through: :pipelines, disable_joins: true
end

将得到以下两个查询:

select id from pipelines where project_id = 5;

select * from builds where pipeline_id in (...)
order by created_at desc
limit 10;

由于第一个查询未按唯一列限制或无 LIMIT 子句,可能加载无限数量的 pipeline ID 到内存中,然后在后续查询中发送。这可能导致 Rails 应用和数据库性能极差。在这种情况下,可能需要重写查询或查看其他移除跨连接的模式。

如何验证已正确移除跨连接

RSpec 配置为自动验证所有 SQL 查询不跨数据库连接。如果此验证在 spec/support/database/cross-join-allowlist.yml 中被禁用,仍可使用 with_cross_joins_prevented 验证隔离代码块:

it 'does not join across databases' do
  with_cross_joins_prevented do
    ::Ci::Build.joins(:project).to_a
  end
end

如果查询跨两个数据库连接,将引发异常。通过移除连接修复上述示例:

it 'does not join across databases' do
  with_cross_joins_prevented do
    ::Ci::Build.preload(:project).to_a
  end
end

实际案例参考:https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67655

现有跨连接的允许列表

识别跨连接的最简单方式是通过失败的流水线。

例如,在 !130038 中,我们将 notification_settings 表移动到 gitlab_main_cell 模式,通过在 db/docs/notification_settings.yml 文件中标记实现。

流水线因以下错误失败:

Database::PreventCrossJoins::CrossJoinAcrossUnsupportedTablesError:

Unsupported cross-join across 'users, notification_settings' querying 'gitlab_main_clusterwide, gitlab_main_cell' discovered when executing query 'SELECT "users".* FROM "users" WHERE "users"."id" IN (SELECT "notification_settings"."user_id" FROM ((SELECT "notification_settings"."user_id" FROM "notification_settings" WHERE "notification_settings"."source_id" = 119 AND "notification_settings"."source_type" = 'Project' AND (("notification_settings"."level" = 3 AND EXISTS (SELECT true FROM "notification_settings" "notification_settings_2" WHERE "notification_settings_2"."user_id" = "notification_settings"."user_id" AND "notification_settings_2"."source_id" IS NULL AND "notification_settings_2"."source_type" IS NULL AND "notification_settings_2"."level" = 2)) OR "notification_settings"."level" = 2))) notification_settings)'

要使流水线通过,必须将此跨连接查询加入允许列表。

可通过将代码包装在 ::Gitlab::Database.allow_cross_joins_across_databases 辅助方法中显式允许跨数据库连接。替代方式是将给定关系标记为 relation.allow_cross_joins_across_databases

此方法仅应用于:

  • 现有代码
  • 帮助迁移远离跨连接的代码(例如在迁移中为未来使用回填数据以移除跨连接)

allow_cross_joins_across_databases 辅助方法使用示例:

# 限制执行数据库对象的代码块
::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336590') do
  subject.perform(1, 4)
end
# 将关系标记为允许跨数据库连接
def find_diff_head_pipeline
  all_pipelines
    .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336891')
    .for_sha_or_source_sha(diff_head_sha)
    .first
end

在模型关联或作用域中,可如下使用:

class Group < Namespace
 has_many :users, -> {
    allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/422405")
  }, through: :group_members
end

覆盖关联可能产生意外后果甚至导致数据丢失,如我们在 issue 424307 中注意到的。不要覆盖现有 ActiveRecord 关联来标记跨连接为允许,如下例所示。

class Group < Namespace
  has_many :users, through: :group_members

  # 不要这样覆盖关联。
  def users
    super.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/422405")
  end
end

url 参数应指向一个包含里程碑的问题,用于计划修复跨连接。如果跨连接用于迁移中,则无需修复代码。详情请见 https://gitlab.com/gitlab-org/gitlab/-/issues/340017

移除跨数据库事务

处理多个数据库时,需特别注意影响多个数据库的数据修改。在 GitLab 14.4 引入,自动检查会阻止跨数据库修改。

当在任意数据库服务器上启动的事务修改至少两个不同数据库时,应用会触发跨数据库修改错误(仅在测试环境)。

示例:

# 在主数据库上开启事务
ApplicationRecord.transaction do
  ci_build.update!(updated_at: Time.current) # 更新 CI 数据库
  ci_build.project.update!(updated_at: Time.current) # 更新主数据库
end
# 引发错误:在修改 'ci_build, projects' 表的事务中检测到 'main, ci' 的跨数据库数据修改

上述代码在事务中更新两个记录的时间戳。随着 CI 数据库拆分的持续工作,我们无法确保数据库事务的完整性。如果第二个更新查询失败,第一个更新查询不会回滚,因为 ci_build 记录位于不同的数据库服务器上。更多信息请参阅事务指南页面。

修复跨数据库事务

可通过将代码包装在 Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction 辅助方法中显式允许跨数据库事务。

此方法仅应用于现有代码。

temporary_ignore_tables_in_transaction 辅助方法使用示例:

class GroupMember < Member
   def update_two_factor_requirement
     return unless user

     # 标记并忽略涉及 members 和 users/user_details/user_preferences 的跨数据库事务
     Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction(
       %w[users user_details user_preferences], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424288'
     ) do
       user.update_two_factor_requirement
     end
   end
end
移除事务块

没有打开的事务,跨数据库修改检查无法引发错误。此更改牺牲了一致性。如果在第一个 UPDATE 查询后应用程序失败,第二个 UPDATE 查询将永远不会执行。

transaction 块的相同代码:

ci_build.update!(updated_at: Time.current) # CI 数据库
ci_build.project.update!(updated_at: Time.current) # 主数据库
异步处理

如果需要更保证操作一致性,可在后台作业中执行。后台作业异步调度,出错时重试多次。仍存在引入极小不一致的可能性。

示例:

current_time = Time.current

MyAsyncConsistencyJob.perform_async(cu_build.id)

ci_build.update!(updated_at: current_time)
ci_build.project.update!(updated_at: current_time)

MyAsyncConsistencyJob 也会尝试在时间戳不同时更新。

追求完美一致性

目前,我们尚无工具(甚至可能不需要)来确保与单数据库类似的特性。如果您认为正在处理的代码需要这些特性,可通过包装有问题的测试代码并创建后续问题来禁用跨数据库修改检查:

allow_cross_database_modification_within_transaction(url: 'gitlab issue URL') do
  ApplicationRecord.transaction do
    ci_build.update!(updated_at: Time.current) # 更新 CI 数据库
    ci_build.project.update!(updated_at: Time.current) # 更新主数据库
  end
end
``
如有疑问,请随时联系 [Tenant Scale ](https://handbook.gitlab.com/handbook/engineering/infrastructure-platforms/tenant-scale/) 获取建议。

##### 避免跨数据库使用 `dependent: :nullify` 和 `dependent: :destroy`
有时我们可能希望跨数据库使用 `dependent: :nullify`  `dependent: :destroy`。这在技术上是可行的,但存在问题,因为这些钩子在 `#destroy` 调用的外部事务上下文中运行,这会创建跨数据库事务而我们正试图避免。这种方式导致的跨数据库事务在切换到拆分架构时可能产生令人困惑的结果,因为现在某些查询发生在事务外部,它们可能部分应用而外部事务失败,可能导致意外错误。

对于需要在数据库外清理数据(例如对象存储)的非平凡对象,建议设置 [`dependent: :restrict_with_error`](https://guides.rubyonrails.org/association_basics.html#options-for-has-one-dependent)。此类对象应提前显式移除。使用 `dependent: :restrict_with_error` 可确保在未清理完时禁止销毁父对象。

如果只需要从 PostgreSQL 中清理子记录,请考虑使用[松散外键](loose_foreign_keys.md)

## 跨数据库的外键
我们有许多地方使用跨数据库引用的外键。在两个独立的 PostgreSQL 数据库中无法实现此功能,因此我们需要以高性能方式复制 PostgreSQL 提供的行为。我们不能也不应尝试复制 PostgreSQL 防止创建无效引用的数据保证,但我们仍需要一种方法来替代级联删除,以避免出现孤立数据或指向不存在的记录,这可能导致错误。因此我们创建了["松散外键"](loose_foreign_keys.md),这是一种异步清理孤立记录的机制。

### 现有跨数据库外键的允许列表
识别跨数据库外键的最简单方式是通过失败的流水线。

例如,在 [!130038](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130038/diffs) 中,我们将 `notification_settings` 表移动到 `gitlab_main_cell` 模式,通过在 `db/docs/notification_settings.yml` 文件中标记实现。

`notification_settings.user_id` 是指向 `users` 的列,但 `users` 表属于不同数据库,因此现在被视为跨数据库外键。

我们在 [`no_cross_db_foreign_keys_spec.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/01d3a1e41513200368a22bbab5d4312174762ee0/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb) 中有一个规范来捕获此类跨数据库外键情况,如果遇到此类外键则会失败。

要使流水线通过,必须将此跨数据库外键加入允许列表。

为此,通过在相同规范中将其添加为异常(如[此示例](https://gitlab.com/gitlab-org/gitlab/-/blob/7d99387f399c548af24d93d564b35f2f9510662d/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb#L26))显式允许现有跨数据库外键存在。这样规范就不会失败。

稍后,此外键可转换为松散外键,如我们在 [!130080](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130080/diffs) 中所做。

## 多数据库测试
在我们的测试 CI 流水线中,默认使用设置好的多个数据库(包括 `main`  `ci` 数据库)测试 GitLab。但在合并请求中,例如当我们修改某些数据库相关代码或为 MR 添加标签 `~"pipeline:run-single-db"` 时,我们还会在[另外两种数据库模式](../pipelines/_index.md#single-database-testing)下运行测试:`single-db` 和 `single-db-ci-connection`。

为处理测试需要在特定数据库模式下运行的情况,我们有一些 RSpec 辅助方法来限制测试可运行的模式,并在其他模式下跳过。

| 辅助方法名                                 | 测试运行环境 |
|---------------------------------------------| ---      |
| `skip_if_shared_database(:ci)`              | **多数据库**   |
| `skip_if_database_exists(:ci)`              | **单数据库**  **单数据库 CI 连接**   |
| `skip_if_multiple_databases_are_setup(:ci)` |  **单数据库**   |
| `skip_if_multiple_databases_not_setup(:ci)` | **单数据库 CI 连接**  **多数据库**   |

## 锁定不属于数据库模式的表的写入
当独立数据库被提升并与主数据库分离时,为防止出现脑裂情况,作为额外保护措施,请运行 Rake 任务 `gitlab:db:lock_writes`。此命令锁定以下表的写入:
- 属于主数据库或安全数据库的 `gitlab_ci` 中的遗留表
- 属于 CI 数据库或安全数据库的 `gitlab_main` 中的遗留表
- 属于 CI 数据库或主数据库的 `gitlab_sec` 中的遗留表

 Rake 任务为所有表添加触发器,防止对需要锁定的表执行任何 `INSERT``UPDATE``DELETE`  `TRUNCATE` 语句。

如果此任务在仅使用单个数据库同时包含 `gitlab_main`  `gitlab_ci` 表的 GitLab 设置上运行,则不会锁定任何表。

要撤销操作,运行相反的 Rake 任务:`gitlab:db:unlock_writes`

### 监控
使用 [`Database::MonitorLockedTablesWorker`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/workers/database/monitor_locked_tables_worker.rb) 检查表锁状态。如果需要,它会锁定表。

此脚本结果可在 [Kibana](https://log.gprd.gitlab.net/app/r/s/4qrz2) 中查看。如果计数不为 0,则存在一些本应锁定但未锁定的表。字段 `json.extra.database_monitor_locked_tables_worker.results.ci.tables_need_locks`  `json.extra.database_monitor_locked_tables_worker.results.main.tables_need_locks` 应包含状态错误的表列表。

日志通过 [Elasticsearch Watcher](https://log.gprd.gitlab.net/app/management/insightsAndAlerting/watcher/watches) 监控。观察器名为 `table_locks_needed`,源代码在 [GitLab Runbook 仓库](https://gitlab.com/gitlab-com/runbooks/-/tree/master/elastic/managed-objects/log_gprd/watches)。警报发送到 [#g_tenant-scale](https://gitlab.enterprise.slack.com/archives/C01TQ838Y3T) Slack 频道。

### 自动化
有两个进程会自动锁定表:
- 数据库迁移。参见 [`Gitlab::Database::MigrationHelpers::AutomaticLockWritesOnTables`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/database/migration_helpers/automatic_writes_on_tables.rb)
- `Database::MonitorLockedTablesWorker` 在需要时锁定表。可通过 `lock_tables_in_monitoring` 功能标志禁用。

`Gitlab::Database::MigrationHelpers::AutomaticLockWritesOnTables` 比较每次数据库迁移运行前后的表列表,然后锁定相关数据库中新增的表。这不涵盖所有情况。因为某些迁移需要在同一事务性迁移中重新创建表。例如:将标准非分区表转换为分区表。案例参考 [此示例](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/184636)。此类迁移会重新创建表但不锁定。但 `Database::MonitorLockedTablesWorker` 的每日 cron 作业负责在 Slack 上发出警报,然后自动锁定这些表的写入。

### 手动锁定表
如果需要手动锁定表,请使用数据库迁移。创建常规迁移并添加锁定表的代码。例如,在 CI 数据库中为 `shards` 表设置写入锁:
```ruby
class EnableWriteLocksOnShards < Gitlab::Database::Migration[2.2]
  def up
    # 在主数据库上,迁移应跳过
    # 我们无法在 DDL 迁移中使用 restrict_gitlab_migration
    return if Gitlab::Database.db_config_name(connection) != 'ci'

    Gitlab::Database::LockWritesManager.new(
      table_name: 'shards',
      connection: connection,
      database_name: :ci,
      with_retries: false
    ).lock_writes
  end

  def down
    # 无操作
  end
end

截断表

main 的独立数据库完全分离后,我们可以通过截断表释放磁盘空间。这将减少数据集:例如,CI 数据库中 users 表的数据不再被读取或更新,因此可通过截断表移除此数据。

为此,GitLab 提供了单独的 Rake 任务,每个数据库一个:

  • gitlab:db:truncate_legacy_tables:main 将截断主数据库中的遗留表
  • gitlab:db:truncate_legacy_tables:ci 将截断 CI 数据库中的遗留表
  • gitlab:db:truncate_legacy_tables:sec 将截断安全数据库中的遗留表

这些任务只能在表的写入被锁定时运行。

本节示例使用 DRY_RUN=true。这确保不会实际截断任何数据。GitLab 强烈建议在运行不带 DRY_RUN=true 的这些任务前进行备份。

这些任务可选择查看操作而不实际更改数据:

$ sudo DRY_RUN=true gitlab-rake gitlab:db:truncate_legacy_tables:main
I, [2023-07-14T17:08:06.665151 #92505]  INFO -- : DRY RUN:
I, [2023-07-14T17:08:06.761586 #92505]  INFO -- : Truncating legacy tables for the database main
I, [2023-07-14T17:08:06.761709 #92505]  INFO -- : SELECT set_config('lock_writes.ci_build_needs', 'false', false)
I, [2023-07-14T17:08:06.765272 #92505]  INFO -- : SELECT set_config('lock_writes.ci_build_pending_states', 'false', false)
I, [2023-07-14T17:08:06.768220 #92505]  INFO -- : SELECT set_config('lock_writes.ci_build_report_results', 'false', false)
[...]
I, [2023-07-14T17:08:06.957294 #92505]  INFO -- : TRUNCATE TABLE ci_build_needs, ci_build_pending_states, ci_build_report_results, ci_build_trace_chunks, ci_build_trace_metadata, ci_builds, ci_builds_metadata, ci_builds_runner_session, ci_cost_settings, ci_daily_build_group_report_results, ci_deleted_objects, ci_freeze_periods, ci_group_variables, ci_instance_variables, ci_job_artifact_states, ci_job_artifacts, ci_job_token_project_scope_links, ci_job_variables, ci_minutes_additional_packs, ci_namespace_mirrors, ci_namespace_monthly_usages, ci_partitions, ci_pending_builds, ci_pipeline_artifacts, ci_pipeline_chat_data, ci_pipeline_messages, ci_pipeline_metadata, ci_pipeline_schedule_variables, ci_pipeline_schedules, ci_pipeline_variables, ci_pipelines, ci_pipelines_config, ci_platform_metrics, ci_project_mirrors, ci_project_monthly_usages, ci_refs, ci_resource_groups, ci_resources, ci_runner_machines, ci_runner_namespaces, ci_runner_projects, ci_runner_versions, ci_runners, ci_running_builds, ci_secure_file_states, ci_secure_files, ci_sources_pipelines, ci_sources_projects, ci_stages, ci_subscriptions_projects, ci_trigger_requests, ci_triggers, ci_unit_test_failures, ci_unit_tests, ci_variables, external_pull_requests, p_ci_builds, p_ci_builds_metadata, p_ci_runner_machine_builds, taggings, tags RESTRICT

任务首先找出需要截断的表。截断将分阶段进行,因为我们需要限制单个数据库事务中移除的数据量。表根据外键定义按特定顺序处理。通过在调用任务时添加数字可更改每个阶段处理的表数量。默认值为 5:

sudo DRY_RUN=true gitlab-rake gitlab:db:truncate_legacy_tables:main\[10\]

也可通过设置 UNTIL_TABLE 变量限制截断的表数量。例如,此处理将在截断 ci_unit_test_failures 后停止:

sudo DRY_RUN=true UNTIL_TABLE=ci_unit_test_failures gitlab-rake gitlab:db:truncate_legacy_tables:main