What requires downtime?
原文:https://docs.gitlab.com/ee/development/what_requires_downtime.html
- Dropping Columns
- Renaming Columns
- Changing Column Constraints
- Changing Column Types
- Changing The Schema For Large Tables
- Adding Indexes
- Dropping Indexes
- Adding Tables
- Dropping Tables
- Renaming Tables
- Adding Foreign Keys
- Removing Foreign Keys
- Data Migrations
What requires downtime?
使用数据库时,可以在不使 GitLab 脱机的情况下执行某些操作,其他操作确实需要停机时间. 本指南介绍了各种操作,其影响以及如何在不停机的情况下执行这些操作.
Dropping Columns
删除列很棘手,因为正在运行的 GitLab 进程可能仍在使用这些列. 为了安全地解决此问题,您需要在三个版本中执行三个步骤:
- 忽略列(版本 M)
- 删除列(版本 M + 1)
- 删除忽略规则(版本 M + 2)
之所以将其分布在三个发行版中,是因为删除列是一种破坏性操作,不易回滚.
遵循此过程可帮助我们确保没有部署到 GitLab.com 并升级将这些步骤集中在一起的自我管理安装的过程.
Step 1: Ignoring the column (release M)
第一步是忽略应用程序代码中的列. 这是必要的,因为 Rails 缓存列并在各个地方重复使用此缓存. 这可以通过定义要忽略的列来完成. 例如,要忽略用户模型中的updated_at
,请使用以下命令:
class User < ApplicationRecord
include IgnorableColumns
ignore_column :updated_at, remove_with: '12.7', remove_after: '2019-12-22'
end
多列也可以忽略:
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 中的部署后迁移:
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
来执行重命名. 例如
# A regular migration in db/migrate
class RenameUsersUpdatedAtToUpdatedAtTimestamp < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
rename_column_concurrently :users, :updated_at, :updated_at_timestamp
end
def down
undo_rename_column_concurrently :users, :updated_at, :updated_at_timestamp
end
end
这将负责重命名列,确保数据保持同步,通过索引和外键进行复制等.
注意:如果一列包含 1 个或多个不包含原始列名称的索引,则上述过程将失败. 在这种情况下,您首先需要重命名这些索引.
Step 2: Add A Post-Deployment Migration
重命名过程需要在部署后迁移中进行一些清理. 我们可以使用Gitlab::Database::MigrationHelpers#cleanup_concurrent_column_rename
来执行此清理:
# A post-deployment migration in db/post_migrate
class CleanupUsersUpdatedAtRename < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
cleanup_concurrent_column_rename :users, :updated_at, :updated_at_timestamp
end
def down
undo_cleanup_concurrent_column_rename :users, :updated_at, :updated_at_timestamp
end
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
常规迁移用于创建具有临时名称的新列,并设置一些触发器以使数据保持同步. 这样的迁移如下所示:
# A regular migration in db/migrate
class ChangeUsersUsernameStringToText < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
change_column_type_concurrently :users, :username, :text
end
def down
cleanup_concurrent_column_type_change :users, :username
end
end
Step 2: Create A Post Deployment Migration
接下来,我们需要使用部署后迁移来清理更改:
# A post-deployment migration in db/post_migrate
class ChangeUsersUsernameStringToTextCleanup < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
cleanup_concurrent_column_type_change :users, :username
end
def down
change_column_type_concurrently :users, :username, :string
end
end
就是这样,我们完成了!
Casting data to a new type
某些类型更改需要将数据转换为新类型. 例如,从text
更改为jsonb
. 在这种情况下,请使用type_cast_function
选项. 确保没有不良数据,并且投射将始终成功. 您还可以提供一个自定义函数来处理转换错误.
迁移示例:
def up
change_column_type_concurrently :users, :settings, :jsonb, type_cast_function: 'jsonb'
end
Changing The Schema For Large Tables
虽然change_column_type_concurrently
和rename_column_concurrently
可以用于在rename_column_concurrently
机的情况下更改表的架构,但对于大型表来说,效果并不理想. 由于所有工作都是按顺序进行的,因此迁移可能需要很长时间才能完成,从而阻止了部署的进行. 由于数据库按顺序快速更新许多行,因此它们也可能给数据库带来很大压力.
为减轻数据库压力,在迁移大表中的列时(例如issues
),应改用change_column_type_using_background_migration
或rename_column_using_background_migration
. 这些方法的工作方式与并发的类似,但是使用后台迁移将工作/负载分散在更长的时间段内,而不会减慢部署速度.
例如,要使用后台迁移来更改列类型:
class ExampleMigration < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
class Issue < ActiveRecord::Base
self.table_name = 'issues'
include EachBatch
def self.to_migrate
where('closed_at IS NOT NULL')
end
end
def up
change_column_type_using_background_migration(
Issue.to_migrate,
:closed_at,
:datetime_with_timezone
)
end
def down
change_column_type_using_background_migration(
Issue.to_migrate,
:closed_at,
:datetime
)
end
end
这将将issues.closed_at
的类型更改为timestamp with time zone
.
请记住,传递给change_column_type_using_background_migration
的关系必须包含EachBatch
,否则将引发TypeError
.
然后,此迁移需要在单独的发行版( 而不是补丁程序发行版)中进行清除迁移,该清除迁移应从队列中窃取并处理所有剩余的行. 例如:
class MigrateRemainingIssuesClosedAt < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
class Issue < ActiveRecord::Base
self.table_name = 'issues'
include EachBatch
end
def up
Gitlab::BackgroundMigration.steal('CopyColumn')
Gitlab::BackgroundMigration.steal('CleanupConcurrentTypeChange')
migrate_remaining_rows if migrate_column_type?
end
def down
# Previous migrations already revert the changes made here.
end
def migrate_remaining_rows
Issue.where('closed_at_for_type_change IS NULL AND closed_at IS NOT NULL').each_batch do |batch|
batch.update_all('closed_at_for_type_change = closed_at')
end
cleanup_concurrent_column_type_change(:issues, :closed_at)
end
def migrate_column_type?
# Some environments may have already executed the previous version of this
# migration, thus we don't need to migrate those environments again.
column_for('issues', 'closed_at').type == :datetime # rubocop:disable Migration/Datetime
end
end
这同样适用于rename_column_using_background_migration
:
- 使用帮助程序创建迁移,该迁移将安排后台迁移以将写入分散在更长的时间范围内.
- 在下一个每月发行版中,创建清理迁移以从 Sidekiq 队列中窃取,迁移所有丢失的行并清理重命名. 如果该列已被重命名,则此迁移应在从 Sidekiq 队列中窃取后跳过步骤.
有关更多信息,请参阅有关清理后台迁移的文档 .
Adding Indexes
使用add_concurrent_index
时,添加索引不需要停机.
另请参阅《 迁移样式指南》 .
Dropping Indexes
删除索引不需要停机.
Adding Tables
此操作是安全的,因为还没有使用该表的代码.
Dropping Tables
使用部署后迁移可以安全地完成删除表的操作,但前提是应用程序不再使用该表.
Renaming Tables
重命名表需要停机,因为在数据库迁移期间/之后,应用程序可能会继续使用旧表名.
Adding Foreign Keys
添加外键通常需要 3 个步骤:
- 开始交易
- 运行
ALTER TABLE
添加约束 - 检查所有现有数据
因为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 个步骤:
- 迁移初始数据
- 部署应用程序代码
- 迁移所有剩余数据
通常这有效,但并非总是如此. 例如,如果要将字段的格式从 JSON 更改为其他格式,我们会遇到一些问题. 如果我们在部署应用程序代码之前更改现有数据,则很可能会遇到错误. 另一方面,如果我们在部署应用程序代码后进行迁移,则可能会遇到相同的问题.
如果您只需要更正一些无效数据,则部署后迁移通常就足够了. 如果您需要更改数据格式(例如,从 JSON 更改为其他格式),通常最好为新数据格式添加一个新列,然后让应用程序使用该列. 在这种情况下,程序将是:
- 以新格式添加新列
- 将现有数据复制到此新列
- 部署应用程序代码
- In a post-deployment migration, copy over any remaining data
通常,没有一个万能的解决方案,因此最好在合并请求中讨论此类迁移,以确保以最佳方式实现它们.