날짜 범위 파티셔닝
GitLab v19.4요약
GitLab 마이그레이션 헬퍼가 가장 잘 지원하는 방식은 날짜 범위 파티셔닝이며, 테이블의 각 파티션이 한 달치 데이터를 담습니다. 더 구체적인 예로 audit_events 테이블을 살펴봅니다. 더 자세히 보기 위해 단순화한 audit_events 스키마를 가정합니다.
설명#
GitLab 마이그레이션 헬퍼가 가장 잘 지원하는 방식은 날짜 범위 파티셔닝이며, 테이블의 각 파티션이 한 달치 데이터를 담습니다. 이 경우 파티셔닝 키는 타임스탬프나 날짜 칼럼이어야 합니다. 이 방식의 파티셔닝이 제대로 동작하려면 대부분의 쿼리가 특정 날짜 범위의 데이터에 접근해야 합니다.
더 구체적인 예로 audit_events 테이블을 살펴봅니다.
이 테이블은 애플리케이션 데이터베이스에서 처음으로 파티셔닝된 테이블입니다. 이
테이블은 애플리케이션에서 발생하는 보안 이벤트의 감사 항목을 기록합니다.
거의 모든 경우에 사용자는 특정 기간에 일어난 감사 활동을 보려고
합니다. 그래서 날짜 범위 파티셔닝이 데이터 접근 방식에
자연스럽게 들어맞았습니다.
더 자세히 보기 위해 단순화한 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에는 인덱스가 있으므로 성능 영향은
받아들일 만한 수준일 가능성이 높지만, 더 복잡한 쿼리에서는
오버헤드가 상당할 수 있습니다.
데이터의 접근 패턴이 파티셔닝 전략과 맞을 때만 파티셔닝을 활용해야 하며,
그렇지 않으면 성능이 나빠집니다.
시간 범위 파티셔닝 전략#
GitLab은 시간 범위 파티셔닝에 세 가지 전략을 지원합니다.
- 일별 파티셔닝
- 주별 파티셔닝
- 월별 파티셔닝
시간 범위 파티셔닝 사용#
모델에서 시간 범위 파티셔닝을 사용하려면 PartitionedTable 모듈을 포함하고 파티션 설정을 구성합니다.
class WebHookLog < ApplicationRecord
include PartitionedTable
partitioned_by :created_at, strategy: :monthly, retain_for: 1.month
end
사용 가능한 전략#
일별 전략 (:daily)#
일별 전략은 하루에 파티션 하나를 만듭니다.
partitioned_by :created_at, strategy: :daily, retain_for: 7.days
주별 전략 (:weekly)#
주별 전략은 한 주에 파티션 하나를 만듭니다. 주는 월요일에 시작합니다.
partitioned_by :created_at, strategy: :weekly, retain_for: 4.weeks
월별 전략 (:monthly)#
월별 전략은 한 달에 파티션 하나를 만듭니다.
partitioned_by :created_at, strategy: :monthly, retain_for: 3.months, analyze_interval: 3.days
구성 옵션#
column: 파티셔닝에 사용할 칼럼입니다(필수이며, 타임스탬프나 날짜 칼럼이어야 합니다)strategy::daily,:weekly,:monthly중 하나입니다(필수)retain_for: 파티션 보존 기간으로, 기간 값 또는:ever입니다(필수, 보존 참고)analyze_interval: 새 파티션에 ANALYZE를 실행하는 주기입니다(선택 사항)retain_detached_partitions_for: 분리된 파티션을 삭제하기 전까지 유지하는 기간으로, 기간 값입니다(선택 사항, 분리 후 보존 참고)
세분화된 파티셔닝이 필요한 대용량 테이블에는 :daily를, 중간 수준의 세분화에는 :weekly를, 일별 파티셔닝이 과한 중간 규모 데이터 테이블에는 :monthly를 선택합니다.
보존#
시간 기반 전략은 테이블마다 보존 정책을 의식적으로 결정하도록 retain_for를
요구합니다. 보존 설정이 없으면 테이블은 모든 파티션을 영구히 유지하므로 스토리지
비용이 들고 파티션 키를 제한하지 않는 쿼리가 느려집니다.
오래된 파티션을 자동으로 정리하려면 기간 값을 전달합니다.
partitioned_by :created_at, strategy: :monthly, retain_for: 6.months
모든 파티션을 유지하려면 retain_for: :ever와 그 이유를 설명하는 주석으로
명시적으로 제외합니다.
# Audit events are retained indefinitely for compliance.
partitioned_by :created_at, strategy: :monthly, retain_for: :ever
retain_for는 기간 값이나 :ever 여야 합니다. 이를 생략하거나 nil 같은 다른 값을
전달하면 ArgumentError가 발생합니다.
분리 후 보존#
파티션 매니저가 파티션을 분리한 뒤에는 데이터가 아직 필요할 수 있으므로, 삭제하기 전까지
유예 기간 동안 분리된 테이블을 남겨 둡니다. PartitionManager는 모든 파티셔닝된 테이블에
대해 이 유예 기간의 기본값을 RETAIN_DETACHED_PARTITIONS_FOR(1주)로 둡니다.
특정 테이블에 더 짧거나 긴 유예 기간을 사용하려면 retain_detached_partitions_for를 전달합니다.
partitioned_by :created_at, strategy: :daily, retain_for: 14.days, retain_detached_partitions_for: 2.days
retain_detached_partitions_for는 기간 값이어야 합니다. 설정하지 않으면 테이블은
기본 유예 기간을 사용합니다.
예시#
1단계: 파티셔닝된 복사본 생성 (릴리스 N)#
첫 단계는 원본 테이블의 파티셔닝된 복사본을 만드는 마이그레이션을 추가하는 것입니다. 이 마이그레이션은 원본 테이블의 데이터를 기준으로 적절한 파티션을 만들고, 원본 테이블의 쓰기를 파티셔닝된 복사본으로 동기화하는 트리거를 설치합니다.
audit_events 테이블을 created_at 칼럼으로 파티셔닝하는 마이그레이션
예시는 다음과 같습니다.
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
다음으로 임시 파티셔닝 테이블을 config/initializers/postgres_partitioning.rb에
등록합니다. 이 등록으로 트리거가 데이터를 동기화하는 동안 파티션 매니저가 새
파티션을 만듭니다. 예를 들면 다음과 같습니다.
Gitlab::Database::Partitioning.register_tables(
[
{
limit_connection_names: %i[main],
table_name: 'audit_events_partitioned_table_name',
partitioned_column: :created_at, strategy: :monthly, retain_for: :ever
}
]
)
이 예시에는 다음이 포함됩니다.
table_name: 임시 파티셔닝 테이블의 이름입니다 (예:audit_events_b8088ecbd2).partitioned_column: 파티셔닝에 사용하는 칼럼입니다.strategy::daily,:weekly,:monthly중 하나입니다.retain_for: 이 등록에서는:ever로 설정합니다(다음 경고 참고).
이 등록에는 retain_for: :ever를 설정합니다. 시간 기반 전략은 retain_for를 요구하지만,
데이터를 일정 기간 뒤에 삭제해야 하더라도 여기에 실제 기간 값을 설정해서는
안 됩니다. 백필 중에 retain_for가 기간 값으로 설정되어 있으면 파티션 매니저가
오래된 파티션을 분리할 수 있고, 그러면 분리된 파티션으로 데이터를 복사하려다
백필이 실패합니다.
실제 retain_for는 테이블 교체가 끝난 뒤(4단계)에만 모델에 설정합니다.
테이블 교체가 끝나면(4단계) 이 등록을 제거할 수 있습니다.
마이그레이션이 실행된 뒤에는 원본 테이블의 삽입, 갱신, 삭제가 새 테이블에도 그대로 반영됩니다. 갱신과 삭제는 파티셔닝된 테이블에 해당 행이 있을 때만 효과가 있습니다.
2단계: 파티셔닝된 복사본 백필 (릴리스 N)#
두 번째 단계는 원본 테이블의 기존 데이터를 파티셔닝된 복사본으로 백필하는 백그라운드 job을 예약하는 배포 후 마이그레이션을 추가하는 것입니다.
앞의 예시를 이어 가면 마이그레이션은 다음과 같습니다.
class BackfillPartitionAuditEvents < Gitlab::Database::Migration[2.1]
include Gitlab::Database::PartitioningMigrationHelpers
disable_ddl_transaction!
restrict_gitlab_migration gitlab_schema: :gitlab_main_org
MIGRATION = 'BackfillPartitionedAuditEvents'
def up
enqueue_partitioning_data_migration :audit_events, MIGRATION
end
def down
cleanup_partitioning_data_migration :audit_events, MIGRATION
end
end
배치 백그라운드 마이그레이션은 db/docs/batched_background_migrations/ 아래에서 관리되며
고유한 이름이 필요합니다. lib/gitlab/background_migration/에 BackfillPartitionedTable
하위 클래스를 만들고 마이그레이션에서 참조합니다.
class BackfillPartitionedAuditEvents < BackfillPartitionedTable; end
이 단계는 내부적으로 BATCH_SIZE를 50,000, SUB_BATCH_SIZE를 2,500으로 하여 배치 백그라운드 마이그레이션을 큐에 넣습니다. 자세한 내용은 배치 백그라운드 마이그레이션 가이드를 참고합니다.
3단계: 백필 후 정리 (2단계 이후 필수 정지 다음 릴리스)#
GitLab Self-Managed 인스턴스에서 2단계의 백그라운드 마이그레이션이 성공적으로 완료되도록 2단계와 3단계 사이에는 필수 정지가 있어야 합니다.
이 단계에서는 백그라운드 마이그레이션 이후를 정리하는 배포 후 마이그레이션을 하나 더 추가합니다. 여기에는 남은 job을 강제로 실행하고, 중단되거나 실패한 job 때문에 누락됐을 수 있는 데이터를 복사하는 작업이 포함됩니다.
다시 예시를 이어 가면 이 마이그레이션은 다음과 같습니다.
class CleanupPartitionedAuditEventsBackfill < Gitlab::Database::Migration[2.1]
include Gitlab::Database::PartitioningMigrationHelpers
disable_ddl_transaction!
restrict_gitlab_migration gitlab_schema: :gitlab_main_org
def up
finalize_backfilling_partitioned_table :audit_events
end
def down
# no op
end
end
이 마이그레이션이 끝나면 원본 테이블과 파티셔닝된 테이블에는 동일한 데이터가 있어야 합니다. 원본 테이블에 설치된 트리거가 이후에도 데이터가 계속 동기화되도록 보장합니다.
4단계: 파티셔닝된 테이블과 비파티셔닝된 테이블 교체 (릴리스 N+1)#
이 단계는 비파티셔닝 테이블을 파티셔닝된 복사본으로 대체합니다. 다른 모든 마이그레이션 단계가 성공적으로 끝난 뒤에만 이 단계를 진행합니다.
이 방식에는 다음과 같은 제약이 있으며, 교체 마이그레이션 전이나 도중에 반드시 처리해야 합니다.
- 보조 인덱스와 외래 키는 파티셔닝된 테이블에 자동으로 다시 생성되지 않습니다.
- 인덱스에 의존하는 일부 제약(UNIQUE와 EXCLUDE)은 기반 인덱스가 없기 때문에 파티셔닝된 테이블에 자동으로 다시 생성되지 않습니다.
- 원본 비파티셔닝 테이블을 참조하는 외래 키는 파티셔닝된 테이블을 참조하도록 갱신해야 합니다. 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
이 마이그레이션이 끝나면 다음과 같이 됩니다.
- 파티셔닝된 테이블이 비파티셔닝(원본) 테이블을 대체합니다.
- 앞서 만든 동기화 트리거가 삭제됩니다.
이제 파티셔닝된 테이블을 애플리케이션에서 사용할 수 있습니다.
1단계에서 설명한 대로 임시 파티셔닝 테이블을 config/initializers/postgres_partitioning.rb에
등록했다면, 교체 후에는 임시 테이블이 없으므로 그 등록을 지금 제거합니다.
데이터를 일정 기간만 보존하려면 모델에 retain_for를 추가합니다.
class ProjectDailyStatistic < ApplicationRecord
include PartitionedTable
partitioned_by :date, strategy: :monthly, retain_for: 3.months
end
이렇게 하면 파티션 매니저가 오래된 파티션을 자동으로 삭제합니다.