이주 가이드 스타일

GitLab의 마이그레이션을 작성할 때, 다음 사항을 고려해야 합니다. 이러한 작업은 모든 규모의 수백만 조직에서 실행되며, 데이터베이스에는 많은 연도의 데이터가 포함된 경우도 있습니다.

또한, 업그레이드를 위해 서버를 오프라인 상태로 가져가야 하는 것은 대부분의 조직에게 큰 부담입니다. 따라서 마이그레이션이 신중하게 작성되어 온라인으로 적용되고 아래 스타일 가이드에 따라야 합니다.

마이그레이션은 GitLab 설치가 오프라인 상태로 전환되어서는 됩니다. 마이그레이션은 항상 다운타임을 피하기 위해 작성되어야 합니다. 이전에 downtime을 허용하는 마이그레이션을 정의하는 프로세스가 있었습니다. DOWNTIME 상수를 설정하여 downtime을 허용했습니다. 이전의 마이그레이션을 살펴보면 이를 볼 수 있습니다. 그런데 이 프로세스는 4년 동안 사용되지 않았으며, 따라서 항상 다른 방법으로 마이그레이션을 작성하여 downtime을 피할 수 있다는 것을 배웠습니다.

마이그레이션을 작성할 때, 데이터베이스에는 구식 데이터나 불일치가 있을 수 있으므로 이를 고려하고 가드하세요. 데이터베이스 상태에 대해 가능한 한 적은 가정을 하도록 노력하세요.

또한 향후 버전에서 변경될 수 있는 GitLab 특정 코드에 의존하지 마세요. 필요한 경우 마이그레이션에 GitLab 코드를 복사하여 앞으로 호환되게 만드세요.

적절한 마이그레이션 유형 선택

새로운 마이그레이션을 추가하기 전에 가장 적합한 유형을 결정해야 합니다.

현재 수행 할 수있는 세 가지 종류의 마이그레이션이 있으며, 수행 할 작업의 종류 및 완료까지 걸리는 시간에 따라 달라집니다.

  1. 일반 스키마 마이그레이션. 이는 db/migrate에서 실행되는 전통적인 Rails 마이그레이션입니다. (GitLab.com의 경우 Canary가 배포되기 전), 새로운 응용 프로그램 코드가 배포되기 전에 실행됩니다. 이는 배포를 불필요하게 지연시키지 않기 위해 몇 분 내로 상대적으로 빨리 완료되어야 합니다.

    단 하나의 예외는 응용 프로그램의 기능이나 성능이 심각하게 저하되는 변경사항이 없고, 지연할 수없는 변경사항인 마이그레이션일 뿐입니다. 예를 들어, 고유한 튜플을 시행하는 인덱스나 응용 프로그램의 중요한 부분에서 쿼리 성능에 필요한 경우입니다. 그러나 마이그레이션이 받아들일 수없을 만큼 느린 상황에서는 기능 플래그로 기능을 가드하는 것이 더 나을 수 있습니다.

    새로운 모델을 추가하는 마이그레이션 또한 이러한 일반 스키마 마이그레이션의 일부입니다. 유일한 차이점은 마이그레이션 생성에 사용되는 Rails 명령 및 모델과 모델의 추가 생성된 파일입니다.

  2. 배포 후 마이그레이션. 이는 db/post_migrate에서 실행되는 Rails 마이그레이션입니다. GitLab.com 배포와는 별도로 실행됩니다. 보류중인 배후 마이그레이션은 릴리스 관리자의 재량에 따라 배후 배포 마이그레이션 파이프라인을 통해 매일 실행됩니다. 이러한 마이그레이션은 응용 프로그램이 작동하는 데 중요하지 않은 스키마 변경 또는 최대 수 분이 소요되는 데이터 마이그레이션에 사용할 수 있습니다.

배후 배포에 실행해야할 스키마 변경에 대한 일반적인 예로는 다음이 있습니다.

 - 사용되지 않는 열 제거와 같은 정리.
 - 고트래픽 테이블에 중요하지 않은 인덱스 추가.
 - 오랜 시간이 소요되는 중요하지 않은 인덱스 추가.

이러한 마이그레이션은 응용 프로그램이 작동하는 데 중요한 스키마 변경에 사용해서는 됩니다. 이러한 스키마 변경을 배후 배포 마이그레이션으로 수행하는 것은 이전에 문제가 발생했습니다. 예를 들어, 이 문제가 있습니다.

항상 정규 스키마 마이그레이션으로 수행해야 할 변경 사항은 다음과 같습니다.

- 새 테이블 생성, 예시: `create_table`.
- 기존 테이블에 새 열 추가, 예시: `add_column`.
  
NOTE:
배후 배포 마이그레이션은 종종 PDM으로 약어로 사용됩니다.
  1. 일괄 배경 마이그레이션. 이는 일반적인 Rails 마이그레이션이 아니라 Sidekiq 작업을 통해 실행되는 응용 프로그램 코드이나 배후 배포 마이그레이션을 사용하여 예약됩니다. 이러한 마이그레이션은 배후 배포 마이그레이션의 타이밍 가이드라인을 초과하는 데이터 마이그레이션에만 사용하세요. 일괄 배경 마이그레이션은 스키마를 취소해서는 안됩니다.

결정을 안내하는 데 이 다이어그램을 사용하세요. 그러나 이는 도구일 뿐이며 최종 결과는 항상 구체적으로 변경되고 따라 달라질 것입니다.

graph LR A{스키마 변경됨?} A -->|예| C{속도 또는 동작에<br/>중요한가?} A -->|아니오| D{빠른가요?} C -->|예| H{빠른가요?} C -->|아니오| F[배후 배포 마이그레이션] H -->|예| E[정규 마이그레이션] H -->|아니오| I[배후 배포 마이그레이션<br/>+ 기능 플래그] D -->|예| F[배후 배포 마이그레이션] D -->|아니오| G[배경 마이그레이션]

데이터 마이그레이션 파이프라인의 결과에는 마이그레이션에 대한 타이밍 정보가 포함됩니다. 데이터베이스 마이그레이션 파이프라인 결과를 참조하세요.

마이그레이션이 얼마나 걸리는지

일반적으로 한 번의 배포에 대한 모든 마이그레이션은 GitLab.com에서 1시간을 넘게 걸리지 합니다. 다음 지침은 강제 규칙은 아니며, 마이그레이션 기간을 최소화하려고 추정된 지침입니다.

note
모든 기간은 GitLab.com 대비로 측정되어야합니다.
note
데이터베이스 마이그레이션 파이프라인 결과에는 마이그레이션의 타이밍 정보가 포함됩니다.
마이그레이션 유형 권장 기간 참고
정규 마이그레이션 <= 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을 사용하는 다른 사람들에게 혼란을 야기시킵니다.

note
비동기적으로 인덱스를 생성하려면 두 개의 병합 요청이 필요합니다. 작업이 완료되면 인덱스가 추가된 스키마 변경을 커밋하십시오. 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. 이 작업은 블로킹 작업이지만 테이블이 아직 사용되지 않으므로 아직 레코드가 없습니다.

note
서브트랜잭션이 일반적으로 허용되지 않습니다. 필요한 경우 단일 트랜잭션에서 무거운 작업에 여러 개의 별도 트랜잭션을 사용합니다.

단일 트랜잭션에서의 무거운 작업

단일 트랜잭션 마이그레이션을 사용할 때, 트랜잭션이 마이그레이션의 기간 동안 데이터베이스 연결을 유지하므로 마이그레이션 내의 작업이 너무 오래 걸리지 않도록 해야 합니다. 일반적으로 트랜잭션은 빠르게 실행되어야 합니다. 이를 위해 마이그레이션에서 실행되는 각 쿼리의 최대 쿼리 시간 제한을 확인하세요.

당신의 단일 트랜잭션 마이그레이션이 완료되기까지 시간이 오래 걸린다면, 여러 옵션이 있습니다. 모든 경우에 해당하는 적절한 마이그레이션 유형을 선택하는 것을 기억하세요. 이를 위해 마이그레이션이 얼마나 오래 걸리는지에 따라 다른 마이그레이션 유형을 선택해야 합니다.

  • 마이그레이션을 여러 단일 트랜잭션 마이그레이션으로 분할합니다.

  • disable_ddl_transaction!을 사용하여 여러 트랜잭션을 사용합니다. (#disable-transaction-wrapped-migration).

  • 문장 및 잠금 시간 초과 설정을 조정한 후에도 단일 트랜잭션 마이그레이션을 계속 사용합니다. 만약 여러분의 많은 작업 부하가 트랜잭션의 보장을 사용해야 한다면, 마이그레이션이 타임아웃 제한에 도달하지 않고 실행될 수 있는지 확인해야 합니다. 이 조언은 단일 트랜잭션 마이그레이션과 개별 트랜잭션에 둘 다 적용됩니다.

    • 문장 시간 초과: 문장 시간 초과는 GitLab.com의 프로덕션 데이터베이스에 15초로 구성되어 있지만, 색인을 만드는 데에는 종종 15초보다 더 걸립니다. add_concurrent_index를 포함한 기존 도우미를 사용할 때는, 필요에 따라 자동으로 문장 시간 초과를 끕니다. 드물지만, 문장 시간 제한을 disable_statement_timeout`를 사용하여 직접 설정해야 할 수도 있습니다.

참고: 마이그레이션을 실행하기 위해 PgBouncer를 우회하여 기본 데이터베이스에 직접 연결합니다. statement_timeoutlock_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 트랜잭션 내에서 외부 서비스와 상호 작용할 수 없습니다.
  • 트랜잭션은 단일 데이터베이스 연결에 사용됩니다. 여러분의 마이그레이션이 cimain 데이터베이스를 둘 다 타겟으로 하는 경우에는 다중 데이터베이스에 대한 마이그레이션을 따르시기 바랍니다.

네이밍 규칙

데이터베이스 객체(테이블, 인덱스 및 뷰 등)의 이름은 소문자여아 합니다. 소문자 이름을 사용하면 인용 부호가 없는 이름의 쿼리가 오류를 발생시키지 않도록 합니다.

열의 이름은 ActiveRecord 스키마 규칙과 일관성 있게 유지됩니다.

사용자 정의 인덱스 및 제약 조건 이름은 제약 조건 명명 규칙 지침을 따라야 합니다.

긴 인덱스 이름 줄이기

PostgreSQL은 식별자(열 또는 인덱스 이름과 같은)의 길이를 제한합니다. 열 이름은 일반적으로 문제가 되지 않지만 인덱스 이름은 보통 더 깁니다. 이름을 너무 길게하는 경우 간결하게 하는 몇 가지 방법:

  • index_ 대신 i_로 접두사를 붙입니다.
  • 중복된 접두사를 제외합니다. 예를 들어, index_vulnerability_findings_remediations_on_vulnerability_remediation_idindex_vulnerability_findings_remediations_on_remediation_id로 줄입니다.
  • 열이 아닌 인덱스의 목적을 지정합니다. 즉, index_users_for_unconfirmation_notification과 같이 인덱스의 목적을 지정합니다.

마이그레이션 타임스탬프 연령

마이그레이션 파일 이름의 타임스탬프 부분은 마이그레이션이 실행되는 순서를 결정합니다. 다음 간에 대한 대략적인 상관 관계를 유지하는 것이 중요합니다:

  1. 마이그레이션이 GitLab 코드베이스에 추가된 시기.
  2. 마이그레이션 자체의 타임스탬프.

새로운 마이그레이션의 타임스탬프는 절대로 이전 필수 업그레이드 중지 이전이 되어서는 안 됩니다. 마이그레이션은 때때로 통합되며, 이전 필수 중지 이전에 타임스탬프가 있는 마이그레이션이 추가된 경우, 이슈 408304에서 발생한 것과 같은 문제가 발생할 수 있습니다.

예를 들어, 현재 GitLab 16.0을 개발 중이고, 이전 필수 중지는 15.11입니다. 15.11은 2023년 4월 23일에 출시되었습니다. 따라서 최소 허용 가능한 타임스탬프는 20230424000000입니다.

최선의 실천 방법

위의 내용은 엄격한 규칙으로 고려되어야 하지만, 마이그레이션 타임스탬프를 마지막 필수 중지 이후에 병합될 날짜에서 3주 이내로 유지하는 것이 최선의 실천 방법입니다.

마이그레이션 타임스탬프를 업데이트하려면:

  1. cimain 데이터베이스의 마이그레이션을 다운그레이드합니다.

    rake db:migrate:down:main VERSION=<타임스탬프>
    rake db:migrate:down:ci VERSION=<타임스탬프>
    
  2. 마이그레이션 파일을 삭제합니다.
  3. 마이그레이션 스타일 가이드에 따라 마이그레이션을 다시 만듭니다.

마이그레이션 헬퍼 및 버전 관리

데이터베이스 마이그레이션에서 일반적인 패턴에 대한 다양한 헬퍼 메서드가 제공됩니다. 해당 헬퍼는 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 설정 및 시도 간의 대기 시간을 허용합니다. 필요한 잠금을 획득하도록 여러 번 시도하는 것으로 데이터베이스가 다른 문을 처리할 수 있게 합니다.

잠금 다시 시도는 두 가지 다른 헬퍼에 의해 제어됩니다:

  1. enable_lock_retries! : 기본적으로 모든 transactional 마이그레이션에 대해 활성화되어 있습니다.
  2. 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 잠금이 필요하기 때문에 동일한 트랜잭션에서 여러 테이블을 잠그는 것을 피해야 합니다.

이를 위해 세 가지 마이그레이션이 필요합니다.

  1. 인덱스와 함께 외래 키가 없는 테이블 생성
  2. 첫 번째 테이블에 외래 키 추가
  3. 두 번째 테이블에 외래 키 추가

테이블 생성:

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 메서드 내에서 사용할 수 없으며, 마이그레이션을 뒤로 되돌릴 수 있도록 updown 메서드를 수동으로 정의해야 합니다.

도우미 메서드 작동 방식

  1. 50번 반복한다.
  2. 각 반복에서 사전 구성된 lock_timeout를 설정합니다.
  3. 주어진 블록(remove_column)을 실행하려고 시도합니다.
  4. LockWaitTimeout 오류가 발생하면 사전 구성된 sleep_time에 대기한 후 블록을 다시 시도합니다.
  5. 오류가 발생하지 않으면 현재 반복에서 블록이 성공적으로 실행됩니다.

자세한 내용은 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는 이제 AccessExclusiveLockmy_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_keyadd_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이 아닌 열을 추가하고 기본 값을 사용하여 기존 데이터를 채웠다면 해당 기본 값을 애플리케이션 코드가 업데이트된 이후에도 유지해야 하며, 이와 관련된 기본 값을 동일한 마이그레이션에서 제거할 수 없습니다. 마이그레이션이 모델 코드가 업데이트되기 전에 실행되며 모델이 이 열에 대해 알지 못해 설정할 수 없기 때문입니다. 이 경우 다음을 권장합니다.

  1. 표준 마이그레이션에서 기본 값을 가진 열을 추가합니다.
  2. 애플리케이션 다음 시작 후에 기본 값을 제거합니다.

애플리케이션이 재시작된 후에 발생하는 마이그레이션이기 때문에 새로운 열이 발견되었음을 보장합니다.

기본 열 변경

‘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에 필요한 사용 방법의 폐기가 필요합니다.

아래 예는 위의 예와 같지만, 값이 barbaz 열의 곱으로 설정됩니다:

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

시퀀스 삭제

시퀀스 삭제는 흔하지는 않지만, 데이터베이스팀에서 제공하는 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 메소드에서만) 허용됩니다.

테이블 잘라내기

테이블을 잘라내는 것은 흔하지 않지만, 데이터베이스팀에서 제공하는 truncate_tables! 메소드를 사용할 수 있습니다.

내부적으로 다음과 같이 작동합니다.

  • 잘라내려는 테이블의 gitlab_schema를 찾습니다.
  • 테이블의 gitlab_schema가 연결의 gitlab_schemas에 포함되어 있는 경우, 그런 다음 TRUNCATE 문을 실행합니다.
  • 테이블의 gitlab_schema가 연결의 gitlab_schemas에 포함되어 있지 않으면 아무것도 하지 않습니다.

기본 키 교체

기본 키 교체는 파티션 키가 기본 키에 포함되어야 하기 때문에 테이블을 파티션화하는 데 필요합니다.

데이터베이스팀에서 제공하는 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_encryptedencode: falseencode_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_buildsci_job_artifacts에서 vacuum을 확인합니다.

마이그레이션이 충돌하는 잠금이 없는 경우 완전한 테이블 이름을 사용하지 않고도 vacuum 확인을 건너뛸 수 있습니다. 예를 들어 create_async_index_on_job_artifacts와 같은 경우입니다.