마이그레이션 스타일 가이드

GitLab에 대한 마이그레이션을 작성할 때, 이러한 마이그레이션들이 모든 규모의 수백만 기관에서 실행되며, 데이터베이스에 많은 년도의 데이터가있는 경우가 있음을 고려해야합니다.

또한, 작은 변화든 큰 변화든 서버를 오프라인 상태로 가져가는 것은 대부분의 기관에게 큰 부담입니다. 따라서 마이그레이션을 신중히 작성하고 온라인으로 적용할 수 있도록 해야하며 아래 스타일 가이드에 따라야합니다.

마이그레이션은 절대적으로 GitLab 설치가 오프라인 상태가 되어서는 안됩니다. 마이그레이션은 항상 다운타임을 피하기 위한 방법으로 작성되어야합니다. 이전에는 마이그레이션에 DOWNTIME 상수를 설정하여 다운타임을 허용하는 마이그레이션을 정의하는 프로세스가 있었습니다. 오래된 마이그레이션을 살펴보면 이것을 볼 수 있습니다. 이 프로세스는 4년간 사용되지 않은 채 그대로 존재했고, 따라서 항상 다른 방법으로 마이그레이션을 작성하여 다운타임을 피할 수 있음을 배웠습니다.

마이그레이션을 작성할 때, 또한 데이터베이스에는 기간이 경과한 데이터나 불일치가 있을 수 있음을 고려하고 이에 대비해야합니다. 데이터베이스 상태에 대해 최소한의 가정을 하도록 노력하세요.

GitLab 별도의 코드에 의존하지 마십시오. 버전에 따라 변경될 수 있으므로 필요한 경우 마이그레이션에 GitLab 코드를 복사하여 앞으로 호환되도록하세요.

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

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

현재 만들 수있는 세 가지 유형의 마이그레이션이 있으며, 수행해야하는 작업의 유형 및 완료까지 걸리는 시간에 따라 다릅니다.

  1. 일반 스키마 마이그레이션 : 이것은 db/migrate에있는 전통적인 Rails 마이그레이션으로, 새 애플리케이션 코드가 배포되기 에 실행됩니다.
  2. 배포 후 마이그레이션 : 이것은 db/post_migrate에있는 Rails 마이그레이션으로, GitLab.com 배포와 독립적으로 실행됩니다.
  3. 일괄 배경 마이그레이션 : 일반적인 Rails 마이그레이션이 아니지만 Sidekiq 작업을 통해 실행되는 응용 프로그램 코드입니다.

대상 데이터베이스 선택

GitLab은 두 가지 다른 Postgres 데이터베이스에 연결됩니다 : mainci. 이 분할로 마이그레이션에 영향을 미칠 수 있습니다.

이것이 어떻게 마이그레이션에 영향을 미치는지 이해하려면 여러 데이터베이스에 대한 마이그레이션을 읽으십시오.

일반 스키마 마이그레이션 만들기

마이그레이션을 만들려면 다음과 같은 Rails 생성기를 사용할 수 있습니다:

bundle exec rails g migration 마이그레이션_이름_여기에

이렇게하면 db/migrate에 마이그레이션 파일이 생성됩니다.

새 모델을 추가하는 일반 스키마 마이그레이션

새 모델을 만들려면 다음과 같은 Rails 생성기를 사용할 수 있습니다:

bundle exec rails g model 모델_이름_여기에

이로써 다음이 생성됩니다:

  • db/migrate에 마이그레이션 파일
  • app/models에 모델 파일
  • spec/models에 spec 파일

스키마 변경

스키마 변경 사항은 db/structure.sql에 커밋해야합니다. 이 파일은 일반적으로 bundle exec rails db:migrate를 실행할 때 Rails에서 자동으로 생성되므로 일반적으로이 파일을 직접 편집해서는 안됩니다.

로컬 데이터베이스가 main에서 가져오는 스키마와 다르다면 정확한 커밋이 어려울 수 있습니다. 이 경우 scripts/regenerate-schema 스크립트를 사용하여 깨끗한 db/structure.sql을 다시 생성할 수 있습니다.

scripts/regenerate-schema 스크립트를 사용하면 추가적인 차이점이 있을 수 있습니다. 이런 경우 매뉴얼 절차를 사용합니다.

다운타임 피하기

문서 “데이터 마이그레이션에서 다운타임 피하기”에서는 다음과 같은 다양한 데이터베이스 작업을 명시합니다.

및 다운타임 없이 이러한 작업을 수행하는 방법을 설명합니다.

복원 가능성

귀하의 마이그레이션이 반드시 복원 가능해야 합니다. 취약점 또는 버그 발생 시 다운그레이드할 수 있어야 하기 때문에 이는 매우 중요합니다.

참고: GitLab 프로덕션 환경에서 문제가 발생하면 db:rollback을 사용하여 마이그레이션을 롤백하는 대신 롤-포워드 전략을 사용합니다. 자체 호스팅된 인스턴스에서는 업그레이드 프로세스 시작 전에 만들어진 백업을 복원하도록 사용자에게 권장합니다. down 메소드는 기본적으로 개발 환경에서 사용됩니다. 예를 들어, 개발자가 커밋 또는 브랜치 간에 전환할 때 로컬 structure.sql 파일과 데이터베이스가 일관된 상태인지 확인하고 싶을 때 사용됩니다.

귀하의 마이그레이션 중에는 마이그레이션의 복원 가능성이 테스트된 방법을 설명하는 주석을 추가하세요.

일부 마이그레이션은 되돌릴 수 없는 경우가 있습니다. 예를 들어, 데이터 마이그레이션의 경우 일부 데이터 마이그레이션은 데이터베이스의 상태에 대한 정보를 잃기 때문에 되돌릴 수 없을 수 있습니다. 그럼에도 불구하고 up 메소드에 의해 수행된 변경 사항을 되돌릴 수 없는 이유를 설명하는 주석과 함께 down 메소드를 생성하여 마이그레이션 자체는 되돌릴 수 있도록 만들어야 합니다.

def down
  # 아무 작업도 하지 않음
  
  # `up`에 의해 수행된 변경 사항을 되돌릴 수 없는 이유를 설명하는 주석
end

이러한 마이그레이션은 본질적으로 위험하며 data migrations 추가 준비가 필요합니다.

원자성과 트랜잭션

기본적으로 마이그레이션은 단일 트랜잭션입니다. 마이그레이션이 시작되면 트랜잭션이 열리고 모든 단계가 처리된 후에 커밋됩니다.

단일 트랜잭션으로 마이그레이션을 실행하면 하나의 단계라도 실패하면 다른 단계가 실행되지 않으므로 데이터베이스가 유효한 상태로 유지됩니다. 따라서 다음을 따라야 합니다.

  • 모든 마이그레이션을 단일 트랜잭션 마이그레이션에 넣거나
  • 필요한 경우 대부분의 작업을 한 마이그레이션에 넣고 단일 트랜잭션에서 수행할 수 없는 단계를 위해 별도의 마이그레이션을 생성합니다.

예를 들어, 비어 있는 테이블을 생성하고 해당 테이블에 대해 인덱스를 빌드해야 하는 경우 일반 단일 트랜잭션 마이그레이션을 사용하고 기본적인 rails 스키마 문에 있는 add_index를 사용해야 합니다. 이 작업은 블로킹 작업이지만 테이블이 아직 사용되지 않으므로 아직 레코드가 없으므로 문제가 되지 않습니다.

참고: 일반적으로 disable_ddl_transaction!을 사용하여 단일 트랜잭션으로 마이그레이션하는 데 시간이 오래 걸릴 때, 여러 옵션이 있습니다. 어떤 경우에도 마이그레이션이 얼마나 오래 걸리는지에 따라 적절한 마이그레이션 유형을 선택해야 합니다.

명명 규칙

데이터베이스 객체(테이블, 인덱스, 뷰 등)의 이름은 소문자여아 합니다. 소문자 이름을 사용하면 따옴표를 사용하지 않은 이름으로 쿼리를 작성해도 오류가 발생하지 않습니다.

열 이름은 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입니다.

Best practice

위 내용은 엄격한 규칙으로 고려해야 하지만, 마이그레이션이 업스트림에 Merge될 예정인 날짜로부터 경과된 시간에 상관없이 마이그레이션 타임스탬프를 3주 이내로 유지하는 것이 최선의 방법입니다.

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

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

    rake db:migrate:down:main VERSION=<timestamp>
    rake db:migrate:down:ci VERSION=<timestamp>
    
  2. 마이그레이션 파일을 삭제합니다.
  3. 마이그레이션 스타일 가이드를 따라 마이그레이션을 다시 생성합니다.

마이그레이션 도우미 및 버전 관리

데이터베이스 마이그레이션에 대한 많은 일반적인 패턴에 대한 다양한 도우미 메서드가 있습니다. 이러한 도우미 메서드는 Gitlab::Database::MigrationHelpers 및 관련 모듈에서 찾을 수 있습니다.

도우미의 동작을 시간이 지남에 따라 변경할 수 있도록 하기 위해 마이그레이션 도우미에 대한 버전 관리 체계를 구현합니다. 이를 통해 기존 마이그레이션에 대한 도우미의 동작을 유지하면서 새로운 마이그레이션에 대한 동작을 변경할 수 있습니다.

이를 위해 모든 데이터베이스 마이그레이션은 Gitlab::Database::Migration에서 상속되어야 합니다. 즉, “버전화된” 클래스입니다. 새로운 마이그레이션의 경우 최신 버전을 사용해야 합니다 (이는 Gitlab::Database::Migration::MIGRATION_CLASSES에서 조회할 수 있습니다) - 최신 마이그레이션 도우미를 사용하려면.

다음 예에서는 마이그레이션 클래스의 버전 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 테이블에 배타적 잠금이 필요합니다. 테이블이 동시에 액세스되고 다른 프로세스에 의해 수정될 경우, 잠금을 확보하는 데 시간이 걸릴 수 있습니다. 잠금 요청이 대기열에 있고 대기열에 들어간 후에는 users 테이블에서 다른 쿼리도 차단될 수 있습니다.

PostgreSQL 잠금에 대한 DETAILS: 명시적 잠금

안정성을 위해 GitLab.com에는 짧은 statement_timeout이 설정되어 있습니다. 마이그레이션이 호출될 때, 모든 데이터베이스 쿼리가 실행하는 데 고정된 시간이 주어집니다. 최악의 경우, 요청이 잠금 대기열에 있어서 구성된 문장 시간 제한이 지난 후까지 다른 쿼리를 차단하다가 canceling statement due to statement timeout 오류가 발생할 수 있습니다.

이 문제는 실패한 응용 프로그램 업그레이드 프로세스와 심지어 응용 프로그램 안정성 문제를 일으킬 수 있습니다. 테이블이 잠시 동안 접근할 수 없을 수 있기 때문입니다.

데이터베이스 마이그레이션의 신뢰성과 안정성을 높이기 위해 GitLab 코드베이스에는 필요한 잠금을 확보하는 작업을 여러 번 다시 시도하고 시도 사이의 대기 시간을 설정하는 방법을 제공합니다. 필요한 잠금 확보를 위한 여러 번의 짧은 시도는 데이터베이스가 다른 문을 처리할 수 있게 합니다.

락 재시도는 두 가지 다른 도우미에 의해 제어됩니다:

  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

열의 기본값을 변경

열의 기본값을 변경하는 것은 멀티 릴리스 프로세스를 따르지 않으면 응용 프로그램 다운타임을 유발할 수 있습니다. 자세한 내용은 avoiding downtime in migrations for changing column defaults를 참조하십시오.

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

두 개의 외래 키가 있는 새로운 테이블 생성

한 번에 한 개의 외래 키만 생성해야 합니다. 외래 키 제약 조건의 추가는 동시에 여러 테이블을 잠그는 것을 피해야 하기 때문입니다.

이를 위해 세 개의 마이그레이션이 필요합니다:

  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 offense를 일으킵니다.

disable_ddl_transaction!

def up
  with_lock_retries do
    add_concurrent_index :users, :name
  end
end

헬퍼 메서드 사용 시기

with_lock_retries 헬퍼 메서드는 이미 열린 트랜잭션 안에서의 실행이 아닐 때에만 사용할 수 있습니다(PostgreSQL 서브트랜잭션 사용은 권장되지 않음). 표준 Rails 마이그레이션 헬퍼 메서드와 함께 사용할 수 있습니다. 하나의 테이블에서 여러 마이그레이션 헬퍼를 호출하는 것은 문제가 되지 않습니다.

고트랜잭션의 경우 with_lock_retries 헬퍼 메서드를 사용하는 것이 좋습니다. (https://docs.gitlab.com/ee/development/migration_high_traffic_tables.html)에 있는 하이트래픽 테이블 중 하나가 데이터베이스 마이그레이션에 관련된 경우 이 헬퍼 메서드를 사용하는 것이 좋습니다.

예시 변경 사항:

  • 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 모듈에서 구현되었습니다.

최악의 경우 이 메서드는 다음과 같이 동작합니다:

  • 최대 40분 동안 블록을 최대 50번 실행합니다.
    • 대부분의 시간은 각 반복 후에 미리 구성된 대기 기간에서 보냅니다.
  • 50번째 재시도 후, 블록은 lock_timeout없이 표준 마이그레이션 호출과 마찬가지로 실행됩니다.
  • 잠금을 얻을 수 없으면 마이그레이션은 statement timeout 오류로 실패합니다.

매우 긴 시간 동안 실행되는 트랜잭션이 users 테이블에 액세스하는 경우 마이그레이션이 실패할 수 있습니다.

SQL 수준에서의 잠금 재시도 방법론

이 섹션에서는 lock_timeout 사용을 보여주는 단순화된 SQL 예제를 제공합니다. 제시된 스니펫을 여러 개의 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을 여전히 실행 중이기 때문입니다.

더 악몽같은 영향은 일반적으로 트랜잭션 1과 충돌하지 않을 수도 있는 트랜잭션들이 트랜잭션 2가 AccessExclusiveLock를 기다리며 대기하기 때문에 이러한 트랜잭션들이 블로킹될 수 있다는 점입니다. 일반적인 상황에서는 다른 트랜잭션이 my_notes 테이블을 동시에 읽고 쓰려고 시도하더라도 RowExclusiveLock을 소유한 트랜잭션 1과 충돌하지 않기 때문에 지나갈 수 있습니다. 그러나 AccessExclusiveLock 획득 요청을 대기하고 있는 경우 충돌하는 잠금 요청에 대한 후속 요청은 트랜잭션 1과 동시에 실행될 수는 있지만 블로킹되어 있을 수 있습니다.

with_lock_retries를 사용하면 트랜잭션 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! 메서드를 호출하여 단일 트랜잭션 모드를 비활성화해야 합니다.

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

인덱스가 사용되지 않는지 확인하려면 Thanos를 사용할 수 있습니다:

sum by (type)(rate(pg_stat_user_indexes_idx_scan{env="gprd", indexrelname="여기에 인덱스 이름 입력"}[30d]))

인덱스가 있는지 미리 확인할 필요는 없지만 제거되는 인덱스의 이름을 명시하는 것이 필요합니다. 이는 적절한 형식의 remove_index 또는 remove_concurrent_index 메서드에 이름을 옵션으로 전달하거나 remove_concurrent_index_by_name 메서드를 사용하여 수행할 수 있습니다. 이름을 명시적으로 지정하는 것은 올바른 인덱스가 제거되도록 하기 위해 중요합니다.

인덱스 비활성화

인덱스를 비활성화하는 것은 안전한 작업이 아닙니다.

인덱스 추가

인덱스를 추가하기 전에 그것이 필요한지를 고려해보세요. 데이터베이스 인덱스 추가 가이드에는 인덱스가 필요한지 결정하는 데 도움이 되는 자세한 내용과 인덱스 추가를 위한 모범 관행가 포함되어 있습니다.

인덱스의 존재 여부 테스트

마이그레이션이 인덱스의 부재 또는 존재에 따라 조건 로직을 필요로 하는 경우, 해당 인덱스의 존재 여부를 이름을 사용하여 테스트해야 합니다. 이렇게 하면 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를 사용해야 합니다.

새로운 테이블이거나 비어있는 테이블이 high-traffic tables을 참조하지 않는 경우에는 단일 트랜잭션 마이그레이션에서 disable_ddl_transaction!을 필요로 하지 않는 다른 작업과 결합하여 add_reference를 사용할 것을 권장합니다.

기존 열에 외래 키 제약 조건 추가에 대해 더 알아보세요.

NOT NULL 제약 조건

NOT NULL 제약 조건에 대한 스타일 가이드에서 더 많은 정보를 확인하세요. NOT NULL 제약 조건

기본값이 있는 열 추가

GitLab의 최소 버전이 PostgreSQL 11인 것을 고려하면 기본값이 있는 열을 추가하는 것이 매우 쉬워졌으며 모든 경우에 표준 add_column 도우미를 사용해야 합니다.

PostgreSQL 11 이전에는 기본값이 있는 열을 추가하는 것이 문제가 있었습니다. 왜냐하면 이것은 전체 테이블 재작성을 초래했기 때문입니다.

비-nullable 열의 기본값 제거

비-nullable 열을 추가하고 기본값을 사용하여 기존 데이터를 채웠다면, 응용 프로그램 코드가 업데이트된 후에도 해당 기본값을 유지해야 합니다. 마이그레이션이 실행되는 시간에 모델 코드가 업데이트되기 전에는 기본값을 제거할 수 없습니다. 이 경우 다음을 권장합니다.

  1. 표준 마이그레이션에서 기본값이 있는 열을 추가합니다.
  2. 응용 프로그램 다시 배포 마이그레이션에서 기본값을 제거합니다.

응용 프로그램이 다시 시작된 후에 포스트-디플로이맨트 마이그레이션이 실행되므로 새 열이 발견된 것이 확실합니다.

열의 기본값 변경

change_column_default를 사용하여 기존 열의 기본값을 특정 값으로 변경할 수 있습니다. 이 작업이 대규모 테이블에 대한 비용이 많이 드는 작업으로 보일 수 있지만 실제로는 그렇지 않습니다.

다음은 namespaces 중 하나의 가장 큰 테이블 중 하나의 기본 열 값을 변경하는 마이그레이션의 예시입니다. 이것은 다음과 같이 해석될 수 있습니다.

ALTER TABLE namespaces
ALTER COLUMN request_access_enabled
SET DEFAULT false

이 특정 경우에는 기본값이 존재하며, 우리는 단지 namespaces 테이블의 기존 레코드에 대한 메타데이터를 변경하고 있는 것이기 때문에, namespaces 테이블의 모든 기존 레코드를 다시 쓰는 것을 의미하지 않습니다. 새 기본값이 있는 새 열을 만드는 경우에만 모든 레코드가 다시 작성됩니다.

그때문에 앞에서 언급한 이유로 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의 경우 큰 테이블에서 실행해도 괜찮을 수 있지만, 테이블의 작은 일부분만을 업데이트해야 하는 경우에만 그렇습니다. 하지만 깃랩닷컴 스테이징 환경에서 유효성을 검사하지 않고 - 또는 앞으로 그것을 대신할 누군가에게 물어보고 - 무시해서는 안 되는 것을 무시하지 마세요.

외래 키 제약 조건 제거

외래 키 제약 조건을 제거할 때, 해당 외래 키와 관련된 두 테이블에 대해 잠금을 획들해야 합니다. 쓰기 패턴이 많은 테이블의 경우 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

데이터베이스 테이블 삭제

note
테이블을 삭제한 후에는 데이터베이스 사전에 해당 테이블을 추가해야 합니다.

데이터베이스 테이블을 삭제하는 것은 일반적이지 않으며, Rails에서 제공하는 drop_table 메서드는 일반적으로 안전한 것으로 간주됩니다. 테이블을 삭제하기 전에 다음을 고려해보세요.

테이블이 high-traffic table에 있는 외래 키를 가지고 있는 경우, DROP TABLE 문은 동시 트래픽을 멈출 가능성이 있어서 시간이 지났음에도 문장 제한 시간 오류로 실패할 수 있습니다.

테이블에 레코드가 없고, 외래 키가 없는 경우:

  • 마이그레이션에서 drop_table 메서드를 사용하세요.
def change
  drop_table :my_table
end

테이블에 레코드가 있지만, 외래 키가 없는 경우:

  • 테이블과 관련된 응용 프로그램 코드(모델, 컨트롤러, 서비스 등)를 제거하세요.
  • 포스트-디플로이맨트 마이그레이션에서 drop_table을 사용하세요.

이 모든 것이 응용 프로그램 코드가 사용되지 않는다고 확신한다면 단일 마이그레이션에 모두 포함될 수 있습니다. 약간의 위험을 줄이고 싶다면, 응용 프로그램 변경 사항이 Merge된 후 두 번째 Merge Request에 마이그레이션을 넣는 것을 고려해보세요. 이 접근 방식은 롤백할 수 있는 기회를 제공합니다.

def up
  drop_table :my_table
end

def down
  # create_table ...
end

테이블에 외래 키가 있는 경우:

  • 테이블과 관련된 응용 프로그램 코드(모델, 컨트롤러, 서비스 등)를 제거하세요.
  • 포스트-디플로이맨트 마이그레이션에서 with_lock_retries 헬퍼 메서드를 사용하여 외래 키를 제거하세요. 또 다른 후속 포스트-디플로이맨트 마이그레이션에서 drop_table을 사용하세요.

이 모든 것이 응용 프로그램 코드가 사용되지 않는다고 확신한다면 단일 마이그레이션에 모두 포함될 수 있습니다. 약간의 위험을 줄이고 싶다면, 응용 프로그램 변경 사항이 Merge된 후 두 번째 Merge Request에 마이그레이션을 넣는 것을 고려해보세요. 이 접근 방식은 롤백할 수 있는 기회를 제공합니다.

projects 테이블에서 외래 키를 제거하고 트랜잭션을 사용하지 않는 마이그레이션을 사용하여 테이블 삭제:

# 첫번째 마이그레이션 파일
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

테이블 삭제:

# 두번째 마이그레이션 파일
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
note
add_sequence는 외래 키가 있는 열에 대해 피해야 합니다. 이러한 열에 대한 시퀀스 추가는 이전 스키마 상태를 복원하는 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
note
새 인덱스를 미리 소개하여 기본 키를 교체하려면 별도의 마이그레이션에서 미리 소개해야 합니다.

정수 열 유형

기본적으로 정수 열은 4바이트(32비트) 숫자까지 저장할 수 있습니다. 이는 최대 값이 2,147,483,647임을 의미합니다. 바이트 단위로 파일 크기를 저장하는 열을 만들 때 이를 고려해야 합니다. 파일 크기를 바이트 단위로 추적하는 경우, 최대 파일 크기는 2GB를 약간 넘지 못하게 됩니다.

정수 열이 8바이트(64비트) 숫자까지 저장할 수 있도록 허용하려면 명시적으로 8바이트로 제한을 설정해야 합니다. 이렇게 하면 열이 9,223,372,036,854,775,807까지의 값에 대해 저장할 수 있게 됩니다.

Rails 마이그레이션 예시:

add_column(:projects, :foo, :integer, default: 10, limit: 8)

문자열과 텍스트 데이터 유형

더 많은 정보를 위해 text data type 스타일 가이드를 참조하세요.

타임스탬프 열 유형

기본적으로 Rails는 타임스탬프 데이터를 시간대 정보 없이 저장하는 timestamp 데이터 유형을 사용합니다. timestamp 데이터 유형은 add_timestamps 또는 timestamps 메서드를 호출하여 사용됩니다.

또한, Rails는 :datetime 데이터 유형을 timestamp로 변환합니다.

예시:

# timestamps
create_table :users do |t|
  t.timestamps
end

# add_timestamps
def up
  add_timestamps :users
end

# :datetime
def up
  add_column :users, :last_sign_in, :datetime
end

이러한 메서드 대신, 타임스탬프를 시간대와 함께 저장하려면 다음 메서드를 사용해야 합니다:

  • add_timestamps_with_timezone
  • timestamps_with_timezone
  • datetime_with_timezone

이를 통해 모든 타임스탬프에 시간대가 지정되도록 합니다. 결과적으로 기존의 타임스탬프가 시스템의 시간대 변경 시 갑자기 다른 시간대를 사용하지 않게 됩니다. 또한, 최초에 사용된 시간대가 명확해집니다.

데이터베이스에 JSON 저장

Rails 5에서는 JSONB(바이너리 JSON) 열 유형을 네이티브로 지원합니다. 이 열을 추가하는 예시 마이그레이션:

class AddOptionsToBuildMetadata < Gitlab::Database::Migration[2.1]
  def change
    add_column :ci_builds_metadata, :config_options, :jsonb
  end
end

기본적으로 해시 키는 문자열입니다. 선택적으로 다른 키에 대한 다른 액세스를 제공하기 위해 사용자 정의 데이터 유형을 추가할 수 있습니다.

class BuildMetadata
  attribute :config_options, :ind_jsonb # indifferent access를 위한 것이거나, 키로써 오직 심볼이 필요한 경우 :sym_jsonb를 사용하세요.
end

JSONB 열을 사용할 때는 시간이 지남에 따라 삽입되는 데이터를 제어하기 위해 JsonSchemaValidator를 사용하세요.

class BuildMetadata
  validates :config_options, json_schema: { filename: 'build_metadata_config_option' }
end

추가로, JSONB 열의 키를 ActiveRecord 속성으로 노출시킬 수 있습니다. 이것은 복잡한 유효성 검사나 ActiveRecord 변경을 추적할 때 사용합니다. 이 기능은 jsonb_accessor 젬에서 제공되며 JsonSchemaValidator를 대체하지 않습니다.

module Organizations
  class OrganizationSetting < ApplicationRecord
    belongs_to :organization
    
    validates :settings, json_schema: { filename: "organization_settings" }
    
    jsonb_accessor :settings,
      restricted_visibility_levels: [:integer, { array: true }]
    
    validates_each :restricted_visibility_levels do |record, attr, value|
      value&.each do |level|
        unless Gitlab::VisibilityLevel.options.value?(level)
          record.errors.add(attr, format(_("'%{level}' is not a valid visibility level"), level: level))
        end
      end
    end
  end
end

이제 restricted_visibility_levels을 ActiveRecord 속성으로 사용할 수 있습니다:

> s = Organizations::OrganizationSetting.find(1)
=> #<Organizations::OrganizationSetting:0x0000000148d67628>
> s.settings
=> {"restricted_visibility_levels"=>[20]}
> s.restricted_visibility_levels
=> [20]
> s.restricted_visibility_levels = [0]
=> [0]
> s.changes
=> {"settings"=>[{"restricted_visibility_levels"=>[20]}, {"restricted_visibility_levels"=>[0]}], "restricted_visibility_levels"=>[[20], [0]]}

암호화된 속성

데이터베이스에서 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

테스팅

테스팅 Rails 마이그레이션 스타일 가이드를 참조하세요.

데이터 마이그레이션

일반 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.

다음 예를 보고 새 도우미를 사용하는 방법을 확인하세요.

마이그레이션에서 애플리케이션 코드 사용 (권장하지 않음)

마이그레이션에서 일반적으로 애플리케이션 코드(모델 포함)를 사용하는 것은 권장되지 않습니다. 그 이유는 마이그레이션이 오랫동안 유지되고 의존하는 애플리케이션 코드가 변경되어 마이그레이션이 미래에 손상될 수 있기 때문입니다. 과거에는 몇몇 백그라운드 마이그레이션이 수십 줄에 걸쳐 있는 코드를 복사하지 않고 쓰도록 애플리케이션 코드를 사용해야 했습니다. 이러한 드문 경우에는 누구나 코드를 재구성해도 마이그레이션이 손상되지 않았는지 확인할 수 있는 훌륭한 테스트가 필수적입니다. 또한 배치 백그라운드 마이그레이션에 대해 권장하지 않음으로 애플리케이션 코드를 사용하는 것도 권장되지 않습니다. 모델은 마이그레이션에서 선언되어야 합니다.

일반적으로 마이그레이션에서 모델(특히 정의된 모델)을 사용하는 것을 피할 수 있습니다. 이는 MigrationRecord를 상속하는 클래스를 정의함으로써 가능합니다(아래 예제 참조).

모델을 사용하는 경우(마이그레이션에 정의된 모델 포함) 먼저 열 캐시를 지우십시오 (reset_column_information를 사용하여).

단일 테이블 상속(STI)을 활용하는 모델을 사용하는 경우 특별 고려 사항이 있습니다.

이전 마이그레이션에서 테이블을 수정하는 경우에도 당연히 스키마 캐시를 지워야 합니다(User.reset_column_information).

예: my_column 열을 users 테이블에 추가

User.reset_column_information 명령을 생략하지 않도록 중요합니다. 이렇게 하면 구식 스키마가 캐시에서 삭제되고 Active Record가 업데이트된 스키마 정보를 로드합니다.

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을 포함한 것에 유의하세요.

== 20200705232821 AddAndSeedMyColumn: 마이그레이션 진행 중 ==============================
D, [2020-07-06T00:37:12.483876 #130101] DEBUG -- :    (0.2ms)  BEGIN
D, [2020-07-06T00:37:12.521660 #130101] DEBUG -- :    (0.4ms)  SELECT COUNT(*) FROM "user"
-- add_column(:users, :my_column, :integer, {:default=>1})
D, [2020-07-06T00:37:12.523309 #130101] DEBUG -- :    (0.8ms)  ALTER TABLE "users" ADD "my_column" integer DEFAULT 1
   -> 0.0016s
D, [2020-07-06T00:37:12.650641 #130101] DEBUG -- :   AddAndSeedMyColumn::User Load (0.7ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1000]]
D, [2020-07-18T00:41:26.851769 #459802] DEBUG -- :   AddAndSeedMyColumn::User Update (1.1ms)  UPDATE "users" SET "my_column" = $1, "updated_at" = $2 WHERE "users"."id" = $3  [["my_column", 42], ["updated_at", "2020-07-17 23:41:26.849044"], ["id", 1]]
D, [2020-07-06T00:37:12.653648 #130101] DEBUG -- :   ↳ config/initializers/config_initializers_active_record_locking.rb:13:in `_update_row'
== 20200705232821 AddAndSeedMyColumn: 마이그레이션이 완료되었습니다 (0.1706s) =====================

스키마 캐시를 지우지 않으면(User.reset_column_information) 열이 Active Record에서 사용되지 않고 의도한 변경 사항이 이루어지지 않아 다음과 같은 결과를 초래합니다. 쿼리에서 my_column이 누락된 것을 주목하세요.

== 20200705232821 AddAndSeedMyColumn: 마이그레이션 진행 중 ==============================
D, [2020-07-06T00:37:12.483876 #130101] DEBUG -- :    (0.2ms)  BEGIN
D, [2020-07-06T00:37:12.521660 #130101] DEBUG -- :    (0.4ms)  SELECT COUNT(*) FROM "user"
-- add_column(:users, :my_column, :integer, {:default=>1})
D, [2020-07-06T00:37:12.523309 #130101] DEBUG -- :    (0.8ms)  ALTER TABLE "users" ADD "my_column" integer DEFAULT 1
   -> 0.0016s
D, [2020-07-06T00:37:12.650641 #130101] DEBUG -- :   AddAndSeedMyColumn::User Load (0.7ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1  [["LIMIT", 1000]]
D, [2020-07-06T00:37:12.653459 #130101] DEBUG -- :   AddAndSeedMyColumn::User Update (0.5ms)  UPDATE "users" SET "updated_at" = $1 WHERE "users"."id" = $2  [["updated_at", "2020-07-05 23:37:12.652297"], ["id", 1]]
D, [2020-07-06T00:37:12.653648 #130101] DEBUG -- :   ↳ config/initializers/config_initializers_active_record_locking.rb:13:in `_update_row'
== 20200705232821 AddAndSeedMyColumn: 마이그레이션이 완료되었습니다 (0.1706s) =====================

고트래픽 테이블

현재 고트래픽 테이블 디렉터리은 다음과 같습니다.

고트래픽 테이블을 식별하는 것은 어려울 수 있습니다. Self-Managed형 인스턴스는 GitLab.com과 다른 사용 패턴의 GitLab 기능을 사용할 수 있으므로, GitLab.com을 기반으로 가정을 하더라도 충분하지 않을 수 있습니다.

GitLab.com의 고트래픽 테이블을 식별하기 위해 다음과 같은 기준이 고려됩니다. 여기에 연결된 메트릭은 GitLab 내부 전용입니다.

  • 읽기 작업
  • 레코드 수(https://thanos.gitlab.net/graph?g0.range_input=2h&g0.max_source_resolution=0s&g0.expr=topk(500%2C%20max%20by%20(relname)%20(pg_stat_user_tables_n_live_tup%7Benvironment%3D%22gprd%22%7D))&g0.tab=1)
  • 크기가 10GB 이상인 경우

현재 고트래픽 테이블과 비교하여 읽기 작업이 많은 테이블은 좋은 후보가 될 수 있습니다.

일반적인 규칙으로, GitLab.com의 분석이나 보고를 위한 목적으로 고트래픽 테이블에 열을 추가하는 것은 권장하지 않습니다. 이로 인해 Self-Managed형 인스턴스의 성능에 부정적인 영향을 미칠 수 있으며 직접적인 기능 가치를 제공하지 않습니다.

마일스톤

GitLab 16.6부터 모든 새로운 마이그레이션은 다음 구문을 사용하여 마일스톤을 지정해야 합니다.

class AddFooToBar < Gitlab::Database::Migration[2.2]
  milestone '16.6'
  
  def change
    # 여기에 마이그레이션 작성
  end
end

마이그레이션에 올바른 마일스톤을 추가하면 GitLab에서 마이그레이션을 해당하는 GitLab 마이너 버전으로 논리적으로 분할할 수 있습니다. 이로써 다음과 같은 이점이 있습니다.

  • 업그레이드 프로세스를 단순화합니다.
  • 마이그레이션의 타임스탬프만을 의존하는 경우 발생할 수 있는 잠재적인 마이그레이션 순서 문제를 완화합니다.

Autovacuum wraparound 보호

이것은 PostgreSQL의 특별한 autovacuum 실행 모드로, 백그라운드에서 작동하며 vacuum 중인 테이블에 대해 ShareUpdateExclusiveLock이 필요합니다. 크기가 큰 테이블의 경우 몇 시간이 걸릴 수 있으며, 락이 동시에 테이블을 수정하려는 대부분의 DDL 마이그레이션과 충돌할 수 있습니다. 마이그레이션이 충돌하는 락을 가지고 있지 않다면, 완전한 테이블 이름을 사용하지 않고, 예를 들어 create_async_index_on_job_artifacts의 경우 vacuum 체크를 건너뛸 수 있습니다.