마이그레이션에서 다운타임 방지하기
GitLab v19.4요약
데이터베이스를 다루다 보면 일부 작업은 다운타임을 필요로 할 수 있습니다. 칼럼을 제거하는 작업은 까다롭습니다. 이는 칼럼이 명시적으로 무시 처리되지 않은 경우 발생합니다. 또한 해당 칼럼을 참조하는 데이터베이스 뷰도 함께 고려해야 합니다.
데이터베이스를 다루다 보면 일부 작업은 다운타임을 필요로 할 수 있습니다. 마이그레이션에서 다운타임을 허용할 수 없으므로, 다운타임 없이 동일한 결과를 얻기 위한 일련의 단계를 사용해야 합니다. 이 가이드는 다운타임이 필요할 것처럼 보이는 다양한 작업과 그 영향, 그리고 다운타임 없이 그 작업을 수행하는 방법을 설명합니다.
칼럼 삭제하기#
칼럼을 제거하는 작업은 까다롭습니다. 실행 중인 GitLab 프로세스는 해당 칼럼이 존재한다고 가정하기 때문입니다. ActiveRecord는 칼럼이 참조되지 않아도 부팅 시 테이블 스키마를 캐시합니다.
이는 칼럼이 명시적으로 무시 처리되지 않은 경우 발생합니다.
또한 해당 칼럼을 참조하는 데이터베이스 뷰도 함께 고려해야 합니다.
이를 안전하게 처리하려면 세 번의 릴리스에 걸쳐 세 단계를 거쳐야 합니다:
- 칼럼 무시하기 (릴리스 M)
- 칼럼 삭제하기 (릴리스 M+1)
- 무시 규칙 제거하기 (릴리스 M+2)
세 릴리스에 걸쳐 나누어 진행하는 이유는 칼럼 삭제가 쉽게 되돌릴 수 없는 파괴적인 작업이기 때문입니다.
이 절차를 따르면 GitLab.com 배포 및 GitLab Self-Managed 인스턴스의 업그레이드 과정에서 이 단계들이 한꺼번에 묶이지 않도록 할 수 있습니다.
칼럼 무시하기 (릴리스 M)#
첫 번째 단계는 애플리케이션 코드에서 칼럼을 무시 처리하고, 모델 유효성 검사를
포함하여 해당 칼럼에 대한 모든 코드 참조를 제거하는 것입니다. 이 단계가
필요한 이유는 Rails가 칼럼을 캐시하고 이 캐시를 여러 곳에서 재사용하기
때문입니다. 무시할 칼럼을 정의하는 방식으로 이 작업을 수행할 수 있습니다.
예를 들어 릴리스 12.5에서 User 모델의 updated_at을 무시하려면 다음과 같이 작성합니다:
class User < ApplicationRecord
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'
모델이 CE와 EE에 모두 존재하는 경우 칼럼은 CE 모델에서 무시 처리해야 합니다. 모델이 EE에만 존재하는 경우에는 EE 쪽에 추가해야 합니다.
칼럼 무시 규칙을 언제 제거해도 안전한지는 다음 두 가지로 표시해야 합니다:
remove_with: 칼럼 무시를 추가한 후 보통 두 릴리스(M+2) 뒤의 GitLab 릴리스로 설정합니다(예시에서는12.7).remove_after: 칼럼 무시를 제거해도 안전하다고 판단되는 날짜로 설정합니다. 일반적으로 M+1 릴리스 날짜 이후, M+2 개발 사이클 중에 해당합니다. 예를 들어12.7의 개발 사이클이2019-12-18부터2020-01-17사이이고12.6이 칼럼을 삭제하는 릴리스라면,12.6의 릴리스 날짜인2019-12-22로 설정하는 것이 안전합니다.
이 정보를 통해 칼럼 무시에 대해 더 정확하게 판단할 수 있으며, 일반 릴리스와 GitLab.com 배포 모두에서 칼럼 무시를 너무 일찍 제거하지 않도록 보장합니다. 예를 들어 칼럼을 무시하는 변경과 칼럼 무시를 제거하는 변경을 한꺼번에 배포하는 상황(다운타임을 유발할 수 있는)을 방지합니다.
이 예시에서 칼럼을 무시하는 변경은 릴리스 12.5에 포함되었습니다.
칼럼 무시와 칼럼 삭제는 같은 릴리스에서 동시에 진행해서는 안 됩니다. 모델에서 칼럼을 제대로 무시 처리하기 전에 칼럼을 삭제하면 제로 다운타임 마이그레이션에서 문제가 발생할 수 있습니다. 실행 중인 인스턴스가 Rails 스키마 캐시가 만료될 때까지 제거된 칼럼을 조회하려다 실패할 수 있습니다. 이는 제로 다운타임 업그레이드를 따르려는 셀프 매니지드 고객에게 문제가 될 수 있으며, 업데이트된 스키마를 다시 불러오기 위해 실행 중인 모든 GitLab 인스턴스를 명시적으로 재시작해야 합니다. 이 상황을 피하려면 먼저 칼럼을 무시하고(릴리스 M) 그다음 릴리스(릴리스 M+1)에서 삭제합니다.
데이터베이스 뷰에서 참조되는 칼럼 무시하기#
칼럼이 다음 예시처럼 데이터베이스 뷰에서도 참조되는 경우:
CREATE VIEW recently_updated_users_view(id, username, updated_at) AS
SELECT id, username, updated_at
FROM users
WHERE updated_at > now() - interval '30 day'
해당 모델 클래스에도 ignore_columns 지시문을 포함해야 합니다:
class RecentlyUpdatedUsersView < ApplicationRecord
self.table_name = 'recently_updated_users_view'
ignore_columns :updated_at
end
칼럼 삭제하기 (릴리스 M+1)#
예시를 계속하면, 칼럼 삭제는 릴리스 12.6의 post-deployment 마이그레이션에 포함됩니다:
먼저 post-deployment 마이그레이션을 생성합니다:
bundle exec rails g post_deployment_migration remove_users_updated_at_column
칼럼을 제거하는 마이그레이션을 작성할 때는 다음 상황을 고려해야 합니다:
제거할 칼럼에 속하는 인덱스나 제약 조건이 없는 경우#
이 경우에는 트랜잭션 마이그레이션을 사용할 수 있습니다:
class RemoveUsersUpdatedAtColumn < Gitlab::Database::Migration[2.1]
def up
remove_column :users, :updated_at
end
def down
add_column :users, :updated_at, :datetime
end
end
제거할 칼럼에 속하는 인덱스나 제약 조건이 있는 경우#
down 메서드에서 삭제된 인덱스나 제약 조건을 다시 추가해야 하는 경우, 이는
트랜잭션 마이그레이션에서는 수행할 수 없습니다. 마이그레이션은 다음과 같이 작성합니다:
class RemoveUsersUpdatedAtColumn < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
def up
remove_column :users, :updated_at
end
def down
add_column(:users, :updated_at, :datetime, if_not_exists: true)
# Make sure to add back any indexes or constraints,
# that were dropped in the `up` method. For example:
add_concurrent_index(:users, :updated_at)
end
end
down 메서드에서는 칼럼을 다시 추가하기 전에 이미 존재하는지 확인합니다.
마이그레이션이 비트랜잭션 방식이어서 실행 중에 실패했을 수도 있기 때문입니다.
disable_ddl_transaction!는
전체 마이그레이션을 감싸는 트랜잭션을 비활성화하는 데 사용됩니다.
데이터베이스 마이그레이션에 대한 자세한 내용은 마이그레이션 스타일 가이드 페이지를 참고합니다.
제거할 칼럼이 데이터베이스 뷰에서 참조되는 경우#
칼럼이 데이터베이스 뷰에서 참조되는 경우 해당 칼럼에 제약 조건이 붙어 있는 것처럼 동작하므로 칼럼을 삭제하기 전에 먼저 뷰를 업데이트해야 합니다:
- 해당 칼럼을 제외하여 뷰를 다시 생성합니다
- 원본 테이블에서 칼럼을 삭제합니다
down 메서드는 칼럼이 뷰에서 참조되기 전에 존재해야 하므로 역순으로
작업을 수행해야 합니다:
- 원본 테이블에 칼럼을 다시 추가합니다
- 칼럼을 포함하여 뷰를 다시 생성합니다
마이그레이션은 다음과 같이 작성합니다:
class RemoveUsersUpdatedAtColumn < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
def up
execute <<-SQL
DROP VIEW IF EXISTS recently_updated_users_view;
CREATE VIEW recently_updated_users_view(id, username) AS
SELECT id, username
FROM users;
SQL
remove_column :users, :updated_at
end
def down
add_column :users, :updated_at, :datetime
execute <<-SQL
DROP VIEW IF EXISTS recently_updated_users_view;
CREATE VIEW recently_updated_users_view(id, username, updated_at) AS
SELECT id, username, updated_at
FROM users
WHERE updated_at > now() - interval '30 day'
SQL
end
end
무시 규칙 제거하기 (릴리스 M+2)#
다음 릴리스, 이 예시에서는 12.7에서 무시 규칙을 제거하기 위한 또 다른 머지 리퀘스트를 생성합니다.
이렇게 하면 ignore_column 줄이 제거되며, 더 이상 필요하지 않은 경우 IgnoreableColumns 포함도 함께 제거됩니다.
이는 remove_with에 표시된 릴리스와 함께, remove_after 날짜가
지난 후에만 병합해야 합니다.
칼럼 이름 변경하기#
아래 절차는 소규모 테이블에만 적합합니다. 이 절차는 일반 마이그레이션에서 한 칼럼의 모든 데이터를 다른 칼럼으로 복사하며, 대규모 테이블에서는 시간이 너무 오래 걸릴 수 있습니다. 대규모 테이블의 경우 배치 백그라운드 마이그레이션을 사용해 데이터를 복사하고 여러 마일스톤에 걸쳐 이름 변경을 수행하는 방법을 검토해야 합니다.
칼럼 이름을 표준 방식으로 변경하면 다운타임이 필요합니다. 애플리케이션이 데이터베이스 마이그레이션 중이나 이후에도 계속 이전 칼럼 이름을 사용할 수 있기 때문입니다. 다운타임 없이 칼럼 이름을 변경하려면 일반 마이그레이션과 post-deployment 마이그레이션, 두 개의 마이그레이션이 필요합니다. 이 두 마이그레이션은 같은 릴리스에 포함될 수 있습니다. 단계는 다음과 같습니다:
- 일반 마이그레이션 추가하기 (릴리스 M)
- 칼럼 무시하기 (릴리스 M)
- post-deployment 마이그레이션 추가하기 (릴리스 M)
- 무시 규칙 제거하기 (릴리스 M+1)
데이터베이스 뷰에서 참조되는 칼럼을 일반적인 방식으로 이름 변경할 때는 SELECT 부분을 그대로
유지하면서 뷰가 새 칼럼 이름으로 업데이트되므로 추가 단계가 필요하지 않습니다.
다운타임 없이 진행할 때는 위 단계에서 언급한 추가 고려 사항이 있습니다.
일반 마이그레이션 추가하기 (릴리스 M)#
먼저 일반 마이그레이션을 생성해야 합니다. 이 마이그레이션은 이름 변경을 수행하기 위해
Gitlab::Database::MigrationHelpers#rename_column_concurrently를 사용해야
합니다. 예를 들면 다음과 같습니다:
# A regular migration in db/migrate
class RenameUsersUpdatedAtToUpdatedAtTimestamp < Gitlab::Database::Migration[2.1]
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
이렇게 하면 칼럼 이름 변경, 데이터 동기화 유지, 인덱스 및 외래 키 복사가 함께 처리됩니다.
칼럼에 원본 칼럼 이름이 포함되지 않은 인덱스가 하나 이상 있으면 앞서 설명한 절차가 실패합니다. 이 경우 해당 인덱스의 이름을 변경해야 합니다.
칼럼이 데이터베이스 뷰에서 참조되는 경우 뷰를 다시 생성하여
새 칼럼을 가리키도록 해야 합니다. down 작업은
undo_rename_column_concurrently를 실행하기 전에 이를 복원해야 합니다:
# A regular migration in db/migrate including database view recreation
class RenameUsersUpdatedAtToUpdatedAtTimestamp < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
def up
rename_column_concurrently :users, :updated_at, :updated_at_timestamp
execute <<-SQL
DROP VIEW IF EXISTS recently_updated_users_view;
CREATE VIEW recently_updated_users_view(id, username, updated_at) AS
SELECT id, username, updated_at_timestamp
FROM users
WHERE updated_at > now() - interval '30 day'
SQL
end
def down
execute <<-SQL
DROP VIEW IF EXISTS recently_updated_users_view;
CREATE VIEW recently_updated_users_view(id, username, updated_at) AS
SELECT id, username, updated_at
FROM users
WHERE updated_at > now() - interval '30 day'
SQL
undo_rename_column_concurrently :users, :updated_at, :updated_at_timestamp
end
end
칼럼 무시하기 (릴리스 M)#
다음 단계는 애플리케이션 코드에서 칼럼을 무시 처리하고 사용되지 않도록 하는 것입니다. 이 단계가 필요한 이유는 Rails가 칼럼을 캐시하고 이 캐시를 여러 곳에서 재사용하기 때문입니다. 이 단계는 칼럼을 삭제할 때의 첫 번째 단계와 유사하며 동일한 요구 사항이 적용됩니다.
class User < ApplicationRecord
ignore_column :updated_at, remove_with: '12.7', remove_after: '2019-12-22'
end
post-deployment 마이그레이션 추가하기 (릴리스 M)#
이름 변경 절차는 post-deployment 마이그레이션에서 일부 정리 작업이 필요합니다.
이 정리 작업은
Gitlab::Database::MigrationHelpers#cleanup_concurrent_column_rename을 사용하여 수행할 수 있습니다:
# A post-deployment migration in db/post_migrate
class CleanupUsersUpdatedAtRename < Gitlab::Database::Migration[2.1]
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를 사용하면 시스템이 이 상태로 상당한 시간 동안 실행될 수 있습니다.
무시 규칙 제거하기 (릴리스 M+1)#
칼럼을 삭제할 때와 마찬가지로, 이름 변경이 완료된 후 다음 릴리스에서 무시 규칙을 제거해야 합니다.
칼럼 제약 조건 변경하기#
NOT NULL 절(또는 다른 제약 조건)을 추가하거나 제거하는 작업은 일반적으로
다운타임 없이 수행할 수 있습니다. NOT NULL 제약 조건을 추가할 때는 애플리케이션
변경 사항이 먼저 배포되어야 하므로 post-deployment 마이그레이션에서 수행해야 합니다.
반대로 NOT NULL 제약 조건을 제거하는 작업은 일반 마이그레이션에서 수행해야 합니다.
이렇게 하면 NULL 값을 삽입하는 코드가 해당 칼럼에서 안전하게 실행될 수 있습니다.
change_column은 전체 칼럼 타입을 다시 정의하여 비효율적인 쿼리를 만들어내므로
사용을 피합니다.
각 특정 사용 사례에 대해서는 다음 가이드를 참고할 수 있습니다:
칼럼 타입 변경하기#
칼럼의 타입은
Gitlab::Database::MigrationHelpers#change_column_type_concurrently를 사용하여 변경할 수 있습니다.
이 메서드는 rename_column_concurrently와 유사하게 동작합니다. 예를 들어
users.username의 타입을 string에서 text로 변경하려는 경우:
데이터베이스 뷰에서 참조되는 칼럼의 타입을 변경할 때는 그 과정의 일부로 뷰를 다시 생성해야 합니다.
일반 마이그레이션 생성하기#
일반 마이그레이션은 임시 이름으로 새 칼럼을 생성하고 데이터를 동기화 상태로 유지하기 위한 트리거를 설정하는 데 사용됩니다. 이러한 마이그레이션은 다음과 같이 작성합니다:
# A regular migration in db/migrate
class ChangeUsersUsernameStringToText < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
def up
change_column_type_concurrently :users, :username, :text
end
def down
undo_change_column_type_concurrently :users, :username
end
end
칼럼이 데이터베이스 뷰에서 참조되는 경우 뷰를 다시 생성하여 새 임시 칼럼을 가리키도록 해야 합니다.
이후 단계에서 임시 칼럼의 이름을 원래 이름으로 다시 바꾸면 뷰는 내부적으로 스스로 업데이트되므로 다른 변경이 필요하지 않습니다:
# A regular migration in db/migrate including database view recreation
class ChangeUsersUsernameStringToText < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
def up
change_column_type_concurrently :users, :username, :text
# temporary column name follows this pattern: `"#{column}_for_type_change"`
# so the column named `username` becomes `username_for_type_change`
execute <<-SQL
DROP VIEW IF EXISTS recently_updated_users_view;
CREATE VIEW recently_updated_users_view(id, username, updated_at) AS
SELECT id, username_for_type_change, updated_at
FROM users
WHERE updated_at > now() - interval '30 day'
SQL
end
def down
execute <<-SQL
DROP VIEW IF EXISTS recently_updated_users_view;
CREATE VIEW recently_updated_users_view(id, username, updated_at) AS
SELECT id, username, updated_at_timestamp
FROM users
WHERE updated_at > now() - interval '30 day'
SQL
undo_change_column_type_concurrently :users, :username
end
end
post-deployment 마이그레이션 생성하기#
다음으로 post-deployment 마이그레이션을 사용하여 변경 사항을 정리해야 합니다:
# A post-deployment migration in db/post_migrate
class ChangeUsersUsernameStringToTextCleanup < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
def up
cleanup_concurrent_column_type_change :users, :username
end
def down
undo_cleanup_concurrent_column_type_change :users, :username, :string
end
end
이것으로 완료됩니다.
새 타입으로 데이터 캐스팅하기#
일부 타입 변경은 데이터를 새 타입으로 캐스팅해야 합니다. 예를 들어 text에서 jsonb로 변경하는 경우입니다.
이 경우 type_cast_function 옵션을 사용합니다.
잘못된 데이터가 없고 캐스팅이 항상 성공하는지 확인합니다. 캐스팅 오류를 처리하는
커스텀 함수를 제공할 수도 있습니다.
마이그레이션 예시:
def up
change_column_type_concurrently :users, :settings, :jsonb, type_cast_function: 'jsonb'
end
칼럼 기본값 변경하기#
칼럼 기본값을 변경하거나 제거하는 작업은 Rails가 기본값과 동일한 값을 처리하는 방식 때문에 어렵습니다.
partial_inserts 설정이 활성화되면 Rails는 칼럼의 인메모리 값이 칼럼 기본값과 같을 때 해당 칼럼을 INSERT에서 생략하고 데이터베이스가 값을 제공하도록 합니다.
마이그레이션이 칼럼 기본값을 변경하면 실행 중인 프로세스는 스키마 캐시 때문에 이 변경 사항을 알지 못합니다.
제로 다운타임 배포 중에는 이전 프로세스가 여전히 트래픽을 처리하는 동안 post-deployment 마이그레이션이 실행되며, 각 프로세스는 그 기간 동안 오래된 스키마 캐시를 유지할 수 있습니다.
데이터베이스 기본값에 의존하는 프로세스는 이때 잘못된 값을 씁니다.
칼럼에 NOT NULL 제약 조건이 있고 마이그레이션이 해당 기본값을 제거하면, 생략된 칼럼에는 대체할 값이 없어 INSERT가 실패합니다.
칼럼 기본값을 안전하게 변경하거나 제거하려면 데이터베이스 기본값에 의존하는 대신 애플리케이션이 칼럼을 명시적으로 쓰도록 강제해야 합니다.
이를 위해서는 두 개의 마이너 릴리스에 걸친 단계가 필요합니다:
- 모델에
SafelyChangeColumnDefaultconcern 추가하기와 post-migration에서 기본값 변경하기. - 다음 마이너 릴리스에서
SafelyChangeColumnDefaultconcern 정리하기.
Self-Managed 릴리스는 전체 마이너 릴리스를 하나의 제로 다운타임 배포로 묶기 때문에
SafelyChangeColumnDefault를 정리하기 전에 마이너 릴리스 하나를 기다려야 합니다.
모델에 SafelyChangeColumnDefault concern 추가하고 post-migration에서 기본값 변경하기#
첫 번째 단계는 애플리케이션이 모든 INSERT에 칼럼을 항상 쓰도록 칼럼을 표시하는 것입니다:
class Ci::Build < ApplicationRecord
include SafelyChangeColumnDefault
columns_changing_default :partition_id
end
SafelyChangeColumnDefault는 나열된 칼럼을 모든 INSERT에 강제로 포함시키지만, 실제로 쓰이는
값은 여전히 모델에서 나옵니다. 그 값은 스키마 캐시에 의존해서는 안 됩니다.
상수 기본값의 경우, 코드가 소유하며 절대 오래되지 않는 Rails attribute 기본값으로 선언합니다:
class Ci::Build < ApplicationRecord
include SafelyChangeColumnDefault
attribute :partition_id, default: 101
columns_changing_default :partition_id
end
Rails attribute 기본값은 레코드로부터 값을 계산할 수 없습니다. Rails가 레코드를 컨텍스트로 삼지 않고
기본값을 평가하기 때문입니다. 연관(association)처럼 레코드에 의존하는 기본값은 after_initialize 콜백에서 계산하고, 값이 사용자로부터 오지 않은 경우에만 할당합니다:
class ProjectFeature < ApplicationRecord
include SafelyChangeColumnDefault
columns_changing_default :model_experiments_access_level
after_initialize(if: :new_record?) do
unless @attributes['model_experiments_access_level'].came_from_user?
self.model_experiments_access_level = project&.public? ? ENABLED : PRIVATE
end
end
end
할당을 ||=로 보호해서는 안 됩니다.
오래된 스키마 캐시를 가진 프로세스에서는 속성이 이미 캐시된 기본값을 가지고 있으므로, ||=는 할당을 건너뛰고 concern이 계산된 값 대신 오래된 값을 씁니다.
came_from_user? 검사는 스키마 캐시와 관계없이 값이 명시적으로 설정된 경우에만 참입니다.
NULL이 안전하지 않은 nullable 칼럼: 무조건적인 before_create 콜백을 사용합니다.
Rails attribute 기본값은 명시적인 nil을 재정의하지 않습니다.
호출자가 Model.new(col: nil).save!를 실행하면 attribute 기본값이 선언되어 있어도 칼럼은 NULL로 기록됩니다.
칼럼이 nullable이고 NULL이 안전하지 않은 경우(예: NULL 행이 부분 고유 인덱스를 피해 가는 경우), 대신 무조건적인 before_create 콜백을 사용합니다:
class ProjectAuthorization < ApplicationRecord
include SafelyChangeColumnDefault
columns_changing_default :is_unique
before_create :assign_is_unique
private
def assign_is_unique
self.is_unique = true
end
end
attribute 기본값과 달리 이 콜백은 저장 시점에 무조건 실행되며 인메모리의
nil을 모두 재정의합니다. 모델을 통해서는 칼럼이 NULL로 기록되는 일이 없습니다.
그런 다음 기본값을 변경하기 위한 post-deployment 마이그레이션을 생성합니다:
bundle exec rails g post_deployment_migration change_ci_builds_default
class ChangeCiBuildsDefault < Gitlab::Database::Migration[2.1]
def change
change_column_default('ci_builds', 'partition_id', from: 100, to: 101)
end
end
다음 마이너 릴리스에서 SafelyChangeColumnDefault concern 정리하기#
다음 마이너 릴리스에서 columns_changing_default 호출을 제거하는 새 머지 리퀘스트를 생성합니다. 다른 칼럼에
필요하지 않은 경우 SafelyChangeColumnDefault include도 함께 제거합니다.
대규모 테이블의 스키마 변경하기#
change_column_type_concurrently와 rename_column_concurrently는 다운타임 없이
테이블 스키마를 변경하는 데 사용할 수 있지만, 대규모 테이블에서는 잘 동작하지
않습니다. 모든 작업이 순차적으로 진행되기 때문에 마이그레이션을 완료하는 데
매우 오랜 시간이 걸려 배포가 진행되지 못할 수 있습니다. 또한 많은 행을
순차적으로 빠르게 업데이트하므로 데이터베이스에 상당한 부하를
줄 수 있습니다.
데이터베이스 부하를 줄이려면 대규모 테이블(예: issues)의 칼럼을 마이그레이션할 때
백그라운드 마이그레이션을 대신 사용해야 합니다. 백그라운드 마이그레이션은
배포를 늦추지 않으면서 작업/부하를 더 긴 시간에
걸쳐 분산합니다.
자세한 내용은 배치 백그라운드 마이그레이션 정리에 관한 문서를 참고합니다.
인덱스 추가하기#
add_concurrent_index를 사용하면 인덱스를 추가하는 데
다운타임이 필요하지 않습니다.
자세한 내용은 마이그레이션 스타일 가이드를 참고합니다.
인덱스 삭제하기#
인덱스를 삭제하는 데는 다운타임이 필요하지 않습니다.
테이블 추가하기#
이 작업은 아직 해당 테이블을 사용하는 코드가 없으므로 안전합니다.
테이블 삭제하기#
테이블을 삭제할 때는 다운타임을 피하기 위해 여러 릴리스에 걸친 프로세스가 필요합니다:
- 릴리스 M: 테이블을 사용하는 모든 애플리케이션 코드를 제거합니다.
- 릴리스 M+1: 각각 별도의 post-deployment 마이그레이션에서 외래 키를 제거한 후 테이블을 삭제합니다.
데이터베이스 딕셔너리에 설명된 절차를 사용하여 테이블을 db/docs/deleted_tables에 추가합니다.
테이블이 삭제되더라도 데이터베이스 마이그레이션에서는 여전히 참조됩니다.
테이블 삭제 전 외래 키 제거하기#
테이블에 외래 키가 있는 경우, 테이블을 삭제하는 마이그레이션 전에 각 외래 키를
별도의 post-deployment 마이그레이션에서 제거합니다. 다른 테이블을 여전히 참조하는 테이블을
삭제하면 참조된 테이블도 함께 잠깁니다. 트래픽이 많은 테이블을 참조하는 외래 키가 있으면
DROP TABLE이 타임아웃될 수 있습니다.
외래 키가 트래픽이 많은 테이블
(예: users, projects, namespaces)을 참조하는 경우, 제거 작업은 해당 테이블에
ACCESS EXCLUSIVE 잠금을 겁니다. 잠금 재시도가 없으면 이 잠금은 타임아웃되어
수동 재시도가 필요할 수 있습니다. 제거 작업을 with_lock_retries로 감쌉니다. 그러면
마이그레이션은 외래 키를 제거하기 전에 먼저 참조된 테이블을
재시도하며 잠급니다:
class RemoveUploadsArchivedToUsersForeignKey < Gitlab::Database::Migration[2.3]
disable_ddl_transaction!
milestone '19.1'
SOURCE_TABLE_NAME = :uploads_archived
COLUMN = :uploaded_by_user_id
FOREIGN_KEY_NAME = :fk_b94f059d73
def up
with_lock_retries do
remove_foreign_key_if_exists SOURCE_TABLE_NAME, column: COLUMN, name: FOREIGN_KEY_NAME
end
end
def down
add_concurrent_foreign_key SOURCE_TABLE_NAME, :users,
column: COLUMN, on_delete: :nullify, name: FOREIGN_KEY_NAME
end
end
with_lock_retries는 짧은 lock_timeout으로 트랜잭션 내에서 제거 작업을 실행하며,
타임아웃까지 대기하는 대신 잠금을 획득하지 못하면 재시도합니다.
remove_foreign_key_if_exists는 기본적으로 reverse_lock_order: true를 사용하여, 동시
애플리케이션 트랜잭션과의 교착 상태를 방지하기 위해 소스 테이블보다 먼저
참조된 테이블을 잠급니다. 자세한 내용은
reverse_lock_order를 참고합니다.
모든 외래 키가 제거된 후, 별도의 post-deployment 마이그레이션에서 테이블을 삭제합니다.
테이블 이름 변경하기#
테이블 이름을 변경하면 다운타임이 필요합니다. 애플리케이션이 데이터베이스 마이그레이션 중이나 이후에도 계속 이전 테이블 이름을 사용할 수 있기 때문입니다.
테이블과 ActiveRecord 모델이 아직 사용 중이 아니라면, 이전 테이블을 제거하고 새 테이블을 생성하는 것이 테이블을 "이름 변경"하는 선호되는 방법입니다.
여러 릴리스에 걸친 테이블 이름 변경 프로세스를 따르면 다운타임 없이 테이블 이름을 변경할 수 있습니다.
외래 키 추가하기#
외래 키 추가는 다운타임을 유발할 수 있습니다. 자세한 내용은 FK: 다운타임 및 마이그레이션 실패 방지 문서를 참고합니다.
정수형 기본 키를 bigint로 마이그레이션하기#
일부 테이블에서 integer 기본 키(PK)의 오버플로 위험을 방지하기 위해
해당 PK를 bigint로 마이그레이션해야 합니다. 다운타임 없이, 데이터베이스에 과도한
부하를 주지 않으면서 이를 수행하는 과정은 아래에 설명되어 있습니다.
변환 초기화 및 기존 데이터 마이그레이션 시작 (릴리스 N)#
이 과정을 시작하려면 새 bigint 칼럼을 생성하는 일반 마이그레이션을 추가합니다. 제공된
initialize_conversion_of_integer_to_bigint 헬퍼를 사용합니다. 이 헬퍼는 새 레코드에 대해
두 칼럼을 동기화 상태로 유지하는 데이터베이스 트리거도 생성합니다(코드):
# frozen_string_literal: true
class InitializeConversionOfMergeRequestMetricsToBigint < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
TABLE = :merge_request_metrics
COLUMNS = %i[id]
def up
initialize_conversion_of_integer_to_bigint(TABLE, COLUMNS)
end
def down
revert_initialize_conversion_of_integer_to_bigint(TABLE, COLUMNS)
end
end
새 bigint 칼럼을 무시합니다:
# frozen_string_literal: true
class MergeRequest::Metrics < ApplicationRecord
ignore_column :id_convert_to_bigint, remove_with: '16.0', remove_after: '2023-05-22'
end
기존 데이터를 마이그레이션하기 위해 배치 백그라운드 마이그레이션을 대기열에 추가합니다(코드):
# frozen_string_literal: true
class BackfillMergeRequestMetricsForBigintConversion < Gitlab::Database::Migration[2.1]
restrict_gitlab_migration gitlab_schema: :gitlab_main_org
TABLE = :merge_request_metrics
COLUMNS = %i[id]
def up
backfill_conversion_of_integer_to_bigint(TABLE, COLUMNS, sub_batch_size: 200)
end
def down
revert_backfill_conversion_of_integer_to_bigint(TABLE, COLUMNS)
end
end
- Issue#438124로 인해 새 인스턴스는 모든 ID 칼럼이 bigint입니다.
이전 인스턴스(GitLab.com 포함)에서 아직 bigint로 변환되지 않은 ID 목록은
db/integer_ids_not_yet_initialized_to_bigint.yml에서 관리됩니다. 이 파일은 수동으로 편집하지 않습니다. 정리 과정에서 자동으로 업데이트됩니다. - 스키마 파일에는 이미 모든 ID가
bigint로 되어 있으므로,db/structure.sql에는 변경 사항을 푸시하지 않습니다.
백그라운드 마이그레이션 모니터링하기#
마이그레이션이 실행되는 동안 성능을 확인합니다. 아래에 여러 방법을 설명합니다.
배치 백그라운드 마이그레이션의 상위 수준 상태#
배치 백그라운드 마이그레이션 상태를 확인하는 방법을 참고합니다.
데이터베이스 쿼리하기#
관련 데이터베이스 테이블을 직접 쿼리할 수 있습니다. 읽기 전용 복제본에 대한 액세스가 필요합니다. 쿼리 예시:
-- Get details for batched background migration for given table
SELECT * FROM batched_background_migrations WHERE table_name = 'namespaces'\gx
-- Get count of batched background migration jobs by status for given table
SELECT
batched_background_migrations.id, batched_background_migration_jobs.status, COUNT(*)
FROM
batched_background_migrations
JOIN batched_background_migration_jobs ON batched_background_migrations.id = batched_background_migration_jobs.batched_background_migration_id
WHERE
table_name = 'namespaces'
GROUP BY
batched_background_migrations.id, batched_background_migration_jobs.status;
-- Batched background migration progress for given table (based on estimated total number of tuples)
SELECT
m.table_name,
LEAST(100 * sum(j.batch_size) / pg_class.reltuples, 100) AS percentage_complete
FROM
batched_background_migrations m
JOIN batched_background_migration_jobs j ON j.batched_background_migration_id = m.id
JOIN pg_class ON pg_class.relname = m.table_name
WHERE
j.status = 3 AND m.table_name = 'namespaces'
GROUP BY m.id, pg_class.reltuples;
Sidekiq 로그#
배치 백그라운드 마이그레이션을 실행하는 워커를 모니터링하는 데 Sidekiq 로그를 사용할 수도 있습니다:
@gitlab.com이메일 주소로 Kibana에 로그인합니다.- 인덱스 패턴을
pubsub-sidekiq-inf-gprd*로 변경합니다. json.queue: cronjob:database_batched_background_migration필터를 추가합니다.
PostgreSQL 느린 쿼리 로그#
느린 쿼리 로그는 실행하는 데 1초 이상 걸린 쿼리를 추적합니다. 배치 백그라운드 마이그레이션에 대한 쿼리를 확인하려면:
@gitlab.com이메일 주소로 Kibana에 로그인합니다.- 인덱스 패턴을
pubsub-postgres-inf-gprd*로 변경합니다. json.endpoint_id.keyword: Database::BatchedBackgroundMigrationWorker필터를 추가합니다.- 선택 사항. 업데이트만 보려면
json.command_tag.keyword: UPDATE필터를 추가합니다. - 선택 사항. 실패한 구문만 보려면
json.error_severity.keyword: ERROR필터를 추가합니다. - 선택 사항. 테이블 이름으로 필터를 추가합니다.
Grafana 대시보드#
데이터베이스 상태를 모니터링하려면 다음 추가 메트릭을 사용합니다:
- PostgreSQL Tuple Statistics: 활발하게 변환 중인 테이블에 대한 업데이트 비율이 높거나 이 테이블의 데드 튜플 비율이 증가하고 있다면,
autovacuum이 따라가지 못하고 있다는 의미일 수 있습니다. - PostgreSQL Overview: 기본 데이터베이스 서버에서 시스템 사용률이 높거나 초당 트랜잭션(TPS)이 높다면, 마이그레이션이 문제를 일으키고 있다는 의미일 수 있습니다.
Prometheus 메트릭#
각 배치 백그라운드 마이그레이션마다 메트릭이 여러 개 Prometheus에 게시됩니다. 이 메트릭은 Grafana에서 검색하고 시각화할 수 있습니다(예시 보기).
칼럼 교체하기 (릴리스 N + 1)#
백그라운드 마이그레이션이 완료되고 새 bigint 칼럼이 모든 레코드에 대해 채워지면
칼럼을 교체할 수 있습니다. 교체는 post-deployment 마이그레이션으로 수행합니다. 정확한 과정은
변환 대상 테이블에 따라 다르지만 일반적으로 다음 단계로 진행됩니다:
-
제공된
ensure_backfill_conversion_of_integer_to_bigint_is_finished헬퍼를 사용해 배치 마이그레이션이 완료되었는지 확인합니다. 마이그레이션이 완료되지 않았다면 이후 단계는 어차피 실패합니다. 미리 확인하면 더 유용한 오류 메시지를 얻을 수 있습니다.disable_ddl_transaction! restrict_gitlab_migration gitlab_schema: :gitlab_ci def up ensure_backfill_conversion_of_integer_to_bigint_is_finished( :ci_builds, %i[ project_id runner_id user_id ], # optional. Only needed when there is no primary key, for example, like schema_migrations. primary_key: :id ) end def down; end -
Gitlab::Database::MigrationHelpers::ConvertToBigint모듈의add_bigint_column_indexes헬퍼 메서드를 사용하여integer칼럼을 사용하는 기존 인덱스와 대응하는bigint칼럼으로 인덱스를 생성합니다.- 이 헬퍼 메서드는 필요한 모든
bigint인덱스를 생성할 것으로 예상되지만, 누락된 기존 인덱스가 없는지 다시 확인하는 것이 좋습니다. 이 헬퍼에 대한 자세한 내용은 머지 리퀘스트 135781에서 확인할 수 있습니다.
- 이 헬퍼 메서드는 필요한 모든
-
integer칼럼을 사용하는 기존 FK와 대응하는bigint칼럼으로 외래 키(FK)를 생성합니다. 다른 테이블을 참조하는 FK와 마이그레이션 대상 테이블을 참조하는 FK 모두 이 작업을 수행합니다(예시 보기). -
트랜잭션 내에서 칼럼을 교체합니다:
- 관련 테이블을 잠급니다. 교착 상태에 빠질 가능성을 줄이려면 부모에서 자식 순서로 진행하는 것이 좋습니다(예시 보기).
- 칼럼 이름을 바꿔 교체합니다(예시 보기)
- 트리거 함수를 재설정합니다(예시 보기).
- 기본값을 교체합니다(예시 보기).
- PK 제약 조건을 교체합니다(있는 경우)(예시 보기).
- 기존 인덱스를 제거하고 새 인덱스의 이름을 변경합니다(예시 보기).
add_bigint_column_indexes헬퍼로 생성한bigint인덱스의 이름은Gitlab::Database::MigrationHelpers::ConvertToBigint모듈의bigint_index_name을 호출하여 가져올 수 있습니다.
- 기존 외래 키를 제거하고(아직 남아 있는 경우) 새 외래 키의 이름을 변경합니다(예시 보기).
-
변환 칼럼에 대해 생성된 임시 외래 키를 제거합니다. 이름은
Gitlab::Database::MigrationHelpers::ConvertToBigint모듈에서tmp_foreign_key_name으로 가져옵니다. 인덱스보다 외래 키를 먼저 제거합니다. 뒷받침하는 인덱스가 없는 활성 외래 키는 참조된 테이블에서 삭제 작업을 타임아웃시키기 때문입니다. 자세한 내용은 인덱스를 참고합니다. 각 외래 키 제거는 별도의 마이그레이션에 넣어, 한 제거 작업이 잠금을 획득하지 못해 실패하더라도 다른 제거 작업을 막지 않도록 합니다(예시 보기). -
변환 칼럼에 대해 생성된 임시 인덱스를 제거합니다. 이름은
Gitlab::Database::MigrationHelpers::ConvertToBigint모듈에서bigint_index_name으로 가져옵니다.
트리거 및 이전 integer 칼럼 제거하기 (릴리스 N + 2)#
post-deployment 마이그레이션과 제공된 cleanup_conversion_of_integer_to_bigint 헬퍼를 사용하여
데이터베이스 트리거와 이전 integer 칼럼을 삭제합니다(예시 보기).
무시 규칙 제거하기 (릴리스 N + 3)#
칼럼이 삭제된 다음 릴리스에서, 더 이상 필요하지 않으므로 무시 규칙을 제거합니다(예시 보기).
데이터베이스 뷰#
GitLab은 데이터베이스 뷰를 가볍게 사용합니다. 마이그레이션을 처리할 때 추가적인 복잡성을 유발할 수 있기 때문입니다.
현재 뷰가 사용되는 상황은 두 가지입니다:
- Postgres 내부 메트릭 노출하기
- Unified Backup CLI를 위한 제한된 읽기 전용 데이터 노출
Postgres 내부 메트릭#
Postgres 내부 메트릭은 Gitlab::Database::Postgres* 모델(lib/gitlab/database에 있음)을 통해 접근할 수 있으며,
Gitlab::Database::SharedModel 클래스에 의존합니다.
Unified Backup CLI#
Unified Backup CLI는 여러 리포지터리 타입에 대해 gitaly-backup을 트리거하는 데 필요한
제한된 정보를 조회하기 위해 몇 가지 뷰에 의존합니다. 이 뷰는 Gitlab::Backup::Cli::Models::*
(gems/gitlab-backup-cli/lib/gitlab/backup/cli/models에 있음)를 통해 접근할 수 있으며 연결 처리를 위해
Gitlab::Backup::Cli::Models::Base 클래스에 의존합니다.
Unified Backup CLI 코드는 별도의 gem에 있으므로, 메인 코드베이스에도 필요한 뷰가 도구에 필요한 정보를 반환하는지 보장하는 스펙이 포함되어 있습니다. 이를 통해 두 코드베이스 간의 "계약"을 보장합니다.
이 뷰에 필요한 칼럼 중 하나를 변경해야 하는 경우, 다음 단계를 따릅니다:
- 칼럼을 삭제하는 경우
- Durability 팀(Unified Backup 담당)과 Gitaly(
gitaly-backup담당)와 조율합니다
- Durability 팀(Unified Backup 담당)과 Gitaly(
- 칼럼 이름을 변경하는 경우
- 뷰 관련 고려 사항을 포함하여 칼럼 이름 변경하기를 따릅니다
- 칼럼 타입을 변경하는 경우
- 뷰 관련 고려 사항을 포함하여 칼럼 타입 변경하기를 따릅니다
데이터 마이그레이션#
데이터 마이그레이션은 까다로울 수 있습니다. 데이터를 마이그레이션하는 일반적인 접근 방식은 3단계로 진행하는 것입니다:
- 초기 데이터 배치를 마이그레이션합니다
- 애플리케이션 코드를 배포합니다
- 나머지 데이터를 마이그레이션합니다
보통 이 방법은 효과적이지만 항상 그런 것은 아닙니다. 예를 들어 필드의 형식을 JSON에서 다른 형식으로 변경해야 한다면 약간의 문제가 생깁니다. 애플리케이션 코드를 배포하기 전에 기존 데이터를 변경하면 대부분 오류가 발생합니다. 반대로 애플리케이션 코드를 배포한 후에 마이그레이션해도 같은 문제가 발생할 수 있습니다.
단순히 잘못된 데이터를 수정해야 하는 경우라면 post-deployment 마이그레이션으로 충분한 경우가 많습니다. 데이터 형식을 변경해야 하는 경우(예: JSON에서 다른 형식으로) 일반적으로는 새 데이터 형식을 위한 새 칼럼을 추가하고 애플리케이션이 그 칼럼을 사용하도록 하는 것이 가장 좋습니다. 이 경우 절차는 다음과 같습니다:
- 새 형식으로 새 칼럼을 추가합니다
- 기존 데이터를 이 새 칼럼으로 복사합니다
- 애플리케이션 코드를 배포합니다
- post-deployment 마이그레이션에서 나머지 데이터를 복사합니다
일반적으로 모든 상황에 맞는 단일 해법은 없으므로, 이런 종류의 마이그레이션은 머지 리퀘스트에서 논의하여 최선의 방식으로 구현되도록 하는 것이 좋습니다.
스키마 유효성 검사를 사용하는 JSON/JSONB 칼럼 변경하기#
JsonSchemaValidator로 유효성 검사를 하는 JSON 및 JSONB 칼럼은 JSON 스키마가
additionalProperties: false를 사용할 때 배포 중 실패를 방지하기 위해 다단계
과정이 필요합니다. 추가 속성을 허용하는 스키마는 영향을 받지 않습니다.
이 유효성 검사는 다음과 같은 모델 선언에 의해 트리거됩니다:
validates :onboarding_status, json_schema: { filename: 'user_detail_onboarding_status' }
filename은 app/validators/json_schemas/ 또는
ee/app/validators/json_schemas/의 JSON 스키마 파일에 매핑됩니다.
제로 다운타임 배포 중에는 서로 다른 애플리케이션 인스턴스가 일시적으로 서로 다른 코드 버전을 실행할 수 있습니다. 스키마 변경과 코드 변경이 함께 배포되면, 이전 코드 버전을 실행 중인 인스턴스는 인식되지 않는 속성이 있는 데이터를 처리할 때 유효성 검사에 실패합니다.
속성 추가하기#
새 속성을 추가하려면 두 단계가 필요합니다:
app/validators/json_schemas/또는ee/app/validators/json_schemas/의 JSON 스키마 파일에 속성을 추가합니다. 이 단계에서는 새 속성을required목록에 추가하지 않습니다. 이전 코드 버전을 실행 중인 인스턴스는 새 속성을 채우지 않으므로, 이를 필수로 표시하면 해당 인스턴스에서 유효성 검사 실패가 발생합니다.- 스키마 변경이 완전히 배포된 후, 새 속성을 사용하는 코드를 추가합니다.
속성이 필수여야 한다면, 모든 인스턴스가 값을 채운 후 이 단계 또는 이후 단계에서
required목록에 추가합니다.
속성 제거하기#
속성을 제거하려면 최소 세 단계가 필요합니다:
- 속성을 사용하는 모든 코드를 제거합니다.
- 코드 변경이 완전히 배포된 후, 기존 데이터베이스 레코드에서 속성 값을 제거합니다.
- 데이터 마이그레이션이 완료된 후, JSON 스키마 파일에서 속성을 제거합니다.
배포 타이밍#
GitLab.com 배포에서만 사용되는 속성의 경우, 다음 단계를 병합하기 전에 각 변경 사항이 완전히 배포될 때까지 기다립니다. 새 릴리스를 기다릴 필요는 없습니다.
다른 모든 속성의 경우, 제로 다운타임 업그레이드 절차를 따르는 고객이 유효성 검사 실패를 겪지 않도록 각 단계를 별도의 릴리스에서 진행해야 합니다.