- 적절한 마이그레이션 유형 선택
- 대상 데이터베이스 선택
- 일반 스키마 마이그레이션 생성
- 스키마 변경
- 다운타임 피하기
- 되돌릴 수 있는지 여부
- 원자성과 트랜잭션
- 네이밍 규칙
- 마이그레이션 헬퍼 및 버전 관리
- 데이터베이스 잠금 획득시 재시도 메커니즘
- 인덱스 제거
- 인덱스 추가
- 인덱스의 존재 여부 테스트
- 외래 키 제약 조건 추가
NOT NULL
제약 조건- 기본 값이 있는 열 추가
- NULL이 아닌 열의 기본 값 제거
- 기본 열 변경
- 기존 열 업데이트
- 외래 키 제약 조건 제거
- 데이터베이스 테이블 삭제
- 시퀀스 삭제
- 테이블 잘라내기
- 기본 키 교체
- 암호화된 속성
- 테스트
- 데이터 마이그레이션
- 마이그레이션에서 애플리케이션 코드 사용 (비권장)
- 고트래픽 테이블
- 마일스톤
- Autovacuum wraparound protection
이주 가이드 스타일
GitLab의 마이그레이션을 작성할 때, 다음 사항을 고려해야 합니다. 이러한 작업은 모든 규모의 수백만 조직에서 실행되며, 데이터베이스에는 많은 연도의 데이터가 포함된 경우도 있습니다.
또한, 업그레이드를 위해 서버를 오프라인 상태로 가져가야 하는 것은 대부분의 조직에게 큰 부담입니다. 따라서 마이그레이션이 신중하게 작성되어 온라인으로 적용되고 아래 스타일 가이드에 따라야 합니다.
마이그레이션은 GitLab 설치가 오프라인 상태로 전환되어서는 안됩니다. 마이그레이션은 항상 다운타임을 피하기 위해 작성되어야 합니다. 이전에 downtime을 허용하는 마이그레이션을 정의하는 프로세스가 있었습니다. DOWNTIME
상수를 설정하여 downtime을 허용했습니다. 이전의 마이그레이션을 살펴보면 이를 볼 수 있습니다. 그런데 이 프로세스는 4년 동안 사용되지 않았으며, 따라서 항상 다른 방법으로 마이그레이션을 작성하여 downtime을 피할 수 있다는 것을 배웠습니다.
마이그레이션을 작성할 때, 데이터베이스에는 구식 데이터나 불일치가 있을 수 있으므로 이를 고려하고 가드하세요. 데이터베이스 상태에 대해 가능한 한 적은 가정을 하도록 노력하세요.
또한 향후 버전에서 변경될 수 있는 GitLab 특정 코드에 의존하지 마세요. 필요한 경우 마이그레이션에 GitLab 코드를 복사하여 앞으로 호환되게 만드세요.
적절한 마이그레이션 유형 선택
새로운 마이그레이션을 추가하기 전에 가장 적합한 유형을 결정해야 합니다.
현재 수행 할 수있는 세 가지 종류의 마이그레이션이 있으며, 수행 할 작업의 종류 및 완료까지 걸리는 시간에 따라 달라집니다.
-
일반 스키마 마이그레이션. 이는
db/migrate
에서 실행되는 전통적인 Rails 마이그레이션입니다. (GitLab.com의 경우 Canary가 배포되기 전), 새로운 응용 프로그램 코드가 배포되기 전에 실행됩니다. 이는 배포를 불필요하게 지연시키지 않기 위해 몇 분 내로 상대적으로 빨리 완료되어야 합니다.단 하나의 예외는 응용 프로그램의 기능이나 성능이 심각하게 저하되는 변경사항이 없고, 지연할 수없는 변경사항인 마이그레이션일 뿐입니다. 예를 들어, 고유한 튜플을 시행하는 인덱스나 응용 프로그램의 중요한 부분에서 쿼리 성능에 필요한 경우입니다. 그러나 마이그레이션이 받아들일 수없을 만큼 느린 상황에서는 기능 플래그로 기능을 가드하는 것이 더 나을 수 있습니다.
새로운 모델을 추가하는 마이그레이션 또한 이러한 일반 스키마 마이그레이션의 일부입니다. 유일한 차이점은 마이그레이션 생성에 사용되는 Rails 명령 및 모델과 모델의 추가 생성된 파일입니다.
-
배포 후 마이그레이션. 이는
db/post_migrate
에서 실행되는 Rails 마이그레이션입니다. GitLab.com 배포와는 별도로 실행됩니다. 보류중인 배후 마이그레이션은 릴리스 관리자의 재량에 따라 배후 배포 마이그레이션 파이프라인을 통해 매일 실행됩니다. 이러한 마이그레이션은 응용 프로그램이 작동하는 데 중요하지 않은 스키마 변경 또는 최대 수 분이 소요되는 데이터 마이그레이션에 사용할 수 있습니다.
배후 배포에 실행해야할 스키마 변경에 대한 일반적인 예로는 다음이 있습니다.
- 사용되지 않는 열 제거와 같은 정리.
- 고트래픽 테이블에 중요하지 않은 인덱스 추가.
- 오랜 시간이 소요되는 중요하지 않은 인덱스 추가.
이러한 마이그레이션은 응용 프로그램이 작동하는 데 중요한 스키마 변경에 사용해서는 안 됩니다. 이러한 스키마 변경을 배후 배포 마이그레이션으로 수행하는 것은 이전에 문제가 발생했습니다. 예를 들어, 이 문제가 있습니다.
항상 정규 스키마 마이그레이션으로 수행해야 할 변경 사항은 다음과 같습니다.
- 새 테이블 생성, 예시: `create_table`.
- 기존 테이블에 새 열 추가, 예시: `add_column`.
NOTE:
배후 배포 마이그레이션은 종종 PDM으로 약어로 사용됩니다.
- 일괄 배경 마이그레이션. 이는 일반적인 Rails 마이그레이션이 아니라 Sidekiq 작업을 통해 실행되는 응용 프로그램 코드이나 배후 배포 마이그레이션을 사용하여 예약됩니다. 이러한 마이그레이션은 배후 배포 마이그레이션의 타이밍 가이드라인을 초과하는 데이터 마이그레이션에만 사용하세요. 일괄 배경 마이그레이션은 스키마를 취소해서는 안됩니다.
결정을 안내하는 데 이 다이어그램을 사용하세요. 그러나 이는 도구일 뿐이며 최종 결과는 항상 구체적으로 변경되고 따라 달라질 것입니다.
데이터 마이그레이션 파이프라인의 결과에는 마이그레이션에 대한 타이밍 정보가 포함됩니다. 데이터베이스 마이그레이션 파이프라인 결과를 참조하세요.
마이그레이션이 얼마나 걸리는지
일반적으로 한 번의 배포에 대한 모든 마이그레이션은 GitLab.com에서 1시간을 넘게 걸리지 안합니다. 다음 지침은 강제 규칙은 아니며, 마이그레이션 기간을 최소화하려고 추정된 지침입니다.
마이그레이션 유형 | 권장 기간 | 참고 |
---|---|---|
정규 마이그레이션 | <= 3분
| 응용 프로그램 기능 또는 성능이 심각하게 저하되고 지연될 수없는 변경사항을 제외한 변경은 유효한 예외입니다. |
배후 배포 마이그레이션 | <= 10분
| 스키마 변경을 제외한 변경을 스키마 변경으로 수행할 수 없기 때문에 유효한 예외입니다. |
배경 마이그레이션 | > 10분
| 이러한 마이그레이션은 큰 테이블에 적합하기 때문에 정밀한 타이밍 가이드라인을 설정할 수는 없으나, 단일 쿼리는 1초의 실행 시간을 초과해서는 안됩니다. |
대상 데이터베이스 선택
GitLab은 두 가지 다른 Postgres 데이터베이스인 ‘main’과 ‘ci’에 연결됩니다. 이 분리는 마이그레이션이 어느 데이터베이스든지 또는 둘 다에서 실행될 수 있기 때문에 영향을 줄 수 있습니다.
다중 데이터베이스에 대한 마이그레이션에서 마이그레이션을 추가하는 경우 이를 고려해야 하는지 또는 어떻게 고려해야 하는지 이해해보세요.
일반 스키마 마이그레이션 생성
마이그레이션을 생성하려면 다음의 Rails 생성기를 사용할 수 있습니다:
bundle exec rails g migration 마이그레이션_이름_여기에
이렇게 하면 db/migrate
에 마이그레이션 파일이 생성됩니다.
새 모델을 추가하는 일반 스키마 마이그레이션
새 모델을 생성하려면 다음의 Rails 생성기를 사용할 수 있습니다:
bundle exec rails g model 모델_이름_여기에
이렇게 하면 다음과 같은 파일이 생성됩니다:
-
db/migrate
에 마이그레이션 파일 -
app/models
에 모델 파일 -
spec/models
에 스펙 파일
스키마 변경
스키마 변경은 db/structure.sql
에 커밋해야 합니다. 이 파일은 일반적으로 bundle exec rails db:migrate
를 실행할 때 Rails에 의해 자동으로 생성되므로 일반적으로 이 파일을 직접 편집해서는 안 됩니다. 테이블에 열을 추가하는 경우 해당 열이 하단에 추가됩니다. 기존 테이블에 대해 열 순서를 수동으로 재정렬하지 마십시오. 이는 Rails에 의해 생성된 db/structure.sql
을 사용하는 다른 사람들에게 혼란을 야기시킵니다.
add_concurrent_index
로 인덱스를 추가하는 머지 요청에 스키마 변경을 커밋하십시오.로컬 데이터베이스가 main
의 스키마와 다른 경우에는 스키마 변경을 Git에 깔끔하게 커밋하기 어려울 수 있습니다.
이 경우 scripts/regenerate-schema
스크립트를 사용하여 추가하는 마이그레이션에 대한 깔끔한 db/structure.sql
을 다시 생성할 수 있습니다. 이 스크립트는 db/migrate
또는 db/post_migrate
에서 찾은 모든 마이그레이션을 적용하므로 스키마에 커밋하고 싶지 않은 마이그레이션이 있는 경우에는 해당 마이그레이션을 이름을 바꾸거나 제거하십시오. 브랜치가 기본 Git 브랜치를 타겟팅하지 않는 경우 TARGET
환경 변수를 설정할 수 있습니다.
# `main`에 대해 스키마 다시 생성
scripts/regenerate-schema
# `12-9-stable-ee`에 대해 스키마 다시 생성
TARGET=12-9-stable-ee scripts/regenerate-schema
scripts/regenerate-schema
스크립트는 추가적인 차이를 만들 수 있습니다. 이런 경우에는 수동 절차를 사용하십시오. 여기서 <마이그레이션 ID>
는 마이그레이션 파일의 DATETIME
부분입니다.
# 마스터에 리베이스
git rebase master
# 변경 사항 롤백
VERSION=<마이그레이션 ID> bundle exec rails db:rollback:main
# 마스터로부터 db/structure.sql 체크아웃
git checkout origin/master db/structure.sql
# 변경사항 마이그레이션
VERSION=<마이그레이션 ID> bundle exec rails db:migrate:main
테이블을 만든 후에는 데이터베이스 사전 안에 추가해야 합니다.
다운타임 피하기
“마이그레이션에서 다운타임 피하기” 문서는 다음과 같은 다양한 데이터베이스 작업을 지정하며:
이러한 작업을 다운타임 없이 수행하는 방법을 설명하고 있습니다.
되돌릴 수 있는지 여부
귀하의 마이그레이션은 반드시 되돌릴 수 있어야 합니다. 이는 매우 중요한 점으로, 취약점 또는 버그가 발생한 경우에 되돌릴 수 있어야 합니다.
참고: GitLab 프로덕션 환경에서 문제가 발생한 경우, db:rollback
을 사용하여 마이그레이션을 롤백하는 대신 롤-포워드 전략이 사용됩니다. 자체 관리형 인스턴스에서는 업그레이드 프로세스가 시작되기 전에 생성된 백업을 복원할 것을 권장합니다. down
메서드는 주로 개발 환경에서 사용되며, 예를 들어 개발자가 커밋이나 브랜치 간에 전환할 때 로컬 structure.sql
파일과 데이터베이스가 일관된 상태인지 확인하고 싶을 때 사용됩니다.
귀하의 마이그레이션에는 되돌림 가능성을 테스트한 방법에 대한 설명이 포함되어야 합니다.
일부 마이그레이션은 되돌릴 수 없습니다. 예를 들어, 일부 데이터 마이그레이션은 데이터베이스의 상태에 대한 정보를 잃기 때문에 되돌릴 수 없을 수 있습니다. 그럼에도 불구하고 마이그레이션 자체는 되돌릴 수 있어야 하므로 up
메소드에서 수행된 변경 내용을 되돌릴 수 없는 이유를 설명하는 주석과 함께 down
메소드를 작성해야 합니다.
def down
# 아무 작업도 수행하지 않음
# `up` 메소드에서 수행된 변경을 되돌릴 수 없는 이유에 대한 설명하는 주석
end
이와 같은 마이그레이션은 본질적으로 리스크가 있는데, 데이터 마이그레이션 추가 시 추가 조치가 필요합니다.
원자성과 트랜잭션
기본적으로 마이그레이션은 단일 트랜잭션이며, 마이그레이션이 시작되면 열려 있으며 모든 단계가 처리된 후에 커밋됩니다.
마이그레이션을 단일 트랜잭션으로 실행하면 모든 단계 중 하나가 실패하면 나머지 단계가 실행되지 않으므로 데이터베이스가 유효한 상태로 유지됩니다. 따라서 다음 중 하나를 해야 합니다:
- 모든 마이그레이션을 단일 트랜잭션으로 묶습니다.
- 필요한 경우 대부분의 작업을 하는 마이그레이션을 하나 작성하고 단일 트랜잭션으로 수행할 수 없는 단계를 위해 별도의 마이그레이션을 작성합니다.
예를 들어, 빈 테이블을 만들고 해당 테이블에 대해 인덱스를 빌드해야 할 경우, 일반 단일 트랜잭션 마이그레이션을 사용하고 기본적인 rails 스키마 문을 사용해야 합니다: add_index
.
이 작업은 블로킹 작업이지만 테이블이 아직 사용되지 않으므로 아직 레코드가 없습니다.
단일 트랜잭션에서의 무거운 작업
단일 트랜잭션 마이그레이션을 사용할 때, 트랜잭션이 마이그레이션의 기간 동안 데이터베이스 연결을 유지하므로 마이그레이션 내의 작업이 너무 오래 걸리지 않도록 해야 합니다. 일반적으로 트랜잭션은 빠르게 실행되어야 합니다. 이를 위해 마이그레이션에서 실행되는 각 쿼리의 최대 쿼리 시간 제한을 확인하세요.
당신의 단일 트랜잭션 마이그레이션이 완료되기까지 시간이 오래 걸린다면, 여러 옵션이 있습니다. 모든 경우에 해당하는 적절한 마이그레이션 유형을 선택하는 것을 기억하세요. 이를 위해 마이그레이션이 얼마나 오래 걸리는지에 따라 다른 마이그레이션 유형을 선택해야 합니다.
-
마이그레이션을 여러 단일 트랜잭션 마이그레이션으로 분할합니다.
-
disable_ddl_transaction!
을 사용하여 여러 트랜잭션을 사용합니다. (#disable-transaction-wrapped-migration). -
문장 및 잠금 시간 초과 설정을 조정한 후에도 단일 트랜잭션 마이그레이션을 계속 사용합니다. 만약 여러분의 많은 작업 부하가 트랜잭션의 보장을 사용해야 한다면, 마이그레이션이 타임아웃 제한에 도달하지 않고 실행될 수 있는지 확인해야 합니다. 이 조언은 단일 트랜잭션 마이그레이션과 개별 트랜잭션에 둘 다 적용됩니다.
- 문장 시간 초과: 문장 시간 초과는 GitLab.com의 프로덕션 데이터베이스에
15초
로 구성되어 있지만, 색인을 만드는 데에는 종종 15초보다 더 걸립니다.add_concurrent_index
를 포함한 기존 도우미를 사용할 때는, 필요에 따라 자동으로 문장 시간 초과를 끕니다. 드물지만, 문장 시간 제한을 disable_statement_timeout`를 사용하여 직접 설정해야 할 수도 있습니다.
- 문장 시간 초과: 문장 시간 초과는 GitLab.com의 프로덕션 데이터베이스에
참고:
마이그레이션을 실행하기 위해 PgBouncer를 우회하여 기본 데이터베이스에 직접 연결합니다. statement_timeout
및 lock_wait_timeout
와 같은 설정을 제어합니다.
문장 시간 초과 제한을 일시적으로 해제하세요
마이그레이션 도우미 disable_statement_timeout
를 사용하면 트랜잭션 또는 연결당 문장 시간 제한을 일시적으로 0
으로 설정할 수 있습니다.
-
트랜잭션 당 옵션은
CREATE INDEX CONCURRENTLY
와 같이 명시적 트랜잭션 내에서 실행되지 않는 문장에 사용됩니다. -
만약
ALTER TABLE ... VALIDATE CONSTRAINT
과 같이 명시적 트랜잭션 블록 내에서 실행되는 문장을 지원한다면, 트랜잭션 당 옵션을 사용해야 합니다.
disable_statement_timeout
사용은 드물게 필요합니다. 대부분의 마이그레이션 도우미는 이미 필요할 때 내부적으로 사용합니다.
예를 들어, 색인을 만드는 것은 보통 GitLab.com의 프로덕션 데이터베이스에서 기본으로된 15초의 문장 시간 제한보다 더 오래 걸립니다.
add_concurrent_index
는 연결당 문장 시간 제한을 일시적으로 해제하기 위해 disable_statement_timeout
에 전달되는 블록 내에 인덱스를 생성합니다.
만약 마이그레이션에 직접적인 SQL 문을 작성하는 경우에는 disable_statement_timeout
를 수동으로 사용해야 합니다.
이를 할 때에는 데이터베이스 검토자들과 유지보수자들을 상담해야 합니다.
트랜잭션 랩핑된 마이그레이션 해제
disable_ddl_transaction!
을 사용하여 여러분의 마이그레이션을 단일 트랜잭션으로 실행하지 않도록 선택할 수 있습니다.
이는 ActiveRecord 메소드입니다.
이 메소드는 다른 데이터베이스 시스템에서도 동작할 수 있지만, GitLab에서는 오로지 PostgreSQL을 사용합니다.
disable_ddl_transaction!
은 항상 다음과 같이 읽어야 합니다:
“이 마이그레이션을 단일 PostgreSQL 트랜잭션으로 실행하지 마십시오. 필요할 때만 PostgreSQL 트랜잭션을 엽니다.”
참고:
명시적인 PostgreSQL 트랜잭션 .transaction
(또는 BEGIN; COMMIT;
)을 사용하지 않더라도, 모든 SQL 문은 여전히 트랜잭션으로 실행됩니다.
트랜잭션에 관한 PostgreSQL 문서를 참조하세요.
참고:
GitLab에서는 disable_ddl_transaction!
을 사용하는 마이그레이션을 가끔씩 비트 트랜잭션 마이그레이션으로 언급했습니다. 이것은 그 마이그레이션이 단일 트랜잭션으로 실행되지는 않았다는 것을 의미합니다.
언제 disable_ddl_transaction!
을 사용해야 하나요? 대부분의 경우에는, 기존의 RuboCop 규칙이나 마이그레이션 도우미가 disable_ddl_transaction!
을 사용해야 할지 감지할 수 있습니다.
당신의 마이그레이션에서 그것을 사용해야 할지 확신이 없다면 disable_ddl_transaction!
을 사용하지 마시고 RuboCop 규칙과 데이터베이스 검토를 따르도록 하세요.
모든 SQL 문이 여전히 트랜잭션으로 실행됩니다.
PostgreSQL에서 명시적 트랜잭션 내에서 (BEGIN
블록을 열고 COMMIT
을 사용하는 것) 실행할 코드는 disable_ddl_transaction!
을 사용해야 합니다.
-
CREATE INDEX CONCURRENTLY
와 같은 작업은 보통 트랜잭션 내에서 실행된CREATE INDEX
와 다르게, 반드시 트랜잭션 외부에서 실행되어야 합니다. 따라서 마이그레이션이 단 하나의 문장CREATE INDEX CONCURRENTLY
를 실행한다 해도,disable_ddl_transaction!
을 사용해야 합니다. 또한 도우미add_concurrent_index
의 사용이disable_ddl_transaction!
을 요구하는 이유입니다.CREATE INDEX CONCURRENTLY
는 규칙보다는 예외에 가깝습니다.
마이그레이션 중에 여러 번의 트랜잭션을 실행해야 할 때에 disable_ddl_transaction!
을 사용해야 합니다.
대부분의 경우, 여러 데이터의 삽입, 업데이트 또는 삭제(DML)를 할 때에는
배치로 수행해야 합니다.
배치 작업을 그룹으로 묶어야 할 경우에, 배치 처리 중에 명시적으로 트랜잭션 블록을 열 수 있습니다.
하나의 상당히 큰 작업 부하에 대해 일괄 배경 마이그레이션을 사용하세요.
마이그레이션 도우미들이 요구하는 경우에 disable_ddl_transaction!
을 사용해야 합니다.
여러 마이그레이션 도우미들은 정확한 시기와 방법으로 트랜잭션을 열도록 요구하기 때문에 disable_ddl_transaction!
과 함께 실행되어야 합니다.
- 외래 키는
CREATE INDEX CONCURRENTLY
와 달리 트랜잭션 내에서 추가할 수 있습니다. 그러나 PostgreSQL은CREATE INDEX CONCURRENTLY
와 유사한 옵션을 제공하지 않습니다. 대신add_concurrent_foreign_key
와 같은 도우미가 소스 및 대상 테이블의 잠금을 최소화하면서 외래 키를 추가하고 검증하기 위해 각자의 트랜잭션을 엽니다. - 어떤 RuboCop 검사가 위반되었는지 확인하고 확신이 없다면
disable_ddl_transaction!
을 건너뛰세요.
마이그레이션이 실제로 PostgreSQL 데이터베이스를 건드리지 않거나 다중 PostgreSQL 데이터베이스에 건드리는 경우에
disable_ddl_transaction!
을 사용해야 합니다.
- 예를 들어, 여러분의 마이그레이션이 Redis 서버를 타깃으로하는 경우에 사용하세요. 원칙적으로, 당신은 PostgreSQL 트랜잭션 내에서 외부 서비스와 상호 작용할 수 없습니다.
- 트랜잭션은 단일 데이터베이스 연결에 사용됩니다.
여러분의 마이그레이션이
ci
와main
데이터베이스를 둘 다 타겟으로 하는 경우에는 다중 데이터베이스에 대한 마이그레이션을 따르시기 바랍니다.
네이밍 규칙
데이터베이스 객체(테이블, 인덱스 및 뷰 등)의 이름은 소문자여아 합니다. 소문자 이름을 사용하면 인용 부호가 없는 이름의 쿼리가 오류를 발생시키지 않도록 합니다.
열의 이름은 ActiveRecord 스키마 규칙과 일관성 있게 유지됩니다.
사용자 정의 인덱스 및 제약 조건 이름은 제약 조건 명명 규칙 지침을 따라야 합니다.
긴 인덱스 이름 줄이기
PostgreSQL은 식별자(열 또는 인덱스 이름과 같은)의 길이를 제한합니다. 열 이름은 일반적으로 문제가 되지 않지만 인덱스 이름은 보통 더 깁니다. 이름을 너무 길게하는 경우 간결하게 하는 몇 가지 방법:
-
index_
대신i_
로 접두사를 붙입니다. - 중복된 접두사를 제외합니다. 예를 들어,
index_vulnerability_findings_remediations_on_vulnerability_remediation_id
를index_vulnerability_findings_remediations_on_remediation_id
로 줄입니다. - 열이 아닌 인덱스의 목적을 지정합니다. 즉,
index_users_for_unconfirmation_notification
과 같이 인덱스의 목적을 지정합니다.
마이그레이션 타임스탬프 연령
마이그레이션 파일 이름의 타임스탬프 부분은 마이그레이션이 실행되는 순서를 결정합니다. 다음 간에 대한 대략적인 상관 관계를 유지하는 것이 중요합니다:
- 마이그레이션이 GitLab 코드베이스에 추가된 시기.
- 마이그레이션 자체의 타임스탬프.
새로운 마이그레이션의 타임스탬프는 절대로 이전 필수 업그레이드 중지 이전이 되어서는 안 됩니다. 마이그레이션은 때때로 통합되며, 이전 필수 중지 이전에 타임스탬프가 있는 마이그레이션이 추가된 경우, 이슈 408304에서 발생한 것과 같은 문제가 발생할 수 있습니다.
예를 들어, 현재 GitLab 16.0을 개발 중이고, 이전 필수 중지는 15.11입니다. 15.11은 2023년 4월 23일에 출시되었습니다. 따라서 최소 허용 가능한 타임스탬프는 20230424000000입니다.
최선의 실천 방법
위의 내용은 엄격한 규칙으로 고려되어야 하지만, 마이그레이션 타임스탬프를 마지막 필수 중지 이후에 병합될 날짜에서 3주 이내로 유지하는 것이 최선의 실천 방법입니다.
마이그레이션 타임스탬프를 업데이트하려면:
-
ci
및main
데이터베이스의 마이그레이션을 다운그레이드합니다.rake db:migrate:down:main VERSION=<타임스탬프> rake db:migrate:down:ci VERSION=<타임스탬프>
- 마이그레이션 파일을 삭제합니다.
- 마이그레이션 스타일 가이드에 따라 마이그레이션을 다시 만듭니다.
마이그레이션 헬퍼 및 버전 관리
데이터베이스 마이그레이션에서 일반적인 패턴에 대한 다양한 헬퍼 메서드가 제공됩니다. 해당 헬퍼는 Gitlab::Database::MigrationHelpers
및 관련 모듈에서 찾을 수 있습니다.
시간이 지남에 따라 헬퍼의 동작을 변경할 수 있도록 데이터베이스 마이그레이션용 버전 관리 체계를 구현합니다. 이를 통해 기존 마이그레이션의 헬퍼 동작을 유지한 채 새로운 마이그레이션에 대한 헬퍼 동작을 변경할 수 있습니다.
이를 위해 모든 데이터베이스 마이그레이션은 최신 버전( Gitlab::Database::Migration::MIGRATION_CLASSES
에서 확인할 수 있음)의 Gitlab::Database::Migration
에서 상속되어야 합니다.
다음 예에서는 마이그레이션 클래스의 버전 2.1을 사용합니다.
class TestMigration < Gitlab::Database::Migration[2.1]
def change
end
end
마이그레이션에 Gitlab::Database::MigrationHelpers
를 직접 포함하지 마십시오. 대신, 최신 버전의 Gitlab::Database::Migration
을 사용하십시오. 최신 버전의 마이그레이션 헬퍼가 자동으로 노출됩니다.
데이터베이스 잠금 획득시 재시도 메커니즘
데이터베이스 스키마를 변경할 때 DDL (데이터 정의 언어) 문을 호출하는 헬퍼 메서드를 사용합니다. 일부 경우에는 이러한 DDL 문이 특정 데이터베이스 잠금이 필요합니다.
예:
def change
remove_column :users, :full_name, :string
end
이 마이그레이션을 실행하려면 users
테이블에 대한 배타적 잠금이 필요합니다. 테이블이 다른 프로세스에 의해 동시에 액세스되고 수정되는 경우, 잠금을 획득하는 데 시간이 걸릴 수 있습니다. 잠금 요청은 큐에 대기 중이며, 큐에 들어간 후 canceling statement due to statement timeout
오류로 실패할 수도 있습니다.
PostgreSQL 잠금에 대한 자세한 내용: 명시적 잠금
안정성을 위해 GitLab.com은 짧은 statement_timeout
을 설정합니다. 마이그레이션이 호출되면 모든 데이터베이스 쿼리는 실행되는 고정 시간이 있습니다. 최악의 경우, 요청이 잠금 큐에 있어 구성된 명령문 시간 초과 기간 동안 다른 쿼리를 차단한 후 canceling statement due to statement timeout
오류로 실패할 수 있습니다.
이 문제는 실패한 애플리케이션 업그레이드 프로세스와 심지어 애플리케이션 안정성 문제를 일으킬 수 있습니다. 테이블이 잠시 동안 접근할 수 없게 될 수 있기 때문입니다.
데이터베이스 마이그레이션의 신뢰성과 안정성을 높이기 위해 GitLab 코드베이스는 작업을 다시 시도할 수 있는 메소드를 제공하여 다양한 lock_timeout
설정 및 시도 간의 대기 시간을 허용합니다. 필요한 잠금을 획득하도록 여러 번 시도하는 것으로 데이터베이스가 다른 문을 처리할 수 있게 합니다.
잠금 다시 시도는 두 가지 다른 헬퍼에 의해 제어됩니다:
-
enable_lock_retries!
: 기본적으로 모든transactional
마이그레이션에 대해 활성화되어 있습니다. -
with_lock_retries
:non-transactional
마이그레이션 내의 블록에 대해 수동으로 활성화됩니다.
트랜잭션 마이그레이션
일반적인 마이그레이션은 트랜잭션 내에서 전체 마이그레이션을 실행합니다. 잠금 다시 시도 메커니즘은 기본적으로 활성화됩니다 (disable_ddl_transaction!
이 아닌 경우).
이로 인해 마이그레이션의 잠금 시간이 제어되며, 제한 내에서 잠금을 획득할 수 없는 경우 전체 마이그레이션을 다시 시도할 수도 있습니다.
가끔 마이그레이션은 다른 객체에 대해 여러 잠금을 획득해야 할 수 있습니다. 카달로그 부풀림을 방지하려면 DDL을 수행하기 전에 명시적으로 모든 잠금을 요청하세요. 더 나은 전략은 마이그레이션을 분할하여 한 번에 하나의 잠금만 획들해야 하도록하는 것입니다.
동일한 테이블에 대한 다수의 변경
잠금 다시 시도 방법이 활성화된 경우 모든 작업이 단일 트랜잭션으로 래핑됩니다. 잠금을 확보한 경우 다음에 다시 잠금을 확보하는 대신 트랜잭션 내에서 가능한 많은 작업을 수행해야 합니다. 블록 내에서 긴 데이터베이스 문을 실행하는 것에 주의하십시오. 획득한 잠금은 트랜잭션이(블록) 완료될 때까지 유지되며 잠금 유형에 따라 다른 데이터베이스 작업을 차단할 수 있습니다.
def up
add_column :users, :full_name, :string
add_column :users, :bio, :string
end
def down
remove_column :users, :full_name
remove_column :users, :bio
end
기본값 변경을 위한 열 값 변경
여러 릴리스 프로세스를 따르지 않으면 열 기본값을 변경하는 것은 응용 프로그램 다운타임을 발생시킬 수 있습니다. 자세한 내용은 데이터베이스에서 열 기본값 변경을 위한 다운타임 방지하기을 참조하십시오.
def up
change_column_default :merge_requests, :lock_version, from: nil, to: 0
end
def down
change_column_default :merge_requests, :lock_version, from: 0, to: nil
end
두 개의 외래 키가 있는 경우 새 테이블 생성
한 번에 한 외래 키만 트랜잭션 당 생성되어야 합니다. 외래 키 제약 조건 추가는 참조된 테이블에 SHARE ROW EXCLUSIVE
잠금이 필요하기 때문에 동일한 트랜잭션에서 여러 테이블을 잠그는 것을 피해야 합니다.
이를 위해 세 가지 마이그레이션이 필요합니다.
- 인덱스와 함께 외래 키가 없는 테이블 생성
- 첫 번째 테이블에 외래 키 추가
- 두 번째 테이블에 외래 키 추가
테이블 생성:
def up
create_table :imports do |t|
t.bigint :project_id, null: false
t.bigint :user_id, null: false
t.string :jid, limit: 255
t.index :project_id
t.index :user_id
end
end
def down
drop_table :imports
end
projects
에 외래 키 추가:
이 경우 add_concurrent_foreign_key
메서드를 사용할 수 있습니다. 왜냐하면 이 보조 메서드에는 잠금 재시도가 내장되어 있기 때문입니다.
disable_ddl_transaction!
def up
add_concurrent_foreign_key :imports, :projects, column: :project_id, on_delete: :cascade
end
def down
with_lock_retries do
remove_foreign_key :imports, column: :project_id
end
end
users
에 외래 키 추가:
disable_ddl_transaction!
def up
add_concurrent_foreign_key :imports, :users, column: :user_id, on_delete: :cascade
end
def down
with_lock_retries do
remove_foreign_key :imports, column: :user_id
end
end
트랜잭션 없는 마이그레이션 사용
disable_ddl_transaction!
을 사용하여 트랜잭션 마이그레이션을 비활성화하는 경우에만 with_lock_retries
도우미를 사용할 수 있습니다. 이는 주어진 블록을 실행하기 위해 트랜잭션을 엽니다.
사용자 지정 RuboCop 규칙을 통해 잠금 재시도 블록 내에 허용된 메서드만 배치할 수 있도록 보장합니다.
disable_ddl_transaction!
def up
with_lock_retries do
add_column(:users, :name, :text, if_not_exists: true)
end
add_text_limit :users, :name, 255 # 제약 조건 유효성 검사를 포함합니다 (전체 테이블 스캔)
end
RuboCop 규칙은 일반적으로 아래에 나열된 표준 Rails 마이그레이션 메서드만 허용합니다. 아래 예제는 RuboCop 위반을 일으킵니다:
disable_ddl_transaction!
def up
with_lock_retries do
add_concurrent_index :users, :name
end
end
도우미 메서드 사용 시기
with_lock_retries
도우미 메서드는 실행이 이미 오픈 트랜잭션 내에서 이루어지지 않은 경우에만 사용할 수 있습니다(기본 제공된 Rails 마이그레이션 도우미 메서드와 함께 사용할 수 있습니다). 동일한 테이블에서 여러 마이그레이션 도우미를 호출하는 것은 문제가 되지 않습니다.
with_lock_retries
도우미 메서드를 사용하는 것은 데이터베이스 마이그레이션이 고트래픽 테이블과 관련된 경우에 권장됩니다.
예시 변경 사항:
-
add_foreign_key
/remove_foreign_key
-
add_column
/remove_column
change_column_default
-
create_table
/drop_table
with_lock_retries
메서드는 change
메서드 내에서 사용할 수 없으며, 마이그레이션을 뒤로 되돌릴 수 있도록 up
및 down
메서드를 수동으로 정의해야 합니다.
도우미 메서드 작동 방식
- 50번 반복한다.
- 각 반복에서 사전 구성된
lock_timeout
를 설정합니다. - 주어진 블록(
remove_column
)을 실행하려고 시도합니다. -
LockWaitTimeout
오류가 발생하면 사전 구성된sleep_time
에 대기한 후 블록을 다시 시도합니다. - 오류가 발생하지 않으면 현재 반복에서 블록이 성공적으로 실행됩니다.
자세한 내용은 Gitlab::Database::WithLockRetries
클래스를 확인하십시오. with_lock_retries
도우미 메서드는 Gitlab::Database::MigrationHelpers
모듈에 구현되어 있습니다.
최악의 경우, 해당 메서드는 다음과 같습니다:
- 최대 50번에 걸쳐 블록을 40분 이상 실행합니다.
- 대부분의 시간은 각 반복 후 사전 구성된 대기 기간에 소요됩니다.
- 50번째 재시도 후 블록은
lock_timeout
없이 표준 마이그레이션 호출처럼 실행됩니다. - 잠금을 획들할 수 없는 경우 마이그레이션은
statement timeout
오류로 실패합니다.
매우 긴 실행 트랜잭션이 users
테이블에 액세스하는 경우 마이그레이션에 실패할 수 있습니다(40분 이상).
SQL 수준에서 잠금 재시도 방법론
이 섹션에서는 lock_timeout
의 간소화된 SQL 예제를 제공하여 lock_timeout
의 사용을 설명합니다. 각 스니펫을 여러 psql
세션에서 실행하여 따라 해볼 수 있습니다.
테이블을 변경하여 열을 추가할 때 AccessExclusiveLock
이 필요하며 이는 대부분의 잠금 유형과 충돌합니다. 대상 테이블이 매우 바쁜 경우 열을 추가하는 트랜잭션이 AccessExclusiveLock
을 적시에 획들지 못할 수 있습니다.
테이블에 행을 삽입하는 트랜잭션을 시도하는 경우를 가정해 봅니다:
-- 트랜잭션 1
BEGIN;
INSERT INTO my_notes (id) VALUES (1);
이 시점에서 트랜잭션 1은 my_notes
에서 RowExclusiveLock
을 획들었습니다. 트랜잭션 1은 커밋 또는 취소하기 전에 추가 명령을 실행할 수 있습니다. my_notes
에 접촉하는 다른 동시 트랜잭션이 있을 수 있습니다.
어떠한 잠금 재시도 도우미도 사용하지 않고 테이블에 열을 추가하려는 트랜잭션 마이그레이션이 시도된다고 가정해 봅니다:
-- 트랜잭션 2
BEGIN;
ALTER TABLE my_notes ADD COLUMN title text;
트랜잭션 2는 이제 AccessExclusiveLock
을 my_notes
테이블에서 획들지 못해서 차단됩니다. 왜냐하면 트랜잭션 1이 여전히 실행 중이고 my_notes
에서 RowExclusiveLock
을 보유하고 있기 때문입니다.
또한 요청한 AccessExclusiveLock
을 대기하고 있는 효과가 더 더러울 수 있습니다. 일반적으로, 다른 트랜잭션이 동시에 my_notes
테이블에서 읽기 및 쓰기를 시도했을 때, 트랜잭션이 커밋되기 전에 요청하는 잠금이 획들지 않고 동시에 실행될 수 있습니다. 그러나 AccessExclusiveLock
을 대기하고 있는 경우, 동시 트랜잭션이 RowExclusiveLock
을 획들는 것이 통상적으로 이와 충돌하지 않기 때문에 이에 관한 추가 요청은 차단됩니다.
with_lock_retries
를 사용할 경우 트랜잭션 2는 지정된 시간 내에 잠금을 획들지 못하고 빠르게 시간 초과됩니다. 이 때 만약 트랜잭션 2가 동일한 블록에 대해 반복적으로 시도합니다.
-- 트랜잭션 2 (잠금 시간 초과 버전)
BEGIN;
SET LOCAL lock_timeout to '100ms'; -- 잠금 재시도 도우미가 추가함.
ALTER TABLE my_notes ADD COLUMN title text;
SET LOCAL
은 매개변수(lock_timeout
) 변경 범위를 트랜잭션에 한정시킵니다.
인덱스 제거
테이블이 비어 있지 않은 경우 인덱스를 제거할 때는 일반 remove_index
메소드 대신 remove_concurrent_index
메소드를 사용해야 합니다.
remove_concurrent_index
메소드는 인덱스를 동시에 제거하므로 잠금이 필요하지 않으며 다운타임이 필요하지 않습니다. 이 메소드를 사용하려면 마이그레이션 본문에서 disable_ddl_transaction!
메소드를 호출하여 단일 트랜잭션 모드를 비활성화해야 합니다. 아래와 같이 마이그레이션 클래스에서 disable_ddl_transaction!
메소드를 호출하세요.
class MyMigration < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
INDEX_NAME = 'index_name'
def up
remove_concurrent_index :table_name, :column_name, name: INDEX_NAME
end
end
Grafana를 사용하여 해당 인덱스가 사용되지 않고 있는지 확인할 수 있습니다.
sum by (type)(rate(pg_stat_user_indexes_idx_scan{env="gprd", indexrelname="INSERT INDEX NAME HERE"}[30d]))
인덱스가 존재하는지 확인할 필요는 없지만 제거하려는 인덱스의 이름을 명시해야 합니다. 이는 remove_index
또는 remove_concurrent_index
의 적절한 형식으로 옵션으로 이름을 전달하거나 remove_concurrent_index_by_name
메소드를 사용하여 수행할 수 있습니다. 명시적으로 이름을 지정하는 것은 올바른 인덱스가 제거되도록 보장하기 위해 중요합니다.
테이블이 작은 경우(빈 테이블이거나 레코드가 1,000
개 미만인 경우) disable_ddl_transaction!
이 필요하지 않은 다른 작업과 결합하여 단일 트랜잭션 마이그레이션에 remove_index
를 사용하는 것이 좋습니다.
인덱스 비활성화
인덱스 추가
인덱스를 추가하기 전에 인덱스가 필요한지를 고려하세요. 데이터베이스 인덱스 추가 가이드에는 인덱스가 필요한지 여부를 결정하는 데 도움이 되는 자세한 내용과 인덱스 추가를 위한 모범 사례가 포함되어 있습니다.
인덱스의 존재 여부 테스트
마이그레이션이 인덱스의 부재 또는 존재에 따라 조건 로직을 필요로 하는 경우 해당 인덱스의 이름을 사용하여 존재 여부를 테스트해야 합니다. 이를 통해 Rails가 인덱스 정의를 비교하는 방법에 문제가 발생하는 것을 피하고 예상치 못한 결과를 방지할 수 있습니다.
더 자세한 내용은 데이터베이스 인덱스 추가 가이드를 확인하세요.
외래 키 제약 조건 추가
기존 열이나 새 열에 외래 키 제약 조건을 추가할 때 해당 열에 인덱스를 추가하는 것을 잊지 마세요.
이것은 모든 외래 키에 필요한 사항으로, 예를 들어 효율적인 캐스케이딩 삭제를 지원하기 위해 필요합니다. 테이블의 많은 행이 삭제될 때 참조된 레코드도 삭제해야 하는 경우입니다. 데이터베이스는 참조 테이블에서 해당 레코드를 찾아야 합니다. 인덱스가 없으면 이로 인해 테이블에서 순차 스캔이 발생하여 시간이 오래 걸릴 수 있습니다.
다음은 외래 키 제약 조건이 포함된 새 열을 추가하는 예시입니다. 이때 index: true
를 포함하여 해당 열에 인덱스를 생성하도록 합니다.
class Migration < Gitlab::Database::Migration[2.1]
def change
add_reference :model, :other_model, index: true, foreign_key: { on_delete: :cascade }
end
end
기존 테이블의 열에 외래 키 제약 조건을 추가해야 하는 경우 add_reference
대신 add_concurrent_foreign_key
및 add_concurrent_index
를 사용해야 합니다.
고트프래픽 테이블을 참조하지 않는 새 테이블 또는 빈 테이블이 있는 경우 disable_ddl_transaction!
이 필요하지 않은 단일 트랜잭션 마이그레이션에 add_reference
를 사용하는 것이 좋습니다. 이렇게 하면 disable_ddl_transaction!
가 필요하지 않은 다른 작업과 결합할 수 있습니다.
기존 열에 외래 키 제약 조건을 추가하는 방법에 대해 자세히 알아보세요.
NOT NULL
제약 조건
NOT NULL
제약 조건에 대한 스타일 가이드를 확인하여 자세한 내용을 알아보세요(database/not_null_constraints.md).
기본 값이 있는 열 추가
GitLab의 최소 PostgreSQL 11 버전을 고려할 때, 기본 값이 있는 열을 추가하는 것은 훨씬 쉬워졌으며 모든 경우에 표준 add_column
도우미를 사용해야 합니다.
PostgreSQL 11 이전에는 기본 값을 가진 열을 추가하는 것이 문제가 되었습니다. 완전한 테이블 다시 쓰기를 유발할 수 있었기 때문입니다.
NULL이 아닌 열의 기본 값 제거
NULL이 아닌 열을 추가하고 기본 값을 사용하여 기존 데이터를 채웠다면 해당 기본 값을 애플리케이션 코드가 업데이트된 이후에도 유지해야 하며, 이와 관련된 기본 값을 동일한 마이그레이션에서 제거할 수 없습니다. 마이그레이션이 모델 코드가 업데이트되기 전에 실행되며 모델이 이 열에 대해 알지 못해 설정할 수 없기 때문입니다. 이 경우 다음을 권장합니다.
- 표준 마이그레이션에서 기본 값을 가진 열을 추가합니다.
- 애플리케이션 다음 시작 후에 기본 값을 제거합니다.
애플리케이션이 재시작된 후에 발생하는 마이그레이션이기 때문에 새로운 열이 발견되었음을 보장합니다.
기본 열 변경
‘change_column_default’를 사용하여 기본 열을 변경하는 것은 큰 테이블의 경우 비싼 작업이고 사실상 열의 기본 값을 변경하는 것이 아닐 수 있습니다.
다음과 같은 마이그레이션을 살펴보겠습니다:
class DefaultRequestAccessGroups < Gitlab::Database::Migration[2.1]
def change
change_column_default(:namespaces, :request_access_enabled, from: false, to: true)
end
end
위의 마이그레이션은 namespaces
테이블 중 하나의 가장 큰 테이블 중 하나인 기본 열 값을 변경합니다. 이는 다음과 같이 번역될 수 있습니다:
ALTER TABLE namespaces
ALTER COLUMN request_access_enabled
SET DEFAULT false
특정 경우에는 기본값이 이미 존재하며 namespaces
테이블의 기존 레코드를 모두 다시 작성하지 않는다는 것을 의미하는, request_access_enabled
열의 메타데이터를 변경합니다. 기본값이 존재하거나 변경하는 것만으로는 namespaces
테이블의 모든 기존 레코드를 다시 작성할 필요가 없습니다. 새로운 기본값을 갖는 새로운 열을 만드는 경우에만 모든 레코드가 다시 작성될 것입니다.
참고: PostgreSQL 11.0에서 빠른 ALTER TABLE ADD COLUMN with a non-null default 가 도입되어, 새로운 기본값이 있는 열을 추가할 때 테이블을 다시 작성할 필요가 없어졌습니다.
위에서 언급한 이유로, disable_ddl_transaction!
을 필요로 하지 않는 단일 트랜잭션 마이그레이션에서 change_column_default
를 사용하는 것이 안전합니다.
기존 열 업데이트
특정 값으로 기존 열을 업데이트하려면 update_column_in_batches
를 사용할 수 있습니다. 이렇게 하면 업데이트가 일괄적으로 처리되어 단일 문에서 많은 행을 업데이트하지 않습니다.
projects
테이블의 foo
열을 some_column
이 'hello'
일 때 10으로 업데이트합니다:
update_column_in_batches(:projects, :foo, 10) do |table, query|
query.where(table[:some_column].eq('hello'))
end
계산된 업데이트가 필요한 경우, 값은 Arel.sql
로 래핑하여 Arel이 이를 SQL 리터럴로 처리하도록 할 수 있습니다. 또한 Rails 6에 필요한 사용 방법의 폐기가 필요합니다.
아래 예는 위의 예와 같지만, 값이 bar
및 baz
열의 곱으로 설정됩니다:
update_value = Arel.sql('bar * baz')
update_column_in_batches(:projects, :foo, update_value) do |table, query|
query.where(table[:some_column].eq('hello'))
end
update_column_in_batches
의 경우 큰 테이블에서 실행할 수 있지만, 테이블의 행 중 일부만 업데이트하는 경우에만 수용 가능할 수 있습니다. 그러나 GitLab.com 스테이징 환경에서 유효성을 검증하지 않고 무시해서는 안 됨을 무시하지 마십시오.
외래 키 제약 조건 제거
외래 키 제약 조건을 제거할 때, 외래 키와 관련된 두 테이블에서 잠금을 획들해야 합니다. 높은 쓰기 패턴의 테이블의 경우 with_lock_retries
를 사용하는 것이 좋으며, 그렇지 않으면 시간 내에 잠금을 확보하지 못할 수 있습니다. 또한 잠금을 획들할 때 데드락에 빠질 수 있습니다. 보통 애플리케이션은 parent,child
순서로 기록을 작성합니다. 그러나 외래 키를 제거할 때는 child,parent
순서로 잠금을 획듭니다. 이 문제를 해결하려면 잠금을 명시적으로 ‘parent,child’ 순서로 획득할 수 있습니다. 예를 들어 다음과 같습니다:
disable_ddl_transaction!
def up
with_lock_retries do
execute('lock table ci_pipelines, ci_builds in access exclusive mode')
remove_foreign_key :ci_builds, to_table: :ci_pipelines, column: :pipeline_id, on_delete: :cascade, name: 'the_fk_name'
end
end
def down
add_concurrent_foreign_key :ci_builds, :ci_pipelines, column: :pipeline_id, on_delete: :cascade, name: 'the_fk_name'
end
데이터베이스 테이블 삭제
참고: 테이블이 삭제된 후 데이터베이스 사전에 추가해야 합니다. 데이터베이스 사전 가이드의 단계를 따라야 합니다.
데이터베이스 테이블 삭제는 드물며, Rails에서 제공하는 ‘drop_table’ 메서드는 일반적으로 안전하다고 간주됩니다. 테이블을 삭제하기 전에 다음 사항을 고려해보세요:
만약 테이블에 고트래픽 테이블 (예: projects
)에 외래 키가 있다면, DROP TABLE
문은 구분 제한 시간 초과 오류(statement timeout error)가 발생할 때까지 동시 트래픽을 막을 것입니다.
테이블 에 컬럼(기능을 사용한 적이 없는 경우)과 외래 키가 없는 경우:
- 마이그레이션에서 ‘drop_table’ 메서드를 사용합니다.
def change
drop_table :my_table
end
테이블 에 레코드가 있지만 외래 키가 없는 경우:
- 테이블과 관련된 애플리케이션 코드(모델, 컨트롤러, 서비스 등)를 제거합니다.
- 배포 이후 마이그레이션에서 ‘drop_table’을 사용합니다.
코드가 사용되지 않음을 확신한다면 모두 하나의 마이그레이션 파일에 담을 수 있습니다. 리스크를 약간 줄이고 싶다면 애플리케이션 변경 사항이 병합된 후 두 번째 병합 요청으로 마이그레이션을 배치하는 것을 고려하십시오. 이 접근 방식은 롤백할 수 있는 기회를 제공합니다.
def up
drop_table :my_table
end
def down
# create_table ...
end
테이블 에 외래 키가 있는 경우:
- 테이블과 관련된 애플리케이션 코드(모델, 컨트롤러, 서비스 등)를 제거합니다.
- 배포 이후 마이그레이션에서 ‘with_lock_retries’ 도우미 메서드를 사용하여 외래 키를 제거합니다. 다른 후속 배포 마이그레이션에서 ‘drop_table’을 사용합니다.
코드가 사용되지 않음을 확신한다면 모두 하나의 마이그레이션 파일에 담을 수 있습니다. 리스크를 감소시키고 싶다면 애플리케이션 변경 사항이 병합된 후 두 번째 병합 요청으로 마이그레이션을 배치하는 것을 고려하십시오. 이 접근 방식은 롤백할 수 있는 기회를 제공합니다.
projects
테이블에서 외래 키를 처리하는 부분 않는 트랜잭션 마이그레이션의 외래 키 제거:
# first migration file
class RemovingForeignKeyMigrationClass < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
def up
with_lock_retries do
remove_foreign_key :my_table, :projects
end
end
def down
add_concurrent_foreign_key :my_table, :projects, column: COLUMN_NAME
end
end
테이블 삭제:
# second migration file
class DroppingTableMigrationClass < Gitlab::Database::Migration[2.1]
def up
drop_table :my_table
end
def down
# 삭제된 외래 키를 제외한 동일한 스키마로 create_table ...
end
end
시퀀스 삭제
- GitLab 15.1에 도입되었습니다.
시퀀스 삭제는 흔하지는 않지만, 데이터베이스팀에서 제공하는 drop_sequence
메소드를 사용할 수 있습니다.
내부적으로 다음과 같이 작동합니다.
시퀀스 제거:
- 시퀀스가 실제로 사용 중인 경우 기본 값을 제거합니다.
-
DROP SEQUENCE
를 실행합니다.
시퀀스 다시 추가:
- 해당 시퀀스를 생성하고, 현재 값 지정 가능합니다.
- 열의 기본 값을 변경합니다.
Rails 마이그레이션 예시:
class DropSequenceTest < Gitlab::Database::Migration[2.1]
def up
drop_sequence(:ci_pipelines_config, :pipeline_id, :ci_pipelines_config_pipeline_id_seq)
end
def down
default_value = Ci::Pipeline.maximum(:id) + 10_000
add_sequence(:ci_pipelines_config, :pipeline_id, :ci_pipelines_config_pipeline_id_seq, default_value)
end
end
참고: 외래 키가 있는 열에 시퀀스를 추가하는 것은 피해야 합니다. 이러한 열에 대해 시퀀스를 추가하는 것은 다시 이전 스키마 상태로 복원하기(down 메소드에서만) 허용됩니다.
테이블 잘라내기
- GitLab 15.11에 도입되었습니다.
테이블을 잘라내는 것은 흔하지 않지만, 데이터베이스팀에서 제공하는 truncate_tables!
메소드를 사용할 수 있습니다.
내부적으로 다음과 같이 작동합니다.
- 잘라내려는 테이블의
gitlab_schema
를 찾습니다. - 테이블의
gitlab_schema
가 연결의gitlab_schema
s에 포함되어 있는 경우, 그런 다음TRUNCATE
문을 실행합니다. - 테이블의
gitlab_schema
가 연결의gitlab_schema
s에 포함되어 있지 않으면 아무것도 하지 않습니다.
기본 키 교체
- GitLab 15.5에 도입되었습니다.
기본 키 교체는 파티션 키가 기본 키에 포함되어야 하기 때문에 테이블을 파티션화하는 데 필요합니다.
데이터베이스팀에서 제공하는 swap_primary_key
메소드를 사용할 수 있습니다.
내부적으로 다음과 같이 작동합니다.
- 기본 키 제약 조건을 삭제합니다.
- 미리 정의된 인덱스를 사용하여 기본 키를 추가합니다.
class SwapPrimaryKey < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
TABLE_NAME = :table_name
PRIMARY_KEY = :table_name_pkey
OLD_INDEX_NAME = :old_index_name
NEW_INDEX_NAME = :new_index_name
def up
swap_primary_key(TABLE_NAME, PRIMARY_KEY, NEW_INDEX_NAME)
end
def down
add_concurrent_index(TABLE_NAME, :id, unique: true, name: OLD_INDEX_NAME)
add_concurrent_index(TABLE_NAME, [:id, :partition_id], unique: true, name: NEW_INDEX_NAME)
unswap_primary_key(TABLE_NAME, PRIMARY_KEY, OLD_INDEX_NAME)
end
end
참고: 기본 키를 교체하려면 미리 새로운 인덱스를 도입하여야 합니다.
암호화된 속성
데이터베이스에 attr_encrypted
속성을 :text
으로 저장하지 마세요. 대신 :binary
를 사용하세요. 이렇게 하면 PostgreSQL에서 bytea
유형을 사용하여 저장을 더 효율적으로 처리할 수 있습니다:
class AddSecretToSomething < Gitlab::Database::Migration[2.1]
def change
add_column :something, :encrypted_secret, :binary
add_column :something, :encrypted_secret_iv, :binary
end
end
이진 열에 암호화된 속성을 저장할 때, attr_encrypted
에 encode: false
및 encode_iv: false
옵션을 제공해야 합니다:
class Something < ApplicationRecord
attr_encrypted :secret,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_32,
algorithm: 'aes-256-gcm',
encode: false,
encode_iv: false
end
테스트
Testing Rails migrations 스타일 가이드를 참조하세요.
데이터 마이그레이션
일반적으로 ActiveRecord 구문 대신 Arel 및 일반 SQL을 선호합니다. 일반 SQL을 사용하는 경우 모든 입력을 수동으로 quote_string
도우미로 인용해야 합니다.
Arel을 사용한 예:
users = Arel::Table.new(:users)
users.group(users[:user_id]).having(users[:id].count.gt(5))
# 이 결과를 사용하여 다른 테이블 업데이트
일반 SQL 및 quote_string
도우미를 사용한 예:
select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(id) > 1").each do |tag|
tag_name = quote_string(tag["name"])
duplicate_ids = select_all("SELECT id FROM tags WHERE name = '#{tag_name}'").map{|tag| tag["id"]}
origin_tag_id = duplicate_ids.first
duplicate_ids.delete origin_tag_id
execute("UPDATE taggings SET tag_id = #{origin_tag_id} WHERE tag_id IN(#{duplicate_ids.join(",")})")
execute("DELETE FROM tags WHERE id IN(#{duplicate_ids.join(",")})")
end
더 복잡한 로직이 필요한 경우, 마이그레이션 내부에서 로컬 모델을 정의하고 사용할 수 있습니다. 예를 들어:
class MyMigration < Gitlab::Database::Migration[2.1]
class Project < MigrationRecord
self.table_name = 'projects'
end
def up
# 데이터베이스를 업데이트하는 모든 모델의 열 정보를 재설정하여 Active Record의 테이블 구조에 대한 정보를 최신으로 유지
Project.reset_column_information
# ... ...
end
end
이렇게 함으로써 모델의 테이블 이름을 명시적으로 설정하여 클래스 이름이나 네임스페이스에서 파생되지 않도록 주의하세요.
마이그레이션에서 애플리케이션 코드 사용에 대한 제한 사항을 주의하세요.
기존 데이터 수정
대부분의 경우, 데이터베이스에서 데이터를 수정할 때 일괄(batch)로 마이그레이션하는 것이 좋습니다.
우리는 이 과정을 보다 효율적으로 순회하기위한 새로운 도우미 each_batch_range
를 도입했습니다. 일괄 크기의 기본값은 BATCH_SIZE
상수에 정의되어 있습니다.
아래 예시를 참고하세요.
일괄로 데이터 삭제:
include ::Gitlab::Database::DynamicModelHelpers
disable_ddl_transaction!
def up
each_batch_range('ci_pending_builds', scope: ->(table) { table.ref_protected }, of: BATCH_SIZE) do |min, max|
execute <<~SQL
DELETE FROM ci_pending_builds
USING ci_builds
WHERE ci_builds.id = ci_pending_builds.build_id
AND ci_builds.status != 'pending'
AND ci_builds.type = 'Ci::Build'
AND ci_pending_builds.id BETWEEN #{min} AND #{max}
SQL
end
end
- 첫 번째 인수는 수정되는 테이블입니다:
'ci_pending_builds'
. - 두 번째 인수는 관련 데이터 집합을 선택하는 람다를 호출합니다 (기본값은
.all
로 설정됨):scope: ->(table) { table.ref_protected }
. - 세 번째 인수는 일괄 크기입니다 (기본값은
BATCH_SIZE
상수에 설정됨):of: BATCH_SIZE
.
새 도우미를 사용하는 방법을 보여주는 예시 MR를 확인하십시오.
마이그레이션에서 애플리케이션 코드 사용 (비권장)
마이그레이션에서 애플리케이션 코드(모델 포함) 사용은 일반적으로 권장되지 않습니다. 이는 마이그레이션이 장기간 유지되고 의존하는 애플리케이션 코드가 변경되어 나중에 마이그레이션을 망칠 수 있기 때문입니다. 과거에는 일부 백그라운드 마이그레이션이 마이그레이션을 망치지 않으려면 모든 파일에 분산된 수백 줄의 코드를 복사하는 것을 피하기 위해 애플리케이션 코드를 사용해야 했던 경우도 있습니다. 이러한 드문 경우에는 누구나 코드를 리팩터링할 때 마이그레이션을 망치지 않는지 학습할 수 있는 훌륭한 테스트가 있어야 합니다. 애플리케이션 코드 사용은 또한 일괄 백그라운드 마이그레이션에 비권장됨에 사용되는 경우에도 비권장되며, 해당 모델은 마이그레이션 내에서 선언되어야 합니다.
일반적으로 마이그레이션에서 애플리케이션 코드(특히 모델)를 사용을 회피하기 위해 MigrationRecord
를 상속하는 클래스를 정의합니다(아래 예시 참조).
모델을 사용하는 경우 (마이그레이션 내에서 정의된 모델 포함) reset_column_information
을 사용하여 먼저 열(컬럼) 캐시를 지워야 합니다.
단일 테이블 상속(STI)을 활용하는 모델을 사용하는 경우 특별한 고려 사항이 있습니다.
이전 데이터베이스 마이그레이션에서 테이블이 수정되었다면 이 메소드를 사용해야 합니다. 만약 두 마이그레이션이 같은 db:migrate
프로세스에서 실행된다면 이 메소드를 사용해야 합니다.
예제:
class AddAndSeedMyColumn < Gitlab::Database::Migration[2.1]
class User < MigrationRecord
self.table_name = 'users'
end
def up
User.count # 모델이 컬럼 정보를 캐싱하는 모든 Active Record 호출
add_column :users, :my_column, :integer, default: 1
User.reset_column_information # 이전 스키마가 캐시에서 제거됩니다.
User.find_each do |user|
user.my_column = 42 if some_condition # Active Record는 정확한 스키마를 여기서 인식합니다.
user.save!
end
end
end
내부 테이블이 수정되고 Active Record를 사용하여 액세스합니다.
또한, 이전, 다른 마이그레이션에서 테이블이 수정되었는데, 두 마이그레이션이 같은 db:migrate
프로세스에서 실행된 경우 사용합니다.
이러한 결과를 가져옵니다. my_column
이 포함됨에 주목하세요.
캐시된 스키마를 지우지 않으면 (User.reset_column_information
를 건너뛰면) 컬럼이 Active Record에서 사용되지 않고 의도한 변경 사항이 이루어지지 않아 다음 결과를 가져옵니다. 여기서 쿼리에서 my_column
이 누락되는 것을 확인할 수 있습니다.
고트래픽 테이블
현재 고트래픽 테이블 목록은 다음과 같습니다.
어떤 테이블이 고트래픽인지 확인하는 것은 어렵습니다. 자체 관리형 인스턴스는 GitLab.com을 기반으로 한 가정만으로는 부족할 수 있으므로 GitLab의 다양한 기능과 사용 패턴을 이용할 것입니다.
GitLab.com을 위한 고트래픽 테이블을 식별하기 위해 다음과 같은 측정 항목이 고려됩니다. 여기에 연결된 메트릭은 GitLab 내부 전용입니다:
현재의 고트래픽 테이블과 비교하여 읽기 작업이 많은 테이블은 좋은 후보가 될 수 있습니다.
일반적인 규칙으로, 단순히 GitLab.com의 분석이나 보고용인 고트래픽 테이블에 열을 추가하는 것은 descloure하는 것입니다. 이는 직접적인 기능 가치를 제공하지 않으면서 모든 자체 관리형 인스턴스에 부정적인 성능 영향을 미칠 수 있습니다.
마일스톤
GitLab 16.6부터 모든 새로운 마이그레이션이 다음과 같은 구문을 사용하여 마일스톤을 지정해야 합니다.
class AddFooToBar < Gitlab::Database::Migration[2.2]
milestone '16.6'
def change
# 여기에 마이그레이션을 추가하세요.
end
end
마이그레이션에 올바른 마일스톤을 추가하면 GitLab 마이너 버전별로 마이그레이션을 논리적으로 분할할 수 있습니다. 이렇게 함으로써:
- 업그레이드 프로세스가 단순화됩니다.
- 순전히 마이그레이션의 타임스태프만을 사용하여 정렬하는 경우 발생하는 잠재적인 마이그레이션 순서 지정 문제를 경감합니다.
Autovacuum wraparound protection
이것은 PostgreSQL의 특수 autovacuum 실행 모드이며, vacuum을 하는 테이블에 ShareUpdateExclusiveLock
이 필요합니다. 큰 테이블의 경우 몇 시간이 걸릴 수 있으며, 동시에 테이블을 수정하려는 대부분의 DDL 마이그레이션이 잠금과 충돌할 수 있습니다. 마이그레이션이 시간 내에 잠금을 획들지 못하기 때문에 실패하고 배포를 차단할 수 있습니다.
배포 후 마이그레이션(PDM) 파이프라인은 테이블 중 하나에서 wraparound 예방 vacuum 프로세스를 감지하면 실행을 중지하고 확인할 수 있습니다. 이를 위해 마이그레이션 이름에 완전한 테이블 이름을 사용해야 합니다. 예를 들어 add_foreign_key_between_ci_builds_and_ci_job_artifacts
는 마이그레이션을 실행하기 전에 ci_builds
와 ci_job_artifacts
에서 vacuum을 확인합니다.
마이그레이션이 충돌하는 잠금이 없는 경우 완전한 테이블 이름을 사용하지 않고도 vacuum 확인을 건너뛸 수 있습니다. 예를 들어 create_async_index_on_job_artifacts
와 같은 경우입니다.