What requires downtime?

原文:https://docs.gitlab.com/ee/development/what_requires_downtime.html

What requires downtime?

使用数据库时,可以在不使 GitLab 脱机的情况下执行某些操作,其他操作确实需要停机时间. 本指南介绍了各种操作,其影响以及如何在不停机的情况下执行这些操作.

Dropping Columns

删除列很棘手,因为正在运行的 GitLab 进程可能仍在使用这些列. 为了安全地解决此问题,您需要在三个版本中执行三个步骤:

  1. 忽略列(版本 M)
  2. 删除列(版本 M + 1)
  3. 删除忽略规则(版本 M + 2)

之所以将其分布在三个发行版中,是因为删除列是一种破坏性操作,不易回滚.

遵循此过程可帮助我们确保没有部署到 GitLab.com 并升级将这些步骤集中在一起的自我管理安装的过程.

Step 1: Ignoring the column (release M)

第一步是忽略应用程序代码中的列. 这是必要的,因为 Rails 缓存列并在各个地方重复使用此缓存. 这可以通过定义要忽略的列来完成. 例如,要忽略用户模型中的updated_at ,请使用以下命令:

  1. class User < ApplicationRecord
  2. include IgnorableColumns
  3. ignore_column :updated_at, remove_with: '12.7', remove_after: '2019-12-22'
  4. end

多列也可以忽略:

  1. ignore_columns %i[updated_at created_at], remove_with: '12.7', remove_after: '2019-12-22'

我们要求通过以下方式指示何时可以安全地删除列忽略:

  • remove_with :设置为 GitLab 版本,通常在添加列忽略后两个版本(M + 2).
  • remove_after :设置为一个日期,在该日期之后,我们认为通常可以在 M + 2 版本的开发周期内删除列忽略项.

这些信息使我们能够更好地推理列忽略,并确保对于常规发行版和部署到 GitLab.com 而言,我们都不会过早删除列忽略. 例如,这避免了我们部署大量更改的情况,其中包括同时忽略列的更改和随后删除列忽略的更改(这将导致停机).

在此示例中,忽略列的更改在 12.5 版中进行.

Step 2: Dropping the column (release M+1)

继续我们的示例,删除该列将进入版本 12.6 中的部署后迁移:

  1. remove_column :user, :updated_at

Step 3: Removing the ignore rule (release M+2)

在下一个版本中,在此示例 12.7 中,我们设置了另一个合并请求以删除忽略规则. 这将删除ignore_column行,并且如果不再需要,还将IgnoreableColumns .

只有在remove_after日期过去之后,才应将其与remove_with指示的发行版合并.

Renaming Columns

重命名列通常需要停机,因为在数据库迁移期间/之后,应用程序可能会继续使用旧的列名称. 要在不停机的情况下重命名列,我们需要两个迁移:常规迁移和部署后迁移. 这些迁移都可以在同一版本中进行.

Step 1: Add The Regular Migration

首先,我们需要创建常规迁移. 此迁移应当前使用Gitlab::Database::MigrationHelpers#rename_column_concurrently来执行重命名. 例如

  1. # A regular migration in db/migrate
  2. class RenameUsersUpdatedAtToUpdatedAtTimestamp < ActiveRecord::Migration[4.2]
  3. include Gitlab::Database::MigrationHelpers
  4. disable_ddl_transaction!
  5. def up
  6. rename_column_concurrently :users, :updated_at, :updated_at_timestamp
  7. end
  8. def down
  9. undo_rename_column_concurrently :users, :updated_at, :updated_at_timestamp
  10. end
  11. end

这将负责重命名列,确保数据保持同步,通过索引和外键进行复制等.

注意:如果一列包含 1 个或多个不包含原始列名称的索引,则上述过程将失败. 在这种情况下,您首先需要重命名这些索引.

Step 2: Add A Post-Deployment Migration

重命名过程需要在部署后迁移中进行一些清理. 我们可以使用Gitlab::Database::MigrationHelpers#cleanup_concurrent_column_rename来执行此清理:

  1. # A post-deployment migration in db/post_migrate
  2. class CleanupUsersUpdatedAtRename < ActiveRecord::Migration[4.2]
  3. include Gitlab::Database::MigrationHelpers
  4. disable_ddl_transaction!
  5. def up
  6. cleanup_concurrent_column_rename :users, :updated_at, :updated_at_timestamp
  7. end
  8. def down
  9. undo_cleanup_concurrent_column_rename :users, :updated_at, :updated_at_timestamp
  10. end
  11. end

注意:如果要重命名大表 ,请仔细考虑第一次迁移已运行但第二次清理迁移尚未运行的状态. 使用Canary ,系统可能会在此状态下运行大量时间.

Changing Column Constraints

通常,无需停机即可添加或删除NOT NULL子句(或其他约束). 但是,这确实需要首先部署所有应用程序更改. 因此,在部署后的迁移中应该发生更改列约束的情况.

避免使用change_column因为它会产生无效查询,因为它会重新定义整个列类型.

您可以针对每个特定用例查看以下指南:

Changing Column Types

可以使用Gitlab::Database::MigrationHelpers#change_column_type_concurrently来更改列的类型. 此方法的工作方式与rename_column_concurrently类似. 例如,假设我们要将users.username的类型从string更改为text .

Step 1: Create A Regular Migration

常规迁移用于创建具有临时名称的新列,并设置一些触发器以使数据保持同步. 这样的迁移如下所示:

  1. # A regular migration in db/migrate
  2. class ChangeUsersUsernameStringToText < ActiveRecord::Migration[4.2]
  3. include Gitlab::Database::MigrationHelpers
  4. disable_ddl_transaction!
  5. def up
  6. change_column_type_concurrently :users, :username, :text
  7. end
  8. def down
  9. cleanup_concurrent_column_type_change :users, :username
  10. end
  11. end

Step 2: Create A Post Deployment Migration

接下来,我们需要使用部署后迁移来清理更改:

  1. # A post-deployment migration in db/post_migrate
  2. class ChangeUsersUsernameStringToTextCleanup < ActiveRecord::Migration[4.2]
  3. include Gitlab::Database::MigrationHelpers
  4. disable_ddl_transaction!
  5. def up
  6. cleanup_concurrent_column_type_change :users, :username
  7. end
  8. def down
  9. change_column_type_concurrently :users, :username, :string
  10. end
  11. end

就是这样,我们完成了!

Casting data to a new type

某些类型更改需要将数据转换为新类型. 例如,从text更改为jsonb . 在这种情况下,请使用type_cast_function选项. 确保没有不良数据,并且投射将始终成功. 您还可以提供一个自定义函数来处理转换错误.

迁移示例:

  1. def up
  2. change_column_type_concurrently :users, :settings, :jsonb, type_cast_function: 'jsonb'
  3. end

Changing The Schema For Large Tables

虽然change_column_type_concurrentlyrename_column_concurrently可以用于在rename_column_concurrently机的情况下更改表的架构,但对于大型表来说,效果并不理想. 由于所有工作都是按顺序进行的,因此迁移可能需要很长时间才能完成,从而阻止了部署的进行. 由于数据库按顺序快速更新许多行,因此它们也可能给数据库带来很大压力.

为减轻数据库压力,在迁移大表中的列时(例如issues ),应改用change_column_type_using_background_migrationrename_column_using_background_migration . 这些方法的工作方式与并发的类似,但是使用后台迁移将工作/负载分散在更长的时间段内,而不会减慢部署速度.

例如,要使用后台迁移来更改列类型:

  1. class ExampleMigration < ActiveRecord::Migration[4.2]
  2. include Gitlab::Database::MigrationHelpers
  3. disable_ddl_transaction!
  4. class Issue < ActiveRecord::Base
  5. self.table_name = 'issues'
  6. include EachBatch
  7. def self.to_migrate
  8. where('closed_at IS NOT NULL')
  9. end
  10. end
  11. def up
  12. change_column_type_using_background_migration(
  13. Issue.to_migrate,
  14. :closed_at,
  15. :datetime_with_timezone
  16. )
  17. end
  18. def down
  19. change_column_type_using_background_migration(
  20. Issue.to_migrate,
  21. :closed_at,
  22. :datetime
  23. )
  24. end
  25. end

这将将issues.closed_at的类型更改为timestamp with time zone .

请记住,传递给change_column_type_using_background_migration的关系必须包含EachBatch ,否则将引发TypeError .

然后,此迁移需要在单独的发行版( 而不是补丁程序发行版)中进行清除迁移,该清除迁移应从队列中窃取并处理所有剩余的行. 例如:

  1. class MigrateRemainingIssuesClosedAt < ActiveRecord::Migration[4.2]
  2. include Gitlab::Database::MigrationHelpers
  3. DOWNTIME = false
  4. disable_ddl_transaction!
  5. class Issue < ActiveRecord::Base
  6. self.table_name = 'issues'
  7. include EachBatch
  8. end
  9. def up
  10. Gitlab::BackgroundMigration.steal('CopyColumn')
  11. Gitlab::BackgroundMigration.steal('CleanupConcurrentTypeChange')
  12. migrate_remaining_rows if migrate_column_type?
  13. end
  14. def down
  15. # Previous migrations already revert the changes made here.
  16. end
  17. def migrate_remaining_rows
  18. Issue.where('closed_at_for_type_change IS NULL AND closed_at IS NOT NULL').each_batch do |batch|
  19. batch.update_all('closed_at_for_type_change = closed_at')
  20. end
  21. cleanup_concurrent_column_type_change(:issues, :closed_at)
  22. end
  23. def migrate_column_type?
  24. # Some environments may have already executed the previous version of this
  25. # migration, thus we don't need to migrate those environments again.
  26. column_for('issues', 'closed_at').type == :datetime # rubocop:disable Migration/Datetime
  27. end
  28. end

这同样适用于rename_column_using_background_migration

  1. 使用帮助程序创建迁移,该迁移将安排后台迁移以将写入分散在更长的时间范围内.
  2. 在下一个每月发行版中,创建清理迁移以从 Sidekiq 队列中窃取,迁移所有丢失的行并清理重命名. 如果该列已被重命名,则此迁移应在从 Sidekiq 队列中窃取后跳过步骤.

有关更多信息,请参阅有关清理后台迁移的文档 .

Adding Indexes

使用add_concurrent_index时,添加索引不需要停机.

另请参阅《 迁移样式指南》 .

Dropping Indexes

删除索引不需要停机.

Adding Tables

此操作是安全的,因为还没有使用该表的代码.

Dropping Tables

使用部署后迁移可以安全地完成删除表的操作,但前提是应用程序不再使用该表.

Renaming Tables

重命名表需要停机,因为在数据库迁移期间/之后,应用程序可能会继续使用旧表名.

Adding Foreign Keys

添加外键通常需要 3 个步骤:

  1. 开始交易
  2. 运行ALTER TABLE添加约束
  3. 检查所有现有数据

因为ALTER TABLE通常会在事务结束之前获取独占锁,所以这意味着该方法将需要停机.

GitLab allows you to work around this by using Gitlab::Database::MigrationHelpers#add_concurrent_foreign_key. This method ensures that no downtime is needed.

Removing Foreign Keys

此操作不需要停机.

Data Migrations

数据迁移可能很棘手. 迁移数据的通常方法是采取 3 个步骤:

  1. 迁移初始数据
  2. 部署应用程序代码
  3. 迁移所有剩余数据

通常这有效,但并非总是如此. 例如,如果要将字段的格式从 JSON 更改为其他格式,我们会遇到一些问题. 如果我们在部署应用程序代码之前更改现有数据,则很可能会遇到错误. 另一方面,如果我们在部署应用程序代码后进行迁移,则可能会遇到相同的问题.

如果您只需要更正一些无效数据,则部署后迁移通常就足够了. 如果您需要更改数据格式(例如,从 JSON 更改为其他格式),通常最好为新数据格式添加一个新列,然后让应用程序使用该列. 在这种情况下,程序将是:

  1. 以新格式添加新列
  2. 将现有数据复制到此新列
  3. 部署应用程序代码
  4. In a post-deployment migration, copy over any remaining data

通常,没有一个万能的解决方案,因此最好在合并请求中讨论此类迁移,以确保以最佳方式实现它们.