- GitLab 스키마
- 마이그레이션
- CI/CD 데이터베이스
- 데이터베이스 간의 외래 키
- 여러 데이터베이스에 대한 테스트
- 데이터베이스 스키마에 속하지 않는 테이블의 쓰기 잠금
- 테이블 자르기
여러 데이터베이스
GitLab이 더 확장될 수 있도록 GitLab 애플리케이션 데이터베이스를 여러 데이터베이스로 분해했습니다.
주요 데이터베이스는 main
, ci
, 그리고 (선택적으로) sec
입니다.
GitLab은 하나, 두 개 또는 세 개의 데이터베이스로 실행될 수 있도록 지원합니다.
GitLab.com에서는 별도의 main
및 ci
데이터베이스를 사용하고 있습니다.
Cells 아키텍처를 구축하기 위해 데이터베이스를 더 분해하여 또 다른 데이터베이스인 gitlab_main_clusterwide
를 도입하고 있습니다.
GitLab 스키마
서로 다른 데이터베이스 간의 허용된 패턴을 제대로 발견하기 위해
GitLab 애플리케이션은 데이터베이스 사전을 구현합니다.
데이터베이스 사전은 테이블을 gitlab_schema
로 가상 분류하여 제공합니다.
이는 개념적으로 PostgreSQL 스키마와 유사합니다.
우리는 CI 분해 기능을 더 잘 고립하기 위해 데이터베이스 스키마를 사용하기로 결정했습니다
복잡한 마이그레이션 절차 때문에 PostgreSQL 스키마를 사용할 수 없다고 판단했습니다.
대신 애플리케이션 수준 분류 개념을 구현했습니다.
GitLab의 각 테이블은 gitlab_schema
가 할당되어야 합니다:
데이터베이스 | 설명 | 비고 |
---|---|---|
gitlab_main |
main: 데이터베이스에 저장된 모든 테이블. |
현재, 이는 Cells 아키텍처 구축을 위해 gitlab_main_cell 로 대체되고 있습니다. gitlab_main_cell 스키마는 GitLab 설치의 셀에 로컬인 모든 테이블을 설명합니다. 예를 들어, projects 및 groups
|
gitlab_main_clusterwide |
모든 행이나 행의 하위 집합이 클러스터 전반에 걸쳐 존재해야 하는 모든 테이블, Cells 아키텍처에서. 예를 들어, users 및 application_settings . |
Cells 1.0 아키텍처에서 실제 클러스터 전반에 걸친 테이블은 없습니다. 각 셀은 자신의 데이터베이스를 가질 것입니다. 실제로 이러한 테이블은 여전히 각 셀에 로컬로 저장됩니다. |
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_sec 로 대체될 예정입니다. |
gitlab_sec |
sec: 데이터베이스에 저장될 모든 보안 및 취약점 기능 테이블 |
분해 진행 중 |
추가로 분해된 데이터베이스와 함께 도입될 더 많은 스키마
스키마 사용은 기본 클래스를 사용하도록 강제합니다:
-
gitlab_main
/gitlab_main_cell
의 경우ApplicationRecord
-
gitlab_ci
의 경우Ci::ApplicationRecord
-
gitlab_geo
의 경우Geo::TrackingBase
-
gitlab_shared
의 경우Gitlab::Database::SharedModel
-
gitlab_pm
의 경우PackageMetadata::ApplicationRecord
-
gitlab_sec
의 경우Gitlab::Database::SecApplicationRecord
gitlab_main_cell
또는 gitlab_main_clusterwide
스키마 중 선택
이 내용은 새로운 위치로 이동했습니다.
모든 셀 로컬 테이블에 대한 샤딩 키 정의
이 내용은 새로운 위치로 이동했습니다.
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
은 Rails가 정의한 테이블(예: schema_migrations
또는 ar_internal_metadata
)과 내부 PostgreSQL 테이블(예: pg_attribute
)을 설명합니다. 그 주요 목적은
다른 데이터베이스를 지원하는 것입니다.
이 데이터베이스는 일부 애플리케이션 정의 gitlab_shared
테이블(예: loose_foreign_keys_deleted_records
)이 누락된 Geo와 같은 유효한 Rails 데이터베이스일 수 있습니다.
gitlab_pm
의 특별한 목적
gitlab_pm
은 공개 리포지토리를 설명하는 패키지 메타데이터를 저장합니다. 이 데이터는 License Compliance 및 Dependency Scanning 제품 카테고리에 사용되며, Composition Analysis Group에 의해 유지 관리됩니다. 이는 나중에 다른 데이터베이스로 라우팅하기 쉽게 하기 위해 gitlab_main
의 별칭입니다.
마이그레이션
여러 데이터베이스에 대한 마이그레이션을 읽으세요.
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
테이블 간의 조인 제거
데이터베이스 간 조인 쿼리는 오류를 발생시킵니다. 도입됨
GitLab 14.3에서, 새로운 쿼리에 대해서만 해당됩니다. 기존의 쿼리는 오류를 발생시키지 않습니다.
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에 있는 조인 쿼리를 제거하는 것입니다.
이는 좋은 평가 후보입니다, 왜냐하면 UsageData
는 사용자에게 중요하지 않으며
비슷한 유용한 메트릭을 더 간단한 접근 방식으로 얻을 수 있을 수도 있습니다.
대안으로, 우리는 아무도 이러한 메트릭을 사용하지 않는 것을 발견할 수도 있으므로, 이를 제거할 수 있습니다.
includes
대신 preload
사용
Rails의 includes
및 preload
메서드는 모두 N+1 쿼리를 피하는 방법입니다.
Rails의 includes
메서드는 조인해야 할지 또는
별도의 쿼리에서 모든 레코드를 로드할 수 있을지에 대한 휴리스틱 접근 방식을 사용합니다.
이 메서드는 다른 테이블의 열을 쿼리해야 할 경우 조인이 필요하다고 가정하지만,
가끔 이 메서드는 잘못 판단하여 필요하지 않을 때도 조인을 실행합니다.
이 경우, 별도의 쿼리에서 데이터를 명시적으로 로드하기 위해 preload
를 사용하면
조인을 피할 수 있으면서도 N+1 쿼리를 여전히 피할 수 있습니다.
이 해결책이 사용된 실제 사례를
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67655에서 볼 수 있습니다.
중복 조인 제거
경우에 따라 쿼리가 과도한(또는 중복) 조인을 수행하는 경우도 있습니다.
일반적인 예는 쿼리가 A
에서 C
로 이동하면서, 양쪽에 외래 키가 있는
어떤 테이블 B
를 통한 경우입니다.
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)
또 다른 일반적인 중복 조인은
다른 테이블까지 조인한 후
기본 키로 필터링할 때 외래 키로 필터링할 수 있었던 경우입니다.
MR 71614에서
이전 코드는 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(또는 사용자 이름) 목록을 다른 동일한 크기의 목록에 매핑해야 하는 경우에 이 패턴이 유용합니다. 일대다 관계에서 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 에서 확인할 수 있습니다.
때때로 join을 pluck
으로 변환하는 것이 쉬워 보일 수 있지만, 종종 이는 메모리에 무제한의 id를 로드한 후 PostgreSQL로 다시 직렬화되는 결과를 초래합니다. 이러한 경우는 확장성이 좋지 않으며, 우리는 다른 옵션 중 하나를 사용해 보기를 권장합니다. plucked 데이터에 limit
를 적용하여 메모리를 제한하는 것이 좋은 생각처럼 보일 수 있지만, 이는 사용자에게 예측할 수 없는 결과를 초래하고 종종 가장 큰 고객(우리 자신을 포함하여)에게 가장 문제가 될 수 있으므로 이에 대해서는 권장하지 않습니다.
일부 외래 키를 테이블에 비정규화하기
비정규화는 특정 쿼리를 단순화하거나 성능을 개선하기 위해 테이블에 중복된 미리 계산된(복제된) 데이터를 추가하는 것을 의미합니다. 이 경우, 세 개의 테이블을 포함하는 join을 수행할 때, 중간 테이블을 통해 join하는 경우에 유용할 수 있습니다.
일반적으로 데이터베이스 스키마를 모델링할 때 “정규화된” 구조가 선호되는 이유는 다음과 같습니다:
- 중복 데이터는 추가 저장 공간을 사용합니다.
- 중복 데이터는 동기화를 유지해야 합니다.
때때로 정규화된 데이터는 성능이 낮을 수 있으므로 비정규화는 GitLab에서 데이터베이스 쿼리 성능을 개선하기 위해 오랫동안 사용해온 일반적인 기법입니다. 위의 문제는 다음 조건이 충족될 때 완화됩니다:
- 데이터가 많지 않다(예: 정수 열만 있음).
- 데이터가 자주 업데이트되지 않는다(예:
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
도 비정규화합니다.
추가 테이블로 비정규화
때때로 이전 비정규화(추가 열 추가)가 특정 케이스에 대해 작동하지 않을 수 있습니다.
이는 데이터가 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
라는 새 테이블을 도입하는 것입니다.
이 테이블에는 2개의 열 project_id
와 ci_feature
가 있습니다.
이 테이블은 프로젝트가 코드 커버리지에 대한 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
가 삭제되면 업데이트(제거)해야 할 수도 있다는 것입니다.
그러나 도메인에 따라 삭제가 엣지 케이스이거나 불가능할 수 있으며, 목록 페이지에서 프로젝트가 표시되는 것에 대한 사용자 영향이 문제가 되지 않을 수 있습니다.
또한 필요에 따라 이러한 행을 삭제하는 로직을 구현하는 것이 가능합니다.
마지막으로, 이 비정규화 및 새로운 쿼리는 조인을 줄이고 필터링을 덜 필요로 하여 성능을 향상시킵니다.
has_one
또는 has_many
through:
관계에 대한 disable_joins
사용
때때로 조인 쿼리는 has_one ... through:
또는 has_many ... through:
를 사용하여 서로 다른 데이터베이스에 걸쳐 테이블을 사용하는 데 의해 발생합니다.
이러한 조인은 때때로 disable_joins:true
를 추가하여 해결할 수 있습니다.
이것은 Rails 기능이며, 우리는 이 기능을 이식했습니다.
또한, 기능 플래그를 사용하여 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
end
다음 두 쿼리를 얻게 됩니다:
select id from pipelines where project_id = 5;
select * from builds where pipeline_id in (...)
order by created_at desc
limit 10;
첫 번째 쿼리가 고유한 열로 제한되지 않거나 LIMIT
절이 없기 때문에, 무제한의 파이프라인 ID를 메모리에 로드할 수 있습니다.
그 이후 쿼리에 전달되면 Rails 애플리케이션과 데이터베이스에서 매우 낮은 성능을 초래할 수 있습니다.
이런 경우에는 쿼리를 다시 작성하거나 교차 조인을 제거하기 위해 위에서 설명한 다른 패턴을 살펴봐야 할 수 있습니다.
교차 조인을 올바르게 제거했는지 검증하는 방법
RSpec는 모든 SQL 쿼리가 데이터베이스 간에 조인하지 않도록 자동으로 검증되도록 구성되어 있습니다.
이 검증이 spec/support/database/cross-join-allowlist.yml
에서 비활성화된 경우에도 with_cross_joins_prevented
를 사용하여 격리된 코드 블록을 검증할 수 있습니다.
이 메소드를 다음과 같이 사용할 수 있습니다:
it '데이터베이스 간에 조인하지 않습니다' do
with_cross_joins_prevented do
::Ci::Build.joins(:project).to_a
end
end
이 쿼리가 두 데이터베이스 간에 조인하면 예외가 발생합니다.
이전 예제는 조인을 제거하면 다음과 같이 수정됩니다:
it '데이터베이스 간에 조인하지 않습니다' 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
스키마로 이동했습니다.
다음과 같은 오류로 파이프라인이 실패했습니다:
Database::PreventCrossJoins::CrossJoinAcrossUnsupportedTablesError:
'users, notification_settings' 간에 지원되지 않는 교차 조인입니다. 쿼리 'gitlab_main_clusterwide, gitlab_main_cell'을 executing 할 때 발견되었습니다.
쿼리 '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
로 표시하는 방법이 있습니다.
이 메소드는 다음의 경우에만 사용해야 합니다:
-
기존 코드에 대해.
-
교차 조인을 제거하기 위해 필요할 경우. 예를 들어 교차 조인을 제거하기 위해 향후 사용을 위해 데이터를 백필하는 마이그레이션에서.
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_diff_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
경고: 관계를 재정의하면 의도하지 않은 결과를 초래할 수 있으며, 문제 424307에서 확인한 것처럼 데이터 손실로 이어질 수 있습니다. 교차 조인을 허용하도록 기존 ActiveRecord 관계를 재정의하지 마십시오. 아래 예제와 같이.
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를 참조하세요.
크로스 데이터베이스 거래 제거하기
여러 데이터베이스를 다룰 때는 한 데이터베이스 이상에 영향을 미치는 데이터 수정에 주의를 기울이는 것이 중요합니다.
GitLab 14.4에서 도입됨, 자동화된 검사가 크로스 데이터베이스 수정을 방지합니다.
어느 데이터베이스 서버에서 시작된 거래 중에 최소 두 개의 서로 다른 데이터베이스가 수정되면, 애플리케이션은 크로스 데이터베이스 수정 오류를 발생시킵니다(테스트 환경에서만).
예제:
# Main DB에서 거래 열기
ApplicationRecord.transaction do
ci_build.update!(updated_at: Time.current) # CI DB에서 UPDATE
ci_build.project.update!(updated_at: Time.current) # Main DB에서 UPDATE
end
# 오류 발생: 'main, ci'의 크로스 데이터베이스 데이터 수정이
# 'ci_build, projects' 테이블을 수정하는 거래 내에서 발견됨
위의 코드 예제는 거래 내에서 두 개의 기록에 대한 타임스탬프를 업데이트합니다. 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
# members와 users/user_details/user_preferences 관련 크로스 데이터베이스 거래를 표시하고 무시하기 위해
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
# Rails 콜백 내에서 namespaces와 routes/redirect_routes 관련 크로스 데이터베이스 거래를 표시하고 무시하기 위해.
cross_database_ignore_tables %w[routes redirect_routes], url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/424277'
end
거래 블록 제거하기
열린 거래가 없으면 크로스 데이터베이스 수정 검사가 오류를 발생시킬 수 없습니다. 이러한 변경을 통해 일관성을 희생하게 됩니다. 첫 번째 UPDATE
쿼리 후 애플리케이션 실패가 발생하면 두 번째 UPDATE
쿼리는 실행되지 않습니다.
transaction
블록 없는 동일한 코드:
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
를 사용해야 하는 경우가 있을 수 있습니다.
기술적으로 이는 가능하지만, #destroy
호출에서 외부 트랜잭션의 맥락에서 이러한 후크가 실행되기 때문에 문제가 발생합니다. 우리가 피하려고 하는 교차 데이터베이스 트랜잭션을 생성합니다.
이러한 방식으로 생성된 교차 데이터베이스 트랜잭션은 우리가 분해되는 것으로 전환할 때 혼란스러운 결과를 초래할 수 있습니다. 이제 외부 트랜잭션이 실패하는 동안 일부 쿼리가 트랜잭션 외부에서 발생할 수 있으며, 이는 부분적으로 적용될 수 있습니다.
이로 인해 놀라운 버그가 발생할 수 있습니다.
데이터베이스 외부에서 데이터를 정리해야 하는 비단순 객체(예: 객체 저장소)의 경우,
dependent: :restrict_with_error
설정을 권장합니다.
이러한 객체는 사전에 명시적으로 제거해야 합니다. dependent: :restrict_with_error
를 사용하면 정리가 되지 않은 경우 부모 객체의 파괴를 금지합니다.
PostgreSQL에서 하위 레코드만 정리하면 되는 경우에는 loose foreign keys 사용을 고려하세요.
데이터베이스 간의 외래 키
두 개의 데이터베이스를 가로지르는 외래 키를 사용하는 곳이 많이 있습니다.
이는 두 개의 개별 PostgreSQL 데이터베이스로는 불가능하므로, PostgreSQL에서 얻는 동작을 성능적으로 복제해야 합니다.
우리는 PostgreSQL에서 제공하는 데이터 보증을 복제하려고 하지 않지만,
오래된 데이터나 아무데도 포인터가 없는 레코드를 남기지 않도록 하기 위해서 연쇄 삭제를 대체할 방법이 필요합니다.
그런 의미에서 우리는 고아 레코드를 정리하는 비동기 프로세스로 “loose foreign keys”를 만들었습니다.
기존 교차 데이터베이스 외래 키를 위한 허용 목록
교차 데이터베이스 외래 키를 식별하는 가장 쉬운 방법은 실패하는 파이프라인을 통해 확인하는 것입니다.
예를 들어, !130038에서는 notification_settings
테이블을 gitlab_main_cell
스키마로 이동했습니다.
notification_settings.user_id
는 users
를 가리키는 열이지만, users
테이블은 다른 데이터베이스에 속하여 이제 이는 교차 데이터베이스 외래 키로 처리됩니다.
no_cross_db_foreign_keys_spec.rb
에서 교차 데이터베이스 외래 키의 이러한 사례를 포착하는 스펙이 있으며, 이는 그러한 교차 데이터베이스 외래 키가 발견되면 실패합니다.
파이프라인을 성공적으로 실행하려면, 이 교차 데이터베이스 외래 키는 허용 목록에 추가되어야 합니다.
이를 위해, 같은 스펙에서 예외로 추가하여 기존의 교차 데이터베이스 외래 키가 존재하도록 명시적으로 허용합니다(이 예제와 같이).
이렇게 하면 스펙이 실패하지 않습니다.
이후, 이 외래 키는 !130080에서 했던 것처럼 loose foreign key로 변환될 수 있습니다.
여러 데이터베이스에 대한 테스트
우리의 테스트 CI 파이프라인에서는 기본적으로 main
및 ci
데이터베이스를 사용하여 GitLab을 여러 데이터베이스로 테스트합니다. 그러나 예를 들어 데이터베이스 관련 코드를 수정하거나 MR에 ~"pipeline:run-single-db"
레이블을 추가할 때와 같이, 우리는
two other database modes에서 추가로 테스트를 실행합니다: 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 및 여러 데이터베이스에서 |
데이터베이스 스키마에 속하지 않는 테이블의 쓰기 잠금
CI 데이터베이스가 승격되고 두 데이터베이스가 완전히 분리되었을 때, 분할 뇌 상황을 방지하기 위한 추가 안전 장치로, Rake 작업 gitlab:db:lock_writes
를 실행합니다. 이 명령은 다음에 대한 쓰기를 잠급니다:
- CI 데이터베이스의
gitlab_main
테이블. - 메인 데이터베이스의
gitlab_ci
테이블.
이 Rake 작업은 모든 테이블에 트리거를 추가하여, 잠금이 필요한 테이블에 대해 INSERT
, UPDATE
, DELETE
, 또는 TRUNCATE
문이 실행되지 않도록 합니다.
이 작업이 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를 사용하여 모니터링됩니다.
이 감시자는 table_locks_needed
라고 불리며 소스 코드는
GitLab Runbook 저장소에 있습니다.
알림은 #g_tenant-scale Slack 채널로 전송됩니다.
자동화
테이블을 자동으로 잠그는 두 가지 프로세스가 있습니다:
- 데이터베이스 마이그레이션.
Gitlab::Database::MigrationHelpers::AutomaticLockWritesOnTables
를 참조하세요. -
Database::MonitorLockedTablesWorker
가 필요 시 테이블을 잠급니다. 이는lock_tables_in_monitoring
기능 플래그로 비활성화할 수 있습니다.
수동으로 테이블 잠그기
테이블을 수동으로 잠궈야 하는 경우 데이터베이스 마이그레이션을 사용합니다.
정기적인 마이그레이션을 생성하고 테이블 잠금을 위한 코드를 추가합니다.
예를 들어, 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
# no-op
end
end
테이블 자르기
main
데이터베이스와 ci
데이터베이스가 완전히 분리되면 테이블을 자름으로써 디스크 공간을 확보할 수 있습니다.
이렇게 하면 데이터 세트가 작아집니다: 예를 들어, CI 데이터베이스의 users
테이블에 있는 데이터는 더 이상 읽히지 않으며 더 이상 업데이트되지 않습니다.
따라서 이 데이터는 테이블을 잘라서 제거할 수 있습니다.
이 목적을 위해 GitLab은 각 데이터베이스에 대해 하나씩 두 개의 Rake 작업을 제공합니다:
-
gitlab:db:truncate_legacy_tables:main
은 Main 데이터베이스의 CI 테이블을 자릅니다. -
gitlab:db:truncate_legacy_tables:ci
는 CI 데이터베이스의 Main 테이블을 자릅니다.
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 -- : Main 데이터베이스에 대한 레거시 테이블 자르기
I, [2023-07-14T17:08:06.761709 #92505] INFO -- : SELECT set_config('lock_writes.ci_build_needs', 'false', false)
I, [2023-07-14T17:08:06.765272 #92505] INFO -- : SELECT set_config('lock_writes.ci_build_pending_states', 'false', false)
I, [2023-07-14T17:08:06.768220 #92505] INFO -- : SELECT set_config('lock_writes.ci_build_report_results', 'false', false)
[...]
I, [2023-07-14T17:08:06.957294 #92505] INFO -- : TRUNCATE TABLE ci_build_needs, ci_build_pending_states, ci_build_report_results, ci_build_trace_chunks, ci_build_trace_metadata, ci_builds, ci_builds_metadata, ci_builds_runner_session, ci_cost_settings, ci_daily_build_group_report_results, ci_deleted_objects, ci_freeze_periods, ci_group_variables, ci_instance_variables, ci_job_artifact_states, ci_job_artifacts, ci_job_token_project_scope_links, ci_job_variables, ci_minutes_additional_packs, ci_namespace_mirrors, ci_namespace_monthly_usages, ci_partitions, ci_pending_builds, ci_pipeline_artifacts, ci_pipeline_chat_data, ci_pipeline_messages, ci_pipeline_metadata, ci_pipeline_schedule_variables, ci_pipeline_schedules, ci_pipeline_variables, ci_pipelines, ci_pipelines_config, ci_platform_metrics, ci_project_mirrors, ci_project_monthly_usages, ci_refs, ci_resource_groups, ci_resources, ci_runner_machines, ci_runner_namespaces, ci_runner_projects, ci_runner_versions, ci_runners, ci_running_builds, ci_secure_file_states, ci_secure_files, ci_sources_pipelines, ci_sources_projects, ci_stages, ci_subscriptions_projects, ci_trigger_requests, ci_triggers, ci_unit_test_failures, ci_unit_tests, ci_variables, external_pull_requests, p_ci_builds, p_ci_builds_metadata, p_ci_job_annotations, p_ci_runner_machine_builds, taggings, tags RESTRICT
작업은 먼저 자르기 필요한 테이블을 찾습니다.
자르기는 한 데이터베이스 트랜잭션에서 제거되는 데이터의 양을 제한해야 하므로 단계적으로 진행됩니다.
테이블은 외래 키 정의에 따라 특정 순서로 처리됩니다.
한 단계에서 처리되는 테이블 수는 작업을 호출할 때 숫자를 추가하여 변경할 수 있습니다. 기본값은 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