- GitLab 스키마
- 마이그레이션
- CI/CD 데이터베이스
- 데이터베이스 간 외래 키
- 다중 데이터베이스 테스트
main_clusterwide
를 포함한 다중 데이터베이스 테스트- 데이터베이스 스키마에 속하지 않는 테이블에 대한 쓰기 잠금
- 테이블 잘라내기
다중 데이터베이스
GitLab이 더 확장될 수 있도록 허용하기 위해,
GitLab 애플리케이션 데이터베이스를 여러 데이터베이스로 분해했습니다.
이 두 개의 데이터베이스는 main
및 ci
입니다.
GitLab은 하나의 데이터베이스 또는 두 개의 데이터베이스로 실행될 수 있습니다.
우리는 GitLab.com에서 두 개의 별도 데이터베이스를 사용하고 있습니다.
Cells 아키텍처를 구축하기 위해,
데이터베이스를 추가로 분해하여 다른 데이터베이스 gitlab_main_clusterwide
를 도입합니다.
GitLab 스키마
다른 데이터베이스 간 허용된 패턴을 올바르게 식별해내기 위해 GitLab 애플리케이션은 데이터베이스 사전을 구현하였습니다.
데이터베이스 사전은 테이블을 가상으로 gitlab_schema
로 분류하는데
개념적으로 PostgreSQL 스키마와 유사합니다.
우리는 PostgreSQL 스키마를 복잡한 마이그레이션 절차 때문에 사용할 수 없다는 것을 결정하였습니다.
대신에, 우리는 애플리케이션 수준의 분류 개념을 구현하였습니다.
GitLab의 각 테이블은 gitlab_schema
가 지정되어야 합니다:
데이터베이스 | 설명 | 참고 |
---|---|---|
gitlab_main
|
main: 데이터베이스에 저장되는 모든 테이블
| 현재 이를 Cells 아키텍처를 위해 gitlab_main_cell 로 대체하고 있습니다. gitlab_main_cell 스키마는 GitLab 설치의 셀에 로컬인 모든 테이블을 설명합니다. 예를 들어, projects 및 groups
|
gitlab_main_clusterwide
| Cells 아키텍처에 따라 GitLab 설치 전체에 있는 모든 테이블 | 예를 들어, users 및 application_settings
|
gitlab_ci
|
ci: 데이터베이스에 저장되는 모든 CI 테이블 (예: ci_pipelines , ci_builds )
| |
gitlab_geo
|
geo: 데이터베이스에 저장되는 모든 Geo 테이블 (예: project_registry , secondary_usage_data )
| |
gitlab_shared
| 모든 분해된 데이터베이스 전체의 데이터를 포함하는 모든 애플리케이션 테이블 (예: loose_foreign_keys_deleted_records ), Gitlab::Database::SharedModel 을 상속한 모델에 대해
| |
gitlab_internal
| Rails 및 PostgreSQL의 모든 내부 테이블 (예: ar_internal_metadata , schema_migrations , pg_* )
| |
gitlab_pm
|
package_metadata 를 저장하는 모든 테이블
|
gitlab_main 의 별칭입니다
|
추가로 분해된 데이터베이스들로 도입될 것으로 예상되는 스키마들
스키마 사용은 기본 클래스를 사용하도록 강요받습니다:
-
gitlab_main_cell
을 위한ApplicationRecord
-
gitlab_main_clusterwide
를 위한MainClusterwide::ApplicationRecord
-
gitlab_ci
를 위한Ci::ApplicationRecord
-
gitlab_geo
를 위한Geo::TrackingBase
-
gitlab_shared
를 위한Gitlab::Database::SharedModel
-
gitlab_pm
를 위한PackageMetadata::ApplicationRecord
gitlab_main_cell
및 gitlab_main_clusterwide
스키마 선택 가이드라인
사용 사례에 따라, 기능이 셀 로컬 또는 클러스터 전역일 수 있으며, 따라서 기능에 사용되는 테이블도 적절한 스키마를 사용해야합니다.
테이블에 적절한 스키마를 선택할 때, Cells 아키텍처의 일환으로 다음 가이드라인을 고려하세요:
-
gitlab_main_cell
을 기본값으로 설정: 대부분의 테이블이 기본적으로gitlab_main_cell
스키마에 할당되기를 기대합니다. 테이블의 데이터가projects
또는namespaces
와 관련이 있다면 이 스키마를 선택하세요. - Tenant Scale 그룹과 상의: 특정 테이블에
gitlab_main_clusterwide
스키마가 더 적절하다고 생각된다면, Tenant Scale 그룹과 승인을 요청하세요. 이것은 확장성에 영향을 미치며 스키마 선택을 다시 고려해야하기 때문에 중요합니다.
기존 테이블이 어떻게 분류되었는지 이해하기 위해 이 대시보드를 사용할 수 있습니다.
스키마가 할당된 후, Merge Request 파이프라인은 다음 중 하나 이상의 이유로 실패할 수 있으며, 해당 가이드라인을 따름으로써 해결할 수 있습니다:
모든 셀 로컬 테이블에 대한 분할 키 정의
다음 gitlab_schema
와 함께 모든 테이블은 “셀 로컬”로 간주됩니다:
gitlab_main_cell
gitlab_ci
새로 만들어진 셀 로컬 테이블은 해당 테이블에 대한 db/docs/
파일에 정의된 sharding_key
가 있어야 합니다.
분할 키의 목적은 조직 분할 청사진에 문서화되어 있지만, 간단히 말해 이 열은 데이터베이스에서 특정 행을 소유하는 조직을 결정하는 표준 방법을 제공하는 데 사용됩니다.
이 열은 미래에는 데이터베이스간 조인을 제약할 표준 방법을 제공하기 위해 사용될 것입니다. 또한, 미래에는 셀 간 데이터 마이그레이션을 제공하기 위해 사용될 것입니다.
외래 키의 실제 이름은 무엇이든 상관없지만, 해당 테이블의 행을 참조해야 합니다. 선택된 sharding_key
열은 null일 수 없어야 합니다.
다음은 유효한 분할 키의 예시입니다:
-
테이블 항목은 프로젝트에만 속합니다:
sharding_key: project_id: projects
-
테이블 항목은 프로젝트에 속하며, 외래 키는
target_project_id
:sharding_key: target_project_id: projects
-
테이블 항목은 네임스페이스/그룹에만 속합니다:
sharding_key: namespace_id: namespaces
-
테이블 항목은 네임스페이스/그룹에만 속하며, 외래 키는
group_id
:sharding_key: group_id: namespaces
-
테이블 항목은 네임스페이스 또는 프로젝트에 속합니다:
sharding_key: project_id: projects namespace_id: namespaces
샤딩 키는 불변해야 합니다
sharding_key
의 선택은 항상 불변해야 합니다. 따라서, 귀하의 기능이 데이터를 프로젝트 또는 그룹/네임스페이스간에 이동시키는 사용자 경험을 필요로 하는 경우, 이동 기능을 다시 디자인하여 새로운 행을 만들어야 할 수 있습니다. 이와 관련한 예시로 이슈 이동 기능이 있습니다. 이 기능은 실제로 기존 이슈
행의 project_id
열을 변경하지 않고 새로운 이슈
행을 만들고 데이터베이스에서 원래 이슈
행으로의 링크를 생성합니다. 데이터를 이동할 수 있어야 하는 특히 어려운 기존 기능이 있는 경우, 샤딩 키를 어떻게 관리해야 하는지에 대한 옵션을 논의하기 위해 조기에 테넌트 스케일 팀에 연락해야 할 것입니다.
프로젝트 및 네임스페이스에 동일한 샤딩 키 사용하기
개발자들은 또한 해당 테이블에서 사용하는 기능에 따라 namespace_id
만을 선택할 수도 있습니다. 이 경우 namespace_id
는 그룹 및 프로젝트 통합 청사진을 따라 개발 중인 프로젝트에 속할 수 있는 테이블에만 필요합니다. 이 경우 namespace_id
는 네임스페이스가 속한 그룹의 ID여야 합니다.
desired_sharding_key
정의를 위한 sharding_key
의 자동 배치
샤딩 키가 없는 수백 개의 테이블에 대해 sharding_key
를 배치해야 합니다. 이 과정은 새로운 열을 추가하여 관련 테이블의 데이터를 배치하고, 이후에 인덱스, 외래 키, 및 Not-NULL 제약 조건을 추가하기 위해 후속 마이그레이션을 만드는 Merge Request을 생성하는 것을 포함할 것입니다.
개발자들을 위한 반복되는 수고량을 최소화하기 위해, 이 특정 테이블의 sharding_key
를 백필하는 방법을 간결하게 설명하는 선언적인 방법을 소개했습니다. 이 내용은 나중에 모든 필요한 Merge Request을 만들기 위해 자동화될 것입니다.
desired_sharding_key
의 예시는 여기에 추가되었습니다:
--- # db/docs/security_findings.yml
table_name: security_findings
classes:
- Security::Finding
...
desired_sharding_key:
project_id:
references: projects
backfill_via:
parent:
foreign_key: scanner_id
table: vulnerability_scanners
sharding_key: project_id
belongs_to: scanner
이 YAML 데이터가 어떻게 사용될 것인지 이해하기 위해 매뉴얼으로 생성된 Merge Request에 매핑할 수 있습니다. 이 아이디어는 이를 자동으로 생성하는 것입니다. YAML의 내용은 배치된 background migration에서 desired_sharding_key
를 자동으로 채울 부모 테이블 및 sharding_key
을 명시합니다. 또한 before_save
에서 sharding_key
를 자동으로 채울 수 있도록 모델에 belongs_to
관계를 추가합니다.
부모 테이블에 이미 desired_sharding_key
가 있는 경우 desired_sharding_key
정의하기
기본적으로 desired_sharding_key
구성은 선택한 sharding_key
이 부모 테이블에 존재하는지 확인합니다. 그러나, 부모 테이블에 이미 desired_sharding_key
구성이 있고 아직 백필되지 않은 경우, awaiting_backfill_on_parent
필드를 포함해야 합니다. 예시:
desired_sharding_key:
project_id:
references: projects
backfill_via:
parent:
foreign_key: package_file_id
table: packages_package_files
sharding_key: project_id
belongs_to: package_file
awaiting_backfill_on_parent: true
desired_sharding_key
구조가 sharding_key
를 백필하는 데 적합하지 않은 경우가 있을 수 있습니다. 이러한 경우에는 테이블을 소유한 팀이 sharding_key
를 매뉴얼으로 추가하기 위해 필요한 Merge Request을 만들어야 할 것입니다.
특정 테이블을 샤딩 키의 적용 대상에서 제외하기
다음을 추가함으로써 특정 테이블을 샤딩 키의 요구 조건에서 제외할 수 있습니다.
exempt_from_sharding: true
이것은 현재 JiHu 특정 테이블의 경우는 .com
데이터베이스에는 어떠한 데이터도 포함하지 않기 때문에 해당 테이블들이 샤딩 키 요구 사항에서 제외되어야 하는 경우입니다. 이것은 !145905에서 구현되었습니다.
샤딩 키 요구 사항에서 제외된 테이블은 또한 진행 대시보드에 표시되지 않습니다.
gitlab_schema
의 영향
gitlab_schema
의 사용은 애플리케이션에 상당한 영향을 미칩니다. gitlab_schema
의 주요 목적은 서로 다른 데이터 액세스 패턴 사이에 장벽을 소개하는 것입니다.
이것은 다음에서 기본적으로 사용됩니다:
gitlab_shared
의 특별한 목적
gitlab_shared
는 설계상 데이터베이스 분할된 모든 데이터를 포함하는 테이블 또는 뷰를 설명하는 특별한 케이스입니다. 이 분류는 loose_foreign_keys_deleted_records
와 같은 애플리케이션에서 정의된 테이블을 설명합니다.
gitlab_shared
를 사용할 때 데이터에 액세스할 때 특별한 처리가 필요합니다.
gitlab_shared
는 구조 뿐만 아니라 데이터를 공유하기 때문에 애플리케이션은 모든 데이터를 순차적으로 탐색하기 위해 작성되어야 합니다.
Gitlab::Database::EachDatabase.each_model_connection([MySharedModel]) do |connection, connection_name|
MySharedModel.select_all_data...
end
따라서, gitlab_shared
테이블의 데이터를 수정하는 마이그레이션은 모든 분할된 데이터베이스에서 실행될 것으로 예상됩니다.
gitlab_internal
의 특별한 목적
gitlab_internal
은 schema_migrations
또는 ar_internal_metadata
와 같이 Rails에서 정의된 테이블, 그리고 내부 PostgreSQL 테이블(예: ‘pg_attribute’)을 설명합니다. 이것의 주요 목적은 다른 데이터베이스를 지원하기 위한 것입니다. 이는 gitlab_shared
테이블(예: loose_foreign_keys_deleted_records
) 중 일부가 없을 수 있는 Geo와 같은 다른 데이터베이스를 위한 것으로, 그런데 이는 유효한 Rails 데이터베이스입니다.
gitlab_pm
의 특별한 목적
gitlab_pm
은 공개 리포지터리에 대한 패키지 메타데이터를 저장합니다. 이 데이터는 라이선스 컴플라이언스 및 의존성 스캔 제품 범주에 사용되며 Composition Analysis Group에서 유지보수됩니다. 이는 향후 다른 데이터베이스로 라우팅하는 것을 더 쉽게 만들기 위해 만든 gitlab_main
의 별칭입니다.
마이그레이션
Migrations for Multiple Databases를 읽으세요.
CI/CD 데이터베이스
단일 데이터베이스 구성
기본적으로 GDK는 여러 데이터베이스를 사용하여 실행하도록 구성되어 있습니다.
ci
데이터베이스에있는 모든 데이터는 단일 데이터베이스 모드에서 액세스할 수 없을 것입니다. 단일 데이터베이스를 사용하려면 별도의 개발 인스턴스를 사용해야 합니다.단일 데이터베이스를 사용하도록 GDK를 구성하려면:
-
GDK 루트 디렉터리에서 다음을 실행하세요:
gdk config set gitlab.rails.databases.ci.enabled false
-
GDK를 다시 구성하세요:
gdk reconfigure
다중 데이터베이스를 사용하도록 변경하려면 gitlab.rails.databases.ci.enabled
를 true
로 설정하고 gdk reconfigure
를 실행하세요.
ci
및 비 ci
테이블 간의 조인 제거
데이터베이스 간에 조인하는 쿼리는 오류를 발생시킵니다. 새로운 쿼리에 대해서만 Introduced in GitLab 14.3, for new queries only. Pre-existing queries do not raise an error.
GitLab은 여러 개별 데이터베이스를 사용하여 실행될 수 있기 때문에, 하나의 쿼리에서 ci
테이블과 비 ci
테이블을 참조하는 것은 불가능합니다. 따라서 SQL 쿼리에서 어떤 종류의 JOIN
을 사용하는 것은 작동하지 않을 것입니다.
데이터베이스 간 조인 제거에 대한 제안
다음 섹션들은 데이터베이스 간에 조인을 식별한 실제 예제들과 이를 수정하는 방법에 대한 가능한 제안들입니다.
코드 제거
우리가 여러 차례보았던 가장 간단한 해결책은 사용되지 않는 기존 스코프입니다. 이것은 가장 쉽게 고칠 수 있는 예제입니다. 따라서 첫 번째 단계는 코드가 사용되지 않는지 조사하고 제거하는 것입니다. 다음은 실제 예제입니다:
- https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67162
- https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66714
- https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66503
코드가 사용되는 경우가 더 있을 수 있지만, 우리는 그것이 필요한지 또는 기능이 이렇게 동작해야 하는지를 평가할 수 있습니다. 새로운 열과 테이블을 추가하는 것으로 문제를 복잡하게 만들기 전에 요구 사항을 여전히 충족하면서 해결책을 간소화할 수 있는지 고려하세요.
UsageData
의 일부 사용을 변경하여 조인 쿼리를 제거하는 방법을 평가 중인 사례가 있습니다.
https://gitlab.com/gitlab-org/gitlab/-/issues/336170에서 평가 중인 이 사례는 사용자에게 중요하지 않으며 더 간단한 접근 방식으로도 유사한 유용한 메트릭을 얻을 수 있을지도 모릅니다. 또는 이러한 메트릭을 사용하는 사람이 아무도 없을 수 있습니다.
includes
대신 preload
사용
Rails의 includes
및 preload
메서드는 N+1 쿼리를 피하는 방법입니다. Rails의 includes
메서드는 휴리스틱 접근 방식을 사용하여 테이블에 조인해야 하는지 또는 별도의 쿼리로 모든 레코드를로드할 수 있는지를 결정합니다. 이 메서드는 다른 테이블의 열을 쿼리해야 한다고 생각되면 조인해야한다고 가정하지만 때로는 이 메서드가 잘못 판단하고 필요하지 않은 경우에도 조인을 실행할 수 있습니다. 이 경우 별도의 쿼리로 데이터를 명시적으로로드하는 preload
을 사용하면 조인을 피할 수 있으면서 여전히 N+1 쿼리를 피할 수 있습니다.
이 해결책이 실제로 사용된 예제를 다음에서 볼 수 있습니다. https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67655.
중복 조인 제거
가끔은 쿼리에서 초과(또는 중복) 조인이 발생하는 경우가 있습니다.
A
에서 B
라는 일부 테이블을 통해 C
에 조인하는 쿼리의 경우, 당신이 C
에있는 행 수를 계산하고 B
의 외래 키 및 NOT NULL
제약 조건을 확인하는 경우가 있습니다. 그 경우 그 행을 계산하는 것만으로 충분할 수도 있습니다.
예를 들어, MR 71811에서 이전에는 project.runners.count
를 수행하여 다음과 같은 쿼리가 생성되었습니다.
select count(*) from projects
inner join ci_runner_projects on ci_runner_projects.project_id = projects.id
where ci_runner_projects.runner_id IN (1, 2, 3)
이것을 project.runner_projects.count
로 변경하여 다음과 같은 쿼리를 생성하여 교차 조인을 피했습니다.
select count(*) from ci_runner_projects
where ci_runner_projects.runner_id IN (1, 2, 3)
또 다른 중복 조인을 제거하는 일반적인 예는 다른 테이블까지 모두 조인 한 다음 기본 키로 필터링하는 경우입니다. 이러한 경우에는 대신 외래 키를 필터링할 수 있었을 것입니다.
예를 들어, 이전 코드는 joins(scan: :build).where(ci_builds: { id: build_ids })
였으며
다음과 같은 쿼리를 생성했습니다.
select ...
inner join security_scans
inner join ci_builds on security_scans.build_id = ci_builds.id
where ci_builds.id IN (1, 2, 3)
그러나 security_scans
에 이미 외래 키 build_id
가 있으므로 코드를 joins(:scan).where(security_scans: { build_id: build_ids })
로 변경할 수 있으며 다음과 같은 쿼리를 생성하여 동일한 응답을 얻을 수 있습니다.
select ...
inner join security_scans
where security_scans.build_id IN (1, 2, 3)
이러한 중복 조인을 제거하는 예는 교차 조인을 제거하는 것뿐만 아니라 더 간단하고 빠른 쿼리를 생성하는 이점이 있습니다.
제한된 pluck 뒤에 find
pluck
또는 pick
을 사용하여 id
배열을 가져오는 것은 반환된 배열의 크기가 제한되어 있음이 보장되지 않는 한 권장되지 않습니다. 보통 이는 결과가 최대 1개일 것이거나, 같은 크기의 메모리 내 id(또는 사용자 이름) 디렉터리이 다른 디렉터리에 매핑되어야 하는 경우에 좋은 패턴입니다. 이것은 1:다 관계로 id 디렉터리을 매핑할 때에는 적합하지 않습니다. 그러면 반환된 id
를 사용하여 관련 레코드를 얻을 수 있습니다:
allowed_user_id = board_user_finder
.where(user_id: params['assignee_id'])
.pick(:user_id)
User.find_by(id: allowed_user_id)
이 방법이 사용된 예제는 https://gitlab.com/gitlab-org/gitlab/-/merge_requests/126856에서 확인할 수 있습니다.
가끔 결합을 pluck
로 변환하기 쉬워보일 수 있지만, 이는 종종 메모리에 제한 없는 양의 id를 로딩하고 그것을 포스트그리스에 다시 직렬화하는 결과를 초래합니다. 이러한 경우는 확장되지 않으며, 다른 옵션을 시도하는 것을 권장합니다. pluck
데이터에 limit
을 적용하여 메모리를 제한하는 것이 좋은 아이디어처럼 보일 수 있지만 사용자에게 예측할 수 없는 결과를 가져오며, 때로는 가장 큰 고객(우리를 포함한)에게 가장 문제가 되는 경우가 많으므로, 이것을 권장하지 않습니다.
외래키를 테이블로 De-normalize
De-normalization은 일부 쿼리를 단순화하거나 성능을 향상시키기 위해 테이블에 중복된 미리 계산된 데이터를 추가하는 것을 의미합니다. 이 경우, 중간 테이블을 통해 결합하는 경우 유용할 수 있습니다.
보통 데이터베이스 스키마를 모델링할 때 “정규화”된 구조가 선호되는데, 이는 다음과 같은 이유 때문입니다:
- 중복된 데이터는 추가 저장 공간을 사용합니다.
- 중복된 데이터는 동기화되어야 합니다.
때로는 정규화된 데이터가 성능이 떨어질 수 있으므로, GitLab은 데이터베이스 쿼리의 성능을 향상시키기 위해 오랜 시간 사용해온 De-normalization 기술을 사용할 수 있습니다. 위의 문제는 다음의 조건을 충족할 때 완화됩니다:
- 데이터가 많지 않을 때 (예를 들어, 정수 열만 있는 경우)
- 데이터가 자주 업데이트되지 않을 때 (예를 들어, 대부분의 테이블에서
project_id
열은 거의 업데이트되지 않음)
우리가 발견한 한 예는 security_scans
테이블입니다. 이 테이블은 security_scans.build_id
라는 외래 키를 가지고 있어 빌드에 대한 조인을 가능하게 합니다. 따라서 프로젝트와 다음과 같이 조인할 수 있습니다:
select projects.* from security_scans
inner join ci_builds on security_scans.build_id = ci_builds.id
inner join projects on ci_builds.project_id = projects.id
이 쿼리의 문제는 ci_builds
가 다른 두 테이블과는 다른 데이터베이스에 있음입니다.
이 경우의 해결책은 security_scans
에 project_id
열을 추가하는 것입니다. 이는 거의 추가 저장 공간을 사용하지 않으며, 이러한 기능의 동작 방식으로 인해 거의 업데이트되지 않습니다 (빌드는 프로젝트를 거의 옮기지 않음).
이로써 쿼리가 단순해졌습니다:
select projects.* from security_scans
inner join projects on security_scans.project_id = projects.id
이는 또한 추가 테이블을 통한 조인 없이 성능을 개선합니다.
이 접근 방식이 적용된 예제는 https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66963에서 확인할 수 있습니다. 이 MR은 비슷한 쿼리를 고치기 위해 pipeline_id
를 De-normalized합니다.
추가 테이블로 De-normalize
이전의 De-normalization(추가 열 추가)가 특정 상황에 맞지 않을 수 있습니다. 데이터가 1:1이 아니거나, 이미 너무 넓은 테이블(예를 들어 projects
테이블)에 열을 추가할 수 없을 때입니다.
이 경우에는 추가 데이터를 별도의 테이블에 저장할 수 있습니다.
이 접근 방식이 사용된 예제는 Project.with_code_coverage
스코프를 구현한 경우입니다. 이 스코프는 기본적으로 코드 커버리지 기능을 사용한 적이 있는 프로젝트 디렉터리만을 좁히는 데 사용되었습니다. 이 쿼리(간소화)는 다음과 같습니다:
select projects.* from projects
inner join ci_daily_build_group_report_results on ci_daily_build_group_report_results.project_id = projects.id
where ((data->'coverage') is not null)
and ci_daily_build_group_report_results.default_branch = true
group by projects.id
이 작업은 여전히 진행 중입니다만, 현재 계획은 projects_with_ci_feature_usage
라는 새 테이블을 도입하는 것입니다. 이 테이블에는 project_id
와 ci_feature
2개의 열이 있습니다. 이 테이블은 프로젝트가 ci_daily_build_group_report_results
를 사용하여 코드 커버리지를 생성하는 첫 번째 시간에 작성될 것입니다. 따라서 새로운 쿼리는 다음과 같을 것입니다:
select projects.* from projects
inner join projects_with_ci_feature_usage on projects_with_ci_feature_usage.project_id = projects.id
where projects_with_ci_feature_usage.ci_feature = 'code_coverage'
위의 예에서는 텍스트 열을 사용했지만, 공간을 절약하기 위해 enum을 사용하는 것이 좋습니다.
이 새로운 설계의 단점은 이것이 필요에 따라 업데이트(예: ci_daily_build_group_report_results
가 삭제된 경우)되어야 할 수도 있습니다. 그러나 도메인에 따라서 필요하지 않은 경우도 있을 수 있습니다. 또한, 삭제는 엣지 케이스나 불가능한 상황일 수 있으며, 프로젝트 디렉터리 페이지에 프로젝트가 표시되는 것이 문제가 되지 않을 수 있습니다. 또한 도메인에서 필요할 때 또는 꼭 필요할 때 이러한 행을 삭제하는 로직을 구현할 수도 있습니다.
마지막으로, 이 De-normalization과 새로운 쿼리는 또한 조인이 적고 필터링이 적은 성능을 향상시킵니다.
has_one
또는 has_many
through:
관계에 대해 disable_joins
사용
가끔 has_one ... through:
또는 has_many ... through:
를 사용하여 다른 데이터베이스에 걸쳐있는 테이블을 이어붙이게 되면 조인 쿼리가 발생합니다. 이러한 조인은 때때로 disable_joins:true
를 추가함으로써 해결될 수 있습니다. 이것은 Rails 기능으로, 우리는 이 기능을 backported했습니다. 우리는 또한 disable_joins
를 활성화하는 람다 구문을 사용할 수 있도록 기능을 확장했습니다. 이 기능을 사용하는 경우 심각한 성능 후퇴가 있는 경우에 대비하여 피처 플래그를 사용하는 것이 좋습니다.
이 접근 방식이 사용된 예제는 https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66709/diffs에서 확인할 수 있습니다.
DB 쿼리에 대한 변경 사항은 중요한데 이를 분석하고 변경 전과 후의 SQL을 비교하는 것이 중요합니다. disable_joins
는 has_many
또는 has_one
관계의 실제 논리에 따라 성능이 굉장히 나쁠 수 있습니다. 확인해야 할 중요한 점은 최종 결과 집합을 구성하는 데 사용되는 중간 결과 집합 중에 메모리에 불특정 양의 데이터가 로딩되지는 않는지입니다. 확인하는 가장 좋은 방법은 생성된 SQL을 살펴보고 각각이 어떤 방식으로 제한되었는지 확인하는 것입니다. LIMIT 1
절이나 고유한 열을 기반으로 한 WHERE
절에 기반해서 어떤 것이든지 자세히 확인할 수 있습니다. 불특정한 중간 데이터 세트는 메모리에 너무 많은 ID를 로딩할 수 있습니다.
아래의 가정적인 코드처럼 매우 낮은 성능을 볼 수 있는 예에서 매우 낮은 성능을 볼 수 있을 수 있습니다:
class Project
has_many :pipelines
has_many :builds, through: :pipelines
end
class Pipeline
has_many :builds
end
class Build
belongs_to :pipeline
end
def some_action
@builds = Project.find(5).builds.order(created_at: :desc).limit(10)
end
위의 경우 some_action
은 다음과 같은 쿼리를 생성합니다:
select * from builds
inner join pipelines on builds.pipeline_id = pipelines.id
where pipelines.project_id = 5
order by builds.created_at desc
limit 10
그러나, 관계를 다음과 같이 변경하는 경우:
class Project
has_many :pipelines
has_many :builds, through: :pipelines, disable_joins: true
그러면 다음 2개의 쿼리를 얻게 됩니다:
select id from pipelines where project_id = 5;
select * from builds where pipeline_id in (...)
order by created_at desc
limit 10;
첫 번째 쿼리는 고유한 열에 의해 제한되지 않거나 LIMIT
절이 없기 때문에 무제한 수의 pipeline ID가 메모리에 로딩될 수 있으므로 다음 쿼리에 전송됩니다. 이것은 Rails 애플리케이션과 데이터베이스에서 매우 낮은 성능을 가져올 수 있습니다. 이러한 경우에는 쿼리를 다시 작성하거나 교차 조인을 제거하는 위에서 설명한 다른 패턴을 살펴보아야 할 수 있습니다.
교차 조인이 올바르게 제거되었는지 확인하는 방법
RSpec는 모든 SQL 쿼리가 데이터베이스 간에 조인하지 않도록 자동으로 검증하도록 구성되어 있습니다. 이 검증이 spec/support/database/cross-join-allowlist.yml
에서 비활성화된 경우에도 with_cross_joins_prevented
를 사용하여 고립된 코드 블록을 검증할 수 있습니다.
다음과 같이 이 메서드를 사용할 수 있습니다:
it 'does not join across databases' do
with_cross_joins_prevented do
::Ci::Build.joins(:project).to_a
end
end
이는 쿼리가 두 개의 데이터베이스 간에 조인되면 예외를 발생시킵니다. 이전 예제는 다음과 같이 조인을 제거하여 수정됩니다.
it 'does not join across databases' do
with_cross_joins_prevented do
::Ci::Build.preload(:project).to_a
end
end
이 메서드를 사용하여 교차 조인을 수정하는 실제 예제는 https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67655에서 확인할 수 있습니다.
기존 교차 조인을 위한 허용 디렉터리
교차 조인을 식별하는 가장 쉬운 방법은 실패한 파이프라인을 통해 알아내는 것입니다.
예를 들어, !130038에서 notification_settings
테이블을 gitlab_main_cell
스키마로 이동하였으며, 이를 db/docs/notification_settings.yml
파일에서 표시했습니다.
파이프라인은 다음과 같은 에러로 실패했습니다.
Database::PreventCrossJoins::CrossJoinAcrossUnsupportedTablesError:
'gitlab_main_clusterwide, gitlab_main_cell'을(를) 쿼리하는 'users, notification_settings' 간의 지원되지 않는 교차 조인이 감지되었습니다. 'SELECT "users".* FROM "users" WHERE "users"."id" IN (SELECT "notification_settings"."user_id" FROM ((SELECT "notification_settings"."user_id" FROM "notification_settings" WHERE "notification_settings"."source_id" = 119 AND "notification_settings"."source_type" = 'Project' AND (("notification_settings"."level" = 3 AND EXISTS (SELECT true FROM "notification_settings" "notification_settings_2" WHERE "notification_settings_2"."user_id" = "notification_settings"."user_id" AND "notification_settings_2"."source_id" IS NULL AND "notification_settings_2"."source_type" IS NULL AND "notification_settings_2"."level" = 2)) OR "notification_settings"."level" = 2))) notification_settings)'를 실행하는 중에 발생했습니다.
파이프라인을 성공시키기 위해서는 이 교차 조인 쿼리를 허용 디렉터리에 추가해야 합니다.
데이터베이스 간의 교차 조인은 ::Gitlab::Database.allow_cross_joins_across_databases
도우미 메서드로 명시적으로 허용할 수 있습니다. 또한 특정 관계를 relation.allow_cross_joins_across_databases
로 표시하는 대체 방법도 있습니다.
이 메서드는 다음과 같이 사용해야 합니다:
# 데이터베이스에서 개체를 실행하는 블록을 범위 지정
::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336590') do
subject.perform(1, 4)
end
# 허용된 관계로 데이터베이스 간에 조인할 수 있도록 지정
def find_actual_head_pipeline
all_pipelines
.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336891')
.for_sha_or_source_sha(diff_head_sha)
.first
end
모델 관계나 스코프에서는 다음 예제처럼 사용할 수 있습니다:
class Group < Namespace
has_many :users, -> {
allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/422405")
}, through: :group_members
end
class Group < Namespace
has_many :users, through: :group_members
# 이와 같이 관계를 재정의하지 마십시오.
def users
super.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/422405")
end
end
url
매개변수는 교차 조인을 수정할 예정인 마일스톤이 있는 이슈를 가리켜야 합니다. 교차 조인이 마이그레이션에 사용된다면 코드를 수정할 필요가 없습니다. 자세한 내용은 https://gitlab.com/gitlab-org/gitlab/-/issues/340017를 참조하십시오.
교차 데이터베이스 트랜잭션 제거
여러 데이터베이스를 다룰 때 한 데이터 수정이 여러 데이터베이스에 영향을 미치는 것에 주의해야 합니다. GilLab 14.4에서 소개된 자동화된 체크는 교차 데이터베이스 수정을 방지합니다.
어떤 데이터베이스 서버에서 시작된 트랜잭션 중에 적어도 두 개의 다른 데이터베이스가 수정되면 애플리케이션은 교차 데이터베이스 수정 오류를 트리거합니다(테스트 환경에서만).
예:
# 메인 DB에서 트랜잭션 열기
ApplicationRecord.transaction do
ci_build.update!(updated_at: Time.current) # CI DB에서 UPDATE
ci_build.project.update!(updated_at: Time.current) # 메인 DB에서 UPDATE
end
# 'ci_build, projects' 테이블을 수정하는 트랜잭션 중에 'main, ci'의 교차 데이터베이스 데이터 수정이 감지되어 오류가 발생합니다
위의 코드 예시는 트랜잭션 내에서 두 레코드의 타임스탬프를 업데이트합니다. CI 데이터베이스 분해 작업이 진행 중이므로 데이터베이스 트랜잭션의 정의를 보장할 수 없습니다.
두 번째 업데이트 쿼리가 실패하면 첫 번째 업데이트 쿼리는 롤백되지 않습니다. 왜냐하면 ci_build
레코드가 다른 데이터베이스 서버에 있기 때문입니다.
더 많은 정보는 트랜잭션 가이드라인 페이지를 참조하세요.
크로스 데이터베이스 트랜잭션 수정
데이터베이스 간 트랜잭션은 Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction
도우미 메서드로 코드를 감싸면 명시적으로 허용될 수 있습니다.
Rails 콜백에서는 cross_database_ignore_tables
메서드를 사용하여 크로스 데이터베이스 트랜잭션을 처리할 수 있습니다.
이러한 메서드는 기존 코드에만 사용해야 합니다.
temporary_ignore_tables_in_transaction
도우미 메서드는 다음과 같이 사용할 수 있습니다:
class GroupMember < Member
def update_two_factor_requirement
return unless user
# 회원 및 사용자/사용자_세부정보/사용자_환경을 포함한 크로스 데이터베이스 트랜잭션을 표시 및 무시하기 위해
Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.temporary_ignore_tables_in_transaction(
%w[users user_details user_preferences], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424288'
) do
user.update_two_factor_requirement
end
end
end
cross_database_ignore_tables
메서드는 다음과 같이 사용할 수 있습니다:
class Namespace < ApplicationRecord
include CrossDatabaseIgnoredTables
# 네임스페이스 및 레일스 콜백 내에서 발생하는 루트/리다이렉트_루트와 관련된 크로스 데이터베이스 트랜잭션을 표시하고 무시합니다.
cross_database_ignore_tables %w[routes redirect_routes], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424277'
end
트랜잭션 블록 제거
열린 트랜잭션이 없으면 크로스 데이터베이스 수정 확인이 오류를 일으키지 않습니다.
이 변경을 통해 일관성을 희생합니다. 첫 번째 UPDATE
쿼리 후 애플리케이션 장애의 경우 두 번째 UPDATE
쿼리가 실행되지 않을 것입니다.
트랜잭션 블록 없이 동일한 코드는 다음과 같습니다:
ci_build.update!(updated_at: Time.current) # CI DB
ci_build.project.update!(updated_at: Time.current) # Main DB
비동기 처리
작업이 일관되게 완료되는 것을 더 보장해야 할 경우 백그라운드 작업 내에서 실행할 수 있습니다. 백그라운드 작업은 비동기적으로 예약되며 오류 발생 시 여러 차례 재시도됩니다. 일관성을 도입할 때에도 매우 작은 불일치 가능성이 여전히 있습니다.
예시:
current_time = Time.current
MyAsyncConsistencyJob.perform_async(cu_build.id)
ci_build.update!(updated_at: current_time)
ci_build.project.update!(updated_at: current_time)
MyAsyncConsistencyJob
은 타임스탬프를 업데이트하려고 시도합니다.
완벽한 일관성을 목표로
현재 우리에게는 하나의 데이터베이스와 유사한 일관성 특성을 보장하기 위한 도구(심지어 필요하지 않을 수도 있음)가 없습니다. 작업 중인 코드가 이러한 속성을 필요로 하는 경우, 테스트에서 크로스 데이터베이스 수정 확인을 비활성화하고 문제가 발생한 테스트 코드를 블록으로 묶고 추후 이슈를 만드는 방식으로 테스트를 수행할 수 있습니다.
allow_cross_database_modification_within_transaction(url: 'gitlab issue URL') do
ApplicationRecord.transaction do
ci_build.update!(updated_at: Time.current) # CI DB에서 업데이트
ci_build.project.update!(updated_at: Time.current) # Main DB에서 업데이트
end
end
도움이 필요하면 언제든 Pods 그룹에 문의하십시오.
데이터베이스 간 dependent: :nullify
및 dependent: :destroy
피하기
데이터베이스 간으로 dependent: :nullify
또는 dependent: :destroy
를 사용하려는 경우가 있을 수 있습니다. 기술적으로는 가능하지만 외부 트랜잭션에서 실행되는 이러한 후크는 교차 데이터베이스 트랜잭션을 만들어 문제가 발생할 수 있습니다. 이러한 방식으로 발생하는 크로스 데이터베이스 트랜잭션은 분해로 전환할 때 혼란스러운 결과를 낳을 수 있습니다. 이제 트랜잭션 외부에서 발생하는 일부 쿼리가 트랜잭션 실패의 영향을 받을 수 있으며 예상치 못한 버그를 유발할 수 있습니다.
데이터베이스 외부에서 데이터를 정리해야 하는 중요한 객체의 경우 (예: 오브젝트 스토리지
), dependent: :restrict_with_error
를 사용하는 것을 권장합니다. 이러한 객체는 명시적으로 미리 제거해야 합니다. dependent: :restrict_with_error
를 사용하면 부모 객체를 소멸시키지 못하게하여 PostgreSQL에서 자식 레코드 자체를 정리합니다.
PostgreSQL에서 자식 레코드를 정리해야만 하는 경우에는 느슨한 외래 키를 고려해보십시오.
데이터베이스 간 외래 키
우리는 두 개의 데이터베이스 간에 참조하는 외래 키를 많은 곳에서 사용합니다. 두 개의 별도의 PostgreSQL 데이터베이스로는 이를 수행할 수 없으므로 우리는 PostgreSQL에서 얻는 행위를 성능 있게 복제해야 합니다. PostgreSQL에서 제공하는 유효하지 않은 참조 생성을 방지하는 데이터 보장은 복제하지 않아야 하지만 이제 고아 데이터나 아무것도 가리키지 않는 레코드를 방지하기 위해 캐스케이딩 삭제를 대체할 방법이 필요합니다. 따라서 우리는 느슨한 외래 키를 만들었습니다. 이것은 고아 레코드를 비동기적으로 정리하는 프로세스입니다.
기존 데이터베이스 간 외래 키의 허용 디렉터리
큼직한 파이프라인에서 교차 데이터베이스 외래 키를 식별하는 가장 쉬운 방법은 실패한 파이프라인을 통해 확인될 수 있습니다.
예를 들어, !130038에서 notification_settings
테이블을 gitlab_main_cell
스키마로 이동시켰습니다. 이로 인해 notification_settings.user_id
는 users
로 연결되지만 users
테이블은 다른 데이터베이스에 속하므로 이제 이것은 크로스 데이터베이스 외래 키로 취급됩니다.
크로스 데이터베이스 외래 키가 발견되면 이를 허용 디렉터리에 추가하여 파이프라인이 성공하도록 해야 합니다.
이를 위해 동일한 스펙에 예외로 추가하여 파이프라인이 실패하지 않도록 해야 합니다. 나중에, 이 외래 키는 !130080에서 한 것처럼 느슨한 외래 키로 변환될 수 있습니다.
다중 데이터베이스 테스트
우리의 테스트 CI 파이프라인에서 기본적으로 GitLab을 여러 데이터베이스로 설정하여 테스트합니다.
main
및 ci
데이터베이스를 사용합니다. 하지만 예를 들어 Merge Request 시, 데이터베이스 관련 코드를 수정하거나 ~"pipeline:run-single-db"
라벨을 추가하는 경우와 같이,
우리의 테스트를 두 가지 다른 데이터베이스 모드에서 추가로 실행합니다:
single-db
및 single-db-ci-connection
.
테스트가 특정 데이터베이스 모드에서 실행되어야 하는 상황을 처리하기 위해 몇 가지 RSpec 도우미가 있습니다. 이를 통해 테스트가 실행될 수 있는 모드를 제한하고 다른 모드에서는 테스트를 건너뛸 수 있습니다.
도우미 이름 | 테스트 실행 |
---|---|
skip_if_shared_database(:ci)
| 여러 데이터베이스에서만 실행 |
skip_if_database_exists(:ci)
| single-db 및 single-db-ci-connection에서만 실행 |
skip_if_multiple_databases_are_setup(:ci)
| 오직 single-db에서 실행 |
skip_if_multiple_databases_not_setup(:ci)
| single-db-ci-connection 및 여러 데이터베이스에서만 실행 |
main_clusterwide
를 포함한 다중 데이터베이스 테스트
기본적으로 CI 파이프라인에서는 main_clusterwide
연결을 설정하지 않습니다. 그러나 ~"pipeline:run-clusterwide-db"
라벨을 추가하면 파이프라인은 main
, ci
및 main_clusterwide
와 같이 3개의 연결로 실행됩니다.
데이터베이스 스키마에 속하지 않는 테이블에 대한 쓰기 잠금
CI 데이터베이스가 홍보되고 두 데이터베이스가 완전히 분리된 경우, 분기된 두 상황을 방지하기 위해
Rake 작업 gitlab:db:lock_writes
를 실행합니다. 이 명령어는 다음 상태의 테이블에 대해 쓰기를 잠그고 트리거를 추가합니다:
- CI 데이터베이스의
gitlab_main
테이블 - 메인 데이터베이스의
gitlab_ci
테이블
이 작업을 GitLab 설정에서 하나의 데이터베이스만 사용하는 경우에는 어떤 테이블도 잠기지 않습니다.
이 작업을 실행한 경우, 해당 작업을 취소하려면 역방향으로 작동하는 Rake 작업 gitlab:db:unlock_writes
를 실행하세요.
모니터링
테이블 잠금 상태는 Database::MonitorLockedTablesWorker
를 사용하여 확인됩니다. 필요한 경우 테이블을 잠그게 됩니다.
이 스크립트의 결과는 Kibana에서 확인할 수 있습니다. 카운트가 0이 아닌 경우, 잠그지 않아야 하는 테이블이 있습니다. 필드 json.extra.database_monitor_locked_tables_worker.results.ci.tables_need_locks
및 json.extra.database_monitor_locked_tables_worker.results.main.tables_need_locks
는 잘못된 상태의 테이블 디렉터리을 포함해야 합니다.
로깅은 Elasticsearch Watcher를 통해 모니터링됩니다. 이 Watcher는 table_locks_needed
로 호출되며 소스 코드는 GitLab Runbook repository에 있습니다. 경고는 #g_tenant-scale Slack 채널로 전송됩니다.
자동화
두 개의 프로세스가 자동으로 테이블을 잠그고 있습니다:
- 데이터베이스 이주.
Gitlab::Database::MigrationHelpers::AutomaticLockWritesOnTables
참조 -
Database::MonitorLockedTablesWorker
는 필요한 경우 테이블을 잠금
매뉴얼으로 테이블 잠그기
매뉴얼으로 테이블을 잠글 필요가 있는 경우, 데이터베이스 이주를 사용하세요. 일반적인 마이그레이션을 작성하고 테이블을 잠그는 코드를 추가하세요. 예를 들어, CI 데이터베이스의 shards
테이블에 쓰기 잠금을 설정하려면 다음과 같이 하세요:
class EnableWriteLocksOnShards < Gitlab::Database::Migration[2.2]
def up
# 메인 데이터베이스에선 마이그레이션을 건너뛰어야 합니다
# DDL 마이그레이션에서 restrict_gitlab_migration 사용 불가능
return if Gitlab::Database.db_config_name(connection) != 'ci'
Gitlab::Database::LockWritesManager.new(
table_name: 'shards',
connection: connection,
database_name: :ci,
with_retries: false
).lock_writes
end
def down
# 아무것도 수행하지 않음
end
end
테이블 잘라내기
main
및 ci
데이터베이스가 완전히 분리된 경우, 테이블을 잘라내어 디스크 공간을 확보할 수 있습니다.
이로써 데이터 세트가 작아지게 됩니다. 예를 들어, CI 데이터베이스의 users
테이블의 데이터가 더 이상 읽히지 않으며 업데이트되지 않습니다. 따라서 이 데이터는 테이블을 잘라내어 제거할 수 있습니다.
이를 위해 GitLab은 각 데이터베이스에 대해 하나씩 두 가지 Rake 작업을 제공합니다:
-
gitlab:db:truncate_legacy_tables:main
은 메인 데이터베이스의 CI 테이블을 잘라냅니다. -
gitlab:db:truncate_legacy_tables:ci
은 CI 데이터베이스의 메인 테이블을 잘라냅니다.
DRY_RUN=true
를 사용합니다. 이를 통해 실제로 데이터가 잘려나가지 않는 것을 보장합니다.
GitLab은 DRY_RUN=true
없이 이러한 작업 중 어느 하나를 실행하기 전에 반드시 백업을 만들 것을 강력히 권장합니다.이러한 작업은 데이터를 실제로 변경하지 않고 수행하는 옵션이 포함되어 있습니다:
$ sudo DRY_RUN=true gitlab-rake gitlab:db:truncate_legacy_tables:main
I, [2023-07-14T17:08:06.665151 #92505] INFO -- : DRY RUN:
I, [2023-07-14T17:08:06.761586 #92505] INFO -- : 메인 데이터베이스에 대한 레거시 테이블을 잘라냅니다
[...]
작업은 먼저 잘라내어야 할 테이블을 찾습니다. 잘라내기는 데이터베이스 트랜잭션에서 제거하는 데이터의 양을 제한해야 하기 때문에 단계별로 진행됩니다. 테이블은 외래 키의 정의에 따라 특정 순서로 처리됩니다. 한 단계에 처리되는 테이블의 수는 작업을 호출할 때 숫자를 추가함으로써 변경할 수 있습니다. 기본값은 5입니다:
sudo DRY_RUN=true gitlab-rake gitlab:db:truncate_legacy_tables:main\[10\]
또한 UNTIL_TABLE
변수를 설정하여 잘라내야 하는 테이블의 수를 제한할 수 있습니다. 이 경우 ci_unit_test_failures
가 잘라내어질 때 프로세스가 중지됩니다:
sudo DRY_RUN=true UNTIL_TABLE=ci_unit_test_failures gitlab-rake gitlab:db:truncate_legacy_tables:main