This page contains information related to upcoming products, features, and functionality. It is important to note that the information presented is for informational purposes only. Please do not rely on this information for purchasing or planning purposes. As with all projects, the items mentioned on this page are subject to change or delay. The development, release, and timing of any products, features, or functionality remain at the sole discretion of GitLab Inc.
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월까지의 테이블 구성

테이블의 대부분 레코드는 병합 요청을 위한 것이었습니다.

Noteable type 전체 레코드 대비 % 레코드 수
병합 요청 67% 1002272868
이슈 23% 348020507
커밋 <~ 5 % 67790930
Epic <~ 0.05 % 6196244
기타 <~ 5%  
전체   1488612100

notenote_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: noteable의 유형을 저장합니다. 예: 이슈, 병합 요청, 커밋.
  • noteable_id: noteable의 ID를 저장합니다.
  • commit_id: 커밋의 Git SHA를 저장합니다.

노트의 noteable_type커밋인 경우 noteable_idNULL이고 commit_id는 커밋을 참조하는 데 사용됩니다.

관련된 모델은 다음과 같습니다: 병합 요청, 취약점, Epic, 코드 스니펫, 커밋, DesignManagement::Design, 이슈, AlertManagement::AlertAbuseReport입니다.

모든 노트는 abuse notes를 제외하고 noteable을 통해 네임스페이스에 속해야 합니다.

lock_manager lwlocks의 경합 감소

테이블의 다형성 연관은 분할에 중요한 영향을 미칩니다.

테이블을 쿼리 대상으로 하는 쿼리가 최소한의 파티션에 액세스하거나 lock_manager lwlocks의 경합을 줄이는 방식으로 분할해야 합니다.

예를 들어 notes 테이블의 가장 흔한 액세스 패턴은 이슈와 같은 noteable의 노트를 가져오는 것입니다. 테이블을 id로 (범위, 해시 또는 목록) 분할하는 경우 다음 쿼리는 모든 파티션에 영향을 줄 수 있습니다: SELECT * FROM notes WHERE noteable_type='이슈' AND noteable_id=1;. 사용자 활동이나 TODO를 표시하려면 id별로 노트를 가져와야 할 경우: SELECT * FROM todos INNER JOIN notes WHERE notes.id=todos.note_id AND todos.id IN (1, 2, 3);. PostgreSQL은 note_id를 포함하지 않은 파티션을 제외하고 쿼리를 실행하기 위해 필요한 최소한의 파티션에만 액세스할 수 있습니다.

notes 테이블의 많은 열이 다형성 연관과 네임스페이스 및 프로젝트용으로 사용되는 열 외에도 널 가능한데, 분할이 보통 분할 열이 널이 아닌 것을 필요로 할 때 어려움이 있습니다.

분할 방법

여기서 notes 테이블을 분할하고 재구성하는 다양한 옵션을 고려합니다.

1. 도메인 모델별로 테이블 분할

도메인 모델별로 별도의 테이블로 분할하는 것은 최선의 실천 방법과 일치하지만, 얻게 되는 테이블은 여전히 100GB를 초과할 것입니다. 예를 들어, 별도의 issue_notes, merge_request_notes, epic_notes 테이블이 있을 수 있습니다.

이점:

  • 도메인 모델과 테이블 구성과 일치
    • 다른 분할 전략을 적용하기에 많은 병합 요청 특정 열이 방해요소이며, 인덱스 팽창과 이상적이지 않은 데이터 정렬에 기여합니다.
  • 다형성 연관 및 제약 조건 문제에 대응
    • 다형성 연관을 가지고 있는데다가, Git SHA 해시를 저장하는 commit_id 열의 존재가 데이터베이스 제약 조건을 완전히 활용하지 못하게 만듭니다.

단점:

  • 코드베이스 전체에 중대한 변경이 필요하게 될 것입니다.

  • 도메인으로 분할한 후에도, 얻게 되는 도메인 테이블은 100GB 크기 제한을 초과하며 각 테이블을 분할해야 할 것입니다.

2. namespace_id를 사용하여 해시로 파티셔닝

노트를 가져오는 일반적인 액세스 패턴을 고려할 때, noteable_type, noteable_id, 및 commit_id를 해시 키 열로 사용할 수 있습니다. 그러나, id를 사용하여 노트를 미리로드하는 쿼리와 같이 일부 쿼리는 namespace_id가 더 나은 선택일 수 있습니다(참조).

이점:

  • 앞으로 일부 파티션은 더 빠른 속도로 성장할 수 있지만, 추가 작업 없이 100GB의 대상 크기 제한을 달성할 수 있습니다.

단점:

  • 해시로 파티셔닝된 테이블의 기본 키는 해시 키 열을 포함해야 합니다. 하지만 여기에서 제안된 모든 해시 키는 null일 수 있으므로 기본 키로 사용될 수 없고 참조 무결성이 손실됩니다. 그러나 이 단점은 다형 연관 열을 기준으로 파티셔닝할 때만 발생합니다. namespace_id는 곧 Cells 1.0을 위한 notes 테이블의 샤딩 키가 될 것입니다.

  • namespace_idnotes 테이블을 위한 샤딩 키로 사용될 수 있으며, Cells 1.0 작업이 진행되면 파티셔닝된 notes 테이블에 외래 키를 쉽게 추가할 수 있습니다.

  • 모든 노트 쿼리를 namespace_id를 포함하도록 업데이트하려면 여전히 일부 코드 변경이 필요합니다.

  • 이 방법은 다형 연관이나 너무 많은 머지 리퀘스트 특정 열과 같은 기존의 구조적 문제를 다루지 않습니다.

3. 테이블의 수직 분할

가장 큰 두 열 notenote_html 또는 단순히 note_html의 수직 분할은 전체 저장 공간을 줄일 수 있습니다. PostgreSQL의 TOAST 기능을 노트 테이블에서 크게 이용할 수 없다는 점에 주목할 가치가 있습니다. 대부분의 노트 텍스트(note 열)는 압축 및 오프라인 저장을 유발하는데 필요한 2kB의 기본 임계값을 초과하지 않습니다.

수직 분할된 열을 위한 테이블은 notes 테이블의 id 열을 사용하여 int 범위로 파티셔닝될 수 있습니다.

이점:

  • notenote_html 열의 수직 분할은 테이블 레이아웃을 개선하고 남아 있는 notes 테이블의 튜플 크기를 조밀하게 만들어 더 나은 공간 지역성을 설정할 수 있습니다.

  • 기존 notes 테이블은 그대로 유지되므로 점진적인 접근으로 간주될 수 있습니다.

단점:

  • 수직 분할된 열을 포함하는 테이블을 미리로드할 때 배치 전략이 구현되어야 합니다. 예를 들어 16개 이상의 파티션이 있다면, 첫 번째 파티션에는 note_id < 100인 노트 레코드의 데이터가 포함됩니다. 두 번째 파티션에는 note_id >= 100인 노트 레코드의 데이터가 포함됩니다. 이렇게 여러 파티션에 액세스하는 미리로드 쿼리가 너무 많은 파티션에 액세스하지 않도록 하기 위해 쿼리를 여러 개로 분할할 수 있습니다: SELECT * FROM p_notes_data WHERE note_id < 100 AND note_id IN (1, 100, 20000, 30000), SELECT * FROM p_notes_data WHERE note_id >= 20000 AND note_id IN (1, 100, 20000, 30000) 등으로 분할할 수 있습니다.

  • notes에서 사용 중인 CacheMarkdownField는 코드베이스의 다른 부분에 암묵적인 종속성이 있습니다. 이들을 재정의하거나 관련 메서드를 투명하게 위임하는 시도는 깔끔하거나 쉽게 작동하지 않습니다. 또한 데이터베이스 기반 Markdown 캐싱 섹션을 확인하세요.

  • 100GB 대상 크기 제한을 충족시키려면 notes 테이블에 대한 추가적인 파티셔닝 작업이 필요합니다.

  • 이 방법은 다형 연관이나 너무 많은 머지 리퀘스트 특정 열과 같은 기존의 구조적 문제를 다루지 않습니다.

데이터베이스 기반 Markdown 캐싱에 대한 참고 사항

병합 요청 노트는 일반적인 감소 패턴을 따르며, 노트가 포함된 병합 요청이 닫혀지면 노트는 점차적으로 관련성을 잃어갑니다. 동일한 감소 패턴이 다른 노트 가능 유형에도 적용될 수 있으며, 일정 기간 이전의 노트에 대한 캐시된 Markdown을 삭제하는 것은 저장된 데이터를 줄이는 데 유효한 방법일 수 있습니다.

캐시된 Markdown이 삭제된 이전 노트를 다시 계산하는 것이 레일즈 애플리케이션과 PostgreSQL 호스트에 부담을 줄 수 있다는 가능한 위험성이 있습니다. 과거에는 많은 노트를 렌더링하고 캐시된 Markdown 버전이 노트용으로 업데이트될 때 애플리케이션과 데이터베이스가 심각하게 손상된 것으로 관찰되었습니다.

쓰래싱 효과가 덜 걱정스러울 때 Redis만을 사용하여 Markdown을 캐싱하고 데이터베이스 캐싱 레이어를 제거하는 것이 값진 조사 대상이 될 수 있습니다.

4. noteable_type을 사용하여 목록으로 파티셔닝

notes 테이블 자체를 noteable_type 열의 값에 따라 파티셔닝하는 것을 고려할 수 있습니다.

주의: 2024년 3월 8일 현재, noteable_typeINVALID 비-NULL 확인 제약 조건이 있습니다. GitLab.com의 프로덕션 데이터베이스에서 noteable_type이 없는 일부 notes 레코드가 발견되었으며 제거되었습니다. 비-NULL 확인 제약 조건은 17.0에서 확인될 예정입니다.

이점:

  • 거의 모든 notes 쿼리에는 noteable_type이 포함되어 있어 파티셔닝에 이 열을 사용하는 것이 이상적일 수 있습니다.

  • noteable_type로 목록별 파티셔닝을 하면 결과 파티션을 도메인으로 추가로 파티셔닝할 수 있게 됩니다.

단점:

  • 하위 파티셔닝에는 여전히 루트 테이블의 기본 키에 파티셔닝 키가 있어야 합니다.

  • 이 방법은 notes 테이블을 참조하는 테이블에 많은 외래 키를 추가해야 합니다. 파티셔닝된 테이블은 복합 기본 키인 (noteable_type, id)를 사용하므로 noteable_type은 모든 참조 테이블에 처음으로 추가되어야 하며 역으로 완전히 작성되어야 합니다.

  • 파티셔닝된 테이블을 사용하는 Active Record 모델이 제대로 작동하도록 하기 위해 추가적인 코드 변경이 필요할 수 있습니다.

  • 이 방법은 다형 연관이나 너무 많은 머지 리퀘스트 특정 열과 같은 기존의 구조적 문제를 다루지 않습니다. 그러나 도메인별 하위 파티셔닝이 가능하다는 가능성은 단점을 줄일 수 있습니다.