날짜 범위 분할

설명

GitLab 마이그레이션 도우미가 가장 잘 지원하는 체계는 날짜 범위 분할입니다. 여기서 테이블의 각 분할은 단일 월의 데이터를 포함합니다. 이 경우 분할 키는 타임스탬프 또는 날짜 열이어야 합니다. 이 유형의 분할이 잘 작동하려면 대부분의 쿼리가 특정 날짜 범위의 데이터에 액세스해야 합니다.

구체적인 예를 들어 audit_events 테이블을 사용해 보겠습니다. 이 테이블은 애플리케이션 데이터베이스에서 분할된 최초의 테이블이었으며(GitLab 13.5 릴리스에 함께 배포 예정), 애플리케이션에서 발생하는 보안 이벤트의 감사 항목을 추적합니다. 대부분의 경우 사용자는 특정 시간대에 발생하는 감사 활동을 보고 싶어합니다. 결과적으로 날짜 범위 분할이 데이터에 액세스하는 자연스러운 방식이었습니다.

더 자세히 살펴보기 위해 간소화된 audit_events 스키마를 상상해 보겠습니다:

CREATE TABLE audit_events (
  id SERIAL NOT NULL PRIMARY KEY,
  author_id INT NOT NULL,
  details jsonb NOT NULL,
  created_at timestamptz NOT NULL);

이제 UI에서 일반적인 쿼리가 특정 날짜 범위에 있는 데이터를 표시하는 경우, 예를 들어 단일 주:

SELECT *
FROM audit_events
WHERE created_at >= '2020-01-01 00:00:00'
  AND created_at < '2020-01-08 00:00:00'
ORDER BY created_at DESC
LIMIT 100

테이블이 created_at 열에 대해 분할되면 기본 테이블은 다음과 같이 보일 것입니다:

CREATE TABLE audit_events (
  id SERIAL NOT NULL,
  author_id INT NOT NULL,
  details jsonb NOT NULL,
  created_at timestamptz NOT NULL,
  PRIMARY KEY (id, created_at))
PARTITION BY RANGE(created_at);

참고: 분할된 테이블의 기본 키는 주요 키 정의의 일부로 분할 키를 포함해야 합니다.

또한 테이블의 분할 목록을 갖게 될 수 있습니다:

audit_events_202001 FOR VALUES FROM ('2020-01-01') TO ('2020-02-01')
audit_events_202002 FOR VALUES FROM ('2020-02-01') TO ('2020-03-01')
audit_events_202003 FOR VALUES FROM ('2020-03-01') TO ('2020-04-01')

각 분할은 기본 audit_events 테이블과 동일한 구조를 갖는 별도의 물리적 테이블이지만, 분할 키가 지정된 범위 내에 있는 행 데이터만 포함합니다. 예를 들어, audit_events_202001 분할은 created_at 열 값이 2020-01-01 이상이고 2020-02-01 미만인 행 데이터만 포함합니다.

이제 앞의 예제 쿼리를 다시 살펴보면, 데이터베이스는 WHERE를 사용하여 모든 일치하는 행이 audit_events_202001 분할에 있는 것을 인식할 수 있습니다. 모든 분할의 모든 데이터를 검색하는 대신, 적절한 분할에 있는 한 달치 데이터만 검색할 수 있습니다. 큰 테이블의 경우, 데이터베이스가 액세스해야 하는 데이터 양을 크게 줄일 수 있습니다. 그러나 분할 키를 기반으로 필터링하지 않는 쿼리(예: 다음과 같은 쿼리)를 상상해 보세요.

SELECT *
FROM audit_events
WHERE author_id = 123
ORDER BY created_at DESC
LIMIT 100

이 예에서 데이터베이스는 검색에서 어떠한 분할도 제거할 수 없습니다. 왜냐하면 일치하는 데이터가 모든 분할에 존재할 수도 있기 때문입니다. 결과적으로 각 분할을 개별적으로 쿼리하고 행 데이터를 단일 결과 집합으로 집계해야 합니다. author_id가 인덱싱될 것이므로 성능 영향은 수용할 수 있을 것으로 생각되지만, 더 복잡한 쿼리의 경우 부하가 상당할 수 있습니다. 성능에 부정적인 영향이 있을 수 있으므로 데이터 액세스 패턴이 분할 전략을 지원하는 경우에만 분할을 활용해야 합니다.

예제

단계 1: 분할 복사본 생성(릴리스 N)

첫 번째 단계는 원본 테이블의 분할된 복사본을 만드는 마이그레이션을 추가하는 것입니다. 해당 마이그레이션은 원본 테이블의 데이터를 기반으로 적절한 분할을 만들고, 원본 테이블에서 파티션된 복사본으로의 쓰기를 동기화하는 트리거를 설치합니다.

created_at 열을 기준으로 audit_events 테이블을 분할하는 예시 마이그레이션은 다음과 같습니다:

class PartitionAuditEvents < Gitlab::Database::Migration[2.1]
  include Gitlab::Database::PartitioningMigrationHelpers

  def up
    partition_table_by_date :audit_events, :created_at
  end

  def down
    drop_partitioned_table_for :audit_events
  end
end

이를 실행한 후에, 원본 테이블에 대한 삽입, 업데이트 또는 삭제는 새 테이블에서도 복제됩니다. 업데이트와 삭제의 경우, 해당 행이 분할된 테이블에 존재하는 경우에만 작업이 영향을 미칩니다.

단계 2: 파티션화된 사본(backfill) 채우기 (릴리스 N)

두 번째 단계는 원본 테이블에서 기존 데이터를 파티션화된 사본으로 백필하는 백프라운드 작업을 예약하는 포스트-디플로이먼트 마이그레이션을 추가하는 것입니다.

위의 예시를 계속하면, 마이그레이션은 다음과 같이 보일 것입니다:

class BackfillPartitionAuditEvents < Gitlab::Database::Migration[2.1]
  include Gitlab::Database::PartitioningMigrationHelpers

  disable_ddl_transaction!

  restrict_gitlab_migration gitlab_schema: :gitlab_main

  def up
    enqueue_partitioning_data_migration :audit_events
  end

  def down
    cleanup_partitioning_data_migration :audit_events
  end
end

이 단계는 일괄 백그라운드 마이그레이션을 대기열에 넣습니다. 이 때, BATCH_SIZE 및 SUB_BATCH_SIZE는 각각 50,0002,500으로 내부적으로 설정됩니다. 자세한 내용은 일괄 백그라운드 마이그레이션 가이드를 참조하세요.

단계 3: 백필 후 정리 (릴리스 N+1)

이 단계는 단계 (2)를 포함하는 릴리스 이후에 최소한 한 번 릴리스가 발표되어야 합니다. 이는 백그라운드 마이그레이션이 셀프 매니지드 인스톨레이션에서 제대로 실행되도록 시간을 주기 위함입니다. 이 단계에서는 백그라운드 마이그레이션 이후에 남은 작업들을 실행하도록 강제하고, 삭제되거나 실패한 작업으로 인해 놓칠 수 있는 데이터를 복사하는 포스트-디플로이먼트 마이그레이션을 추가하세요.

경고: 반드시 단계 2와 3 사이에 필요한 정지가 있어야 합니다. 이는 단계 2의 백그라운드 마이그레이션이 성공적으로 완료되도록 합니다.

다시 말씀드리지만, 위의 예시를 계속하면, 이 마이그레이션은 다음과 같이 보일 것입니다:

class CleanupPartitionedAuditEventsBackfill < Gitlab::Database::Migration[2.1]
  include Gitlab::Database::PartitioningMigrationHelpers

  disable_ddl_transaction!

  restrict_gitlab_migration gitlab_schema: :gitlab_main

  def up
    finalize_backfilling_partitioned_table :audit_events
  end

  def down
    # no op
  end
end

이 마이그레이션이 완료되면, 원본 테이블과 파티션화된 테이블은 동일한 데이터를 포함해야 합니다. 원본 테이블에 설치된 트리거는 데이터가 앞으로도 동기화되도록 보장합니다.

단계 4: 파티션화된 테이블과 비파티션화된 테이블 교체 (릴리스 N+1)

이 단계에서는 비파티션화된 테이블을 해당 파티션화된 사본으로 교체합니다. 이는 모든 다른 마이그레이션 단계가 성공적으로 완료된 후에만 사용해야 합니다.

이 방법의 일부 제한 사항은 교체 마이그레이션 전에 또는 중에 반드시 처리해야 합니다:

  • 보조 인덱스와 외래 키는 파티션화된 테이블에 자동으로 재생성되지 않습니다.
  • 일부 유형의 제약 조건(고유 및 제외)은 인덱스에 의존하므로 파티션화된 테이블에 자동으로 재생성되지 않습니다. 왜냐하면 기본 인덱스가 존재하지 않기 때문입니다.
  • 비파티션화된 원본 테이블을 참조하는 외래 키는 파티션화된 테이블을 참조하도록 업데이트되어야 합니다. 이는 PostgreSQL 11에서 지원되지 않습니다.
  • 원본 테이블을 참조하는 뷰는 파티션화된 테이블을 자동으로 참조하도록 업데이트되지 않습니다.
# frozen_string_literal: true

class SwapPartitionedAuditEvents < ActiveRecord::Migration[6.0]
  include Gitlab::Database::PartitioningMigrationHelpers

  def up
    replace_with_partitioned_table :audit_events
  end

  def down
    rollback_replace_with_partitioned_table :audit_events
  end
end

이 마이그레이션이 완료되면:

  • 파티션화된 테이블이 비파티션화된(원본) 테이블을 대체합니다.
  • 이전에 생성된 동기화 트리거는 삭제됩니다.

이제 파티션화된 테이블은 애플리케이션에서 사용할 준비가 되었습니다.