Status | Authors | Coach | DRIs | Owning Stage | Created |
---|---|---|---|---|---|
ongoing |
@engwan
@euko
| devops plan | 2023-12-22 |
노트 테이블 파티셔닝
문제
‘notes’ 테이블은 GitLab에서 가장 큰 PostgreSQL DB 테이블 중 하나로, 2023년 11월 기준으로 .com
프로덕션 데이터베이스의 크기가 1.5TB를 초과하여 GitLab.com의 신뢰성을 점점 해칠 우려가 있으며 대규모 Self-Managed형 인스턴스에도 영향을 미치고 있습니다.
테이블에 대한 파티셔닝 또는 샤딩 방법이 테이블의 크기를 최대 100GB로 제한하기 위해 가능한 빨리 평가되고 실행되어야 합니다.
‘notes’ 테이블 개요
2023년 11월 현재 테이블 구성
테이블의 대다수 레코드가 Merge Request을 위한 것이었습니다.
노트 유형 | 총 레코드의 % | 레코드 수 |
---|---|---|
Merge Request | 67% | 1002272868 |
이슈 | 23% | 348020507 |
커밋 | <~ 5 % | 67790930 |
Epic | <~ 0.05 % | 6196244 |
기타 | <~ 5% | |
총합 | 1488612100 |
note
및 note_html
열은 각각 183GB 및 580GB를 차지하여 테이블 및 지원 인덱스에서 사용되는 저장 공간의 약 77%를 차지했습니다.
note
는 원시 노트 텍스트를 저장하고 note_html
은 원시 노트 텍스트의 HTML 렌더를 캐시합니다.
열 | 크기 (GB) | 총합 % |
---|---|---|
note
| 183 GB | 16% |
note_html
| 580 GB | 51% |
MR 관련 열 | 94 GB | 0.8% |
기타 열 | 383 GB | 24% |
총합 | 1,240 GB | ~100% |
테이블의 인덱스는 나머지 300GB 정도를 차지했습니다.
‘notes’ 테이블의 디자인
notes
에는 다음 세 가지 열을 통해 다형 연관이 있습니다.
-
noteable_type
: 노트가 속하는 유형을 저장합니다. 예:Issue
,MergeRequest
,Commit
. -
noteable_id
: 노트가 속하는 ID를 저장합니다. -
commit_id
: 커밋의 Git SHA를 저장합니다.
노트의 noteable_type
이 Commit
인 경우 noteable_id
는 NULL
이고 commit_id
를 사용하여 커밋을 참조합니다.
관련 모델은 다음과 같습니다: MergeRequest
, Vulnerability
, Epic
, Snippet
, Commit
, DesignManagement::Design
, Issue
, AlertManagement::Alert
및 AbuseReport
노트는 모두 네임스페이스에 속해야 하지만 인스턴스 수준의 학대 노트를 제외하고는 모두 노트가 속하는 것으로 간주됩니다.
lock_manager lwlocks의 경합 감소
테이블의 다형 연관은 파티셔닝에 중요한 영향을 미칩니다.
- 테이블을 대상으로 하는 쿼리에서 최소한의 파티션을 액세스하거나
- lock_manager lwlocks의 경합을 줄이기 위한 방식으로 수행해야 합니다.
예를 들어, notes
테이블의 가장 일반적인 액세스 패턴은 이슈와 같은 항목에 대한 노트를 가져오는 것입니다.
테이블을 id
로 파티셔닝하면 다음 쿼리를 실행할 때 모든 파티션에 액세스할 수 있습니다: SELECT * FROM notes WHERE noteable_type='Issue' AND noteable_id=1;
.
다음과 같이 id
별로 노트를 가져와야 하는 경우를 가정해보죠:
SELECT * FROM todos INNER JOIN notes WHERE notes.id=todos.note_id AND todos.id IN (1, 2, 3);
.
PostgreSQL은 partitioning 전략이 note_id
를 포함하지 않은 파티션을 제거하고 쿼리를 실행하는 데 필요한 최소한의 파티션에만 액세스할 수 있습니다.
notes
테이블의 많은 열이 널을 허용하는데 반해 파티셔닝은 보통 널을 허용하지 않는 파티셔닝 열이 필요합니다.
파티셔닝 방법
여기에서는 notes
테이블을 파티셔닝하고 재구성하는 다양한 옵션을 고려합니다.
1. 도메인 모델별로 테이블 분할
도메인 모델별로 별도의 테이블로 분할하는 것이 Nov 2023 테이블 구성과 일치하며, 사용자 활동이나 할 일을 표시하기 위해 id
로 노트를 가져오는 등 몇 가지 쿼리에 대한 해결책이 될 수 있지만, 결과 테이블은 여전히 100GB를 초과할 것으로 예상됩니다.
예컨대 별도의 issue_notes
, merge_request_notes
및 epic_notes
테이블을 만들 수 있습니다.
장점:
- 도메인 모델과 테이블 구성의 일치
- 기타 파티셔닝 전략을 적용하는 데 방해가 되는 많은 Merge Request 전용 열들이 있으며, 인덱스 팽창과 이상적이지 않은 데이터 정렬에 기여합니다.
-
다형 연관 및 제약 조건 이슈 해결
- 비권장된 다형 연관 및 Git SHA 해시를 저장하는
commit_id
열과 같은 제약 조건이 존재합니다.
- 비권장된 다형 연관 및 Git SHA 해시를 저장하는
단점:
-
코드 변경이 상당히 필요합니다.
-
도메인별로 분할한 후에도 결과 도메인 테이블이 100GB 목표 크기 제한을 초과할 것으로 예상되며 각 테이블을 파티셔닝해야 합니다.
2. namespace_id
를 사용하여 해시 파티셔닝
노트를 가져오는 일반적인 액세스 패턴을 고려할 때 noteable_type
, noteable_id
및 commit_id
를 해시 키 열로 사용하는 것을 고려해볼 수 있습니다. 그러나 노트를 미리 로드하는 데 사용되는 이와 같은 쿼리같은 몇 가지 쿼리들은 id
와 작동합니다. 따라서 namespace_id
가 단일 해시 키로서 더 나은 선택일 수 있습니다.
장점:
- 추가 작업 없이 100GB 목표 크기 제한을 충족할 수 있습니다. 다만 미래에 일부 파티션은 빠르게 성장할 수 있습니다.
단점:
-
해시 파티셔닝된 테이블의 기본 키는 해시 키 열을 포함해야 하지만 여기서 제안된 모든 해시 키는 널일 수 있으므로 주 키의 일부가 될 수 없으며 참조 무결성이 손실될 수 있습니다. 그러나 이 단점은 다형 연관 열로 파티셔닝하는 경우에만 존재합니다.
namespace_id
는 곧 Cells 1.0의notes
테이블의 분산 키가 될 것으로 예상됩니다. -
namespace_id
는notes
테이블을 참조하는 모든 테이블에 대한 샤딩 키가 될 수 있어 Cells 1.0의 작업이 진행됨에 따라 파티셔닝된notes
테이블에 외래 키를 쉽게 추가할 수 있습니다. -
모든 노트 쿼리를
namespace_id
를 포함하도록 업데이트하기 위해 일부 코드 변경이 필요합니다. -
이 방법은 다형 연관이나 너무 많은 Merge Request 전용 열(및 결과)과 같은 기존 구조적 문제들을 해결하지 않습니다.
3. 테이블의 수직 분할
가장 큰 두 열 note
및 note_html
또는 단순히 note_html
을 별도의 테이블로 수직 분할하면 전체 저장 공간 요구 사항을 줄일 수 있습니다.
주의할 점은 노트 테이블이 PostgreSQL의 TOAST 기능에서 크게 이점을 얻지 못한다는 것입니다.
대부분의 노트 텍스트(note
열)가 2kB의 기본 임계값을 초과하지 않아 압축 및 OUT-OF-LINE 저장을 유발하는 것이 거의 없습니다.
수직으로 분할된 열을 위한 테이블은 notes
테이블의 id
열을 사용하여 int range로 파티셔닝할 수 있습니다.
장점:
-
note
및note_html
열의 수직 분할은 테이블 레이아웃을 개선하고 남은notes
테이블의 튜플 크기를 조밀하게 만들어 더 나은 공간 지역성을 확보할 수 있습니다. -
notes
테이블을 그대로 유지하는 점을 좀 더 점진적인 방식으로 여겨볼 수 있습니다.
단점:
-
lock contention problem을 피하기 위해 수직으로 분할된 열을 포함하는 테이블을 미리 로드하는 데는 배치 전략이 구현되어야 합니다. 파티션이 16개 이상 있다면 첫 번째 파티션에는 ID가
<100
인 노트 레코드 데이터가 포함됩니다. 두 번째 파티션에는 ID가>=100
인 노트 레코드 데이터가 포함됩니다. 이와 같은 계속된 쿼리가 너무 많은 파티션에 액세스하는 것을 방지하기 위해 쿼리를 여러 번으로 분할할 수 있습니다.SELECT * FROM p_notes_data WHERE note_id < 100 AND note_id IN (1, 100, 20000, 30000)
와 같이 쿼리합니다. -
notes
에서 사용하는CacheMarkdownField
는 코드베이스의 다른 부분에 암묵적인 의존성이 있습니다. 관련된 메서드를 재정의하거나 관련 메서드를 투명하게 위임하는 시도는 깨끗하게 또는 쉽게 작동하지 않습니다. Markdown 캐싱에 대한 섹션 또한 참조하세요. -
100GB 목표 크기 제한을 충족시키기 위해
notes
테이블에 대해 추가적인 파티셔닝 작업이 필요합니다. -
이 방법은 다형 연관이나 너무 많은 Merge Request 전용 열(및 결과)과 같은 기존 구조적 문제들을 해결하지 않습니다.
DB 기반 Markdown 캐싱에 대한 참고 사항
Merge Request 메모는 일반적으로 메모의 유효성이 점점 떨어지는 감소 패턴을 따릅니다. 이는 해당 Merge Request이 닫혔을 때 메모가 점점 관련성을 잃기 시작합니다. 동일한 감소 패턴은 다른 중요한 유형에도 적용될 수 있으며, 일정 기간 이전의 메모에 대한 캐시된 Markdown을 삭제하는 것은 저장된 데이터를 줄이는 효과적인 방법일 수 있습니다.
캐시된 Markdown이 삭제된 이전 메모를 다시 계산하는 것은 Rails 애플리케이션과 PostgreSQL 호스트에 던지는 영향이 크다는 가능성이 있습니다. 과거에 많은 메모를 렌더링하고 캐시가 업데이트될 때 응용 프로그램과 데이터베이스에 심각한 부담을 주었던 사례가 있었습니다.
만약 캐시된 Markdown 버전이 메모에 대해 심각한 문제가 되지 않는다면, Redis에만 Markdown을 캐싱하고 데이터베이스 캐싱 레이어를 제거하는 것이 탐구할 가치가 있습니다.
4. noteable_type
을 사용하여 디렉터리별로 분할
noteable_type
값에 따라 notes
테이블을 분할하는 것을 고려해 볼 수 있습니다.
noteable_type
에 INVALID
논-널 체크 제약 조건이 있습니다.
noteable_type
이 없는 일부 notes
레코드가 GitLab.com의 프로덕션 데이터베이스에서 발견되어 제거되었습니다.
논-널 체크 제약 조건은 17.0에서 확인될 예정입니다.이점:
-
거의 모든
notes
쿼리에noteable_type
이 포함되어 있어 디렉터리 별로 분할하는 데 이 열을 사용하기에 이상적입니다. -
noteable_type
을 사용하여 디렉터리 별로 분할하면 결과 파티션을 도메인으로 추가로 분할할 수 있습니다.
단점:
-
하위 분할을 위해서는 여전히 루트 테이블의 기본 키에 분할 키가 있어야 합니다.
-
이 접근 방식은 참조하는 테이블에 많은 외래 키를 추가해야 합니다. 분할된 테이블은 복합 기본 키
(noteable_type, id)
를 사용하므로noteable_type
을 모든 참조 테이블에 먼저 추가하고 백필을 해야 합니다. -
분할된 테이블을 사용하는 Active Record 모델이 작동하도록 하려면 더 많은 코드 변경이 필요할 수 있습니다. 또한 복합 기본 키를 사용하는 파티션 테이블 작업에 대한 더 많은 코드 변경이 필요할 수 있습니다.
-
이 방법은 다른 구조적 문제나 Merge Request과 관련된 열이 너무 많은 문제를 다루지 않습니다. 그러나 도메인별로 하위 분할하는 가능성으로 인해 이러한 단점이 완화될 수 있습니다.