- 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
)이 누락될 수도 있으나 유효한 Rails 데이터베이스입니다.
gitlab_pm
의 특수 목적
gitlab_pm
은 공개 저장소를 설명하는 패키지 메타데이터를 저장합니다. 이 데이터는 라이선스 컴플라이언스 및 의존성 스캔 제품 범주에 사용되며 구성 분석 그룹에서 유지보고 있습니다. 이는 앞으로 다른 데이터베이스로 경로를 설정하기 쉽게 하기 위해 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
테이블 간 조인 제거
데이터베이스 간에 조인하는 쿼리는 오류를 발생시킵니다. 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
가 조인 쿼리를 제거하는 방식을 변경하고 일부 사례를 평가하고 있는 경우에는 사용자에게 중요하지 않으므로 좋은 후보입니다. 또한 이러한 메트릭을 사용하는 사람이 없을 수도 있으므로 제거할 수 있을 것입니다.
includes
대신 preload
사용
Rails의 includes
및 preload
메서드는 둘 다 N+1 쿼리를 피하는 방법입니다. Rails의 includes
메서드는 다른 테이블에 조인해야 하는지 여부를 추측하는 방식으로 모든 레코드를 별도의 쿼리로 로드할 수 있는지를 결정합니다. 이 방법은 때로는 실제로 필요하지 않은 경우에도 조인을 실행하도록 가정하므로 때로는 잘못된 결정을 내리기도 합니다. 이 경우 별도의 쿼리에서 데이터를 명시적으로 로드하는 preload
를 사용하여 조인을 피할 수 있습니다. 이 방법은 여전히 N+1 쿼리를 피하면서 조인을 피할 수 있게 합니다.
해당 해결책이 사용된 실제 예제를 확인할 수 있습니다.
중복된 조인 제거
가끔은 쿼리가 과도하게(또는 중복된) 조인을 수행하는 경우가 있습니다.
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)
다른 공통 중복 조인 예제로, 불필요한 외래 키에 대한 완전한 조인이 있습니다.
대신 외래 키에 대해 필터링할 수 있었던 곳에서 기본 키로 필터링하는 경우가 있습니다. 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
를 사용하여 관련된 레코드를 얻을 수 있습니다:
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를 로드하고 그 후에 이를 PostgreSQL로 다시 질의하는 결과를 초래합니다. 이러한 경우는 확장되지 않으며 다른 옵션을 시도하는 것을 권장합니다. pluck
데이터에 limit
을 적용하여 메모리 크기에 제한을 둘 것 같지만 이는 사용자에게 예측할 수 없는 결과를 가져오고, 가장 큰 고객들 (우리를 포함하여)에게 가장 문제가 많이 발생하는 경우가 많아 이것을 권장하지 않습니다.
외래 키를 테이블에 역정규화하기
역정규화는 특정 쿼리를 단순화하거나 성능을 향상시키기 위해 테이블에 중복된 미리 계산된 데이터를 추가하는 것을 의미합니다. 이 경우, 중간 테이블을 통해 조인하는 경우 유용할 수 있습니다.
일반적으로 데이터베이스 스키마를 모델링할 때 “정규화”된 구조가 선호됩니다. 그 이유는 다음과 같습니다:
- 중복된 데이터가 추가 저장 공간을 사용합니다.
- 중복된 데이터를 동기화하는 데 필요합니다.
정규화된 데이터가 성능면에서 덜 효율적일 때가 있으므로 일부 시간 동안 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
라는 새 테이블을 도입하는 것입니다. 이 테이블에는 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
가 삭제되면 이를 업데이트해야 할 수도 있다는 점입니다. 그러나 도메인에 따라 삭제가 필요하지 않은 경우가 있을 수 있습니다. 또한 프로젝트가 목록 페이지에서 표시되는 것에 대한 사용자 영향이 문제가 되지 않거나 또는 불가능한 경우이거나 사용자가 이러한 행을 필요에 따라 삭제하는 로직을 구현할 수도 있습니다.
마지막으로, 이 역정규화와 새로운 쿼리는 더 적은 조인을 사용하고 더 적은 필터링이 필요하기 때문에 성능이 향상됩니다.
has_one
또는 has_many
through:
관계에 disable_joins
사용
가끔 다른 데이터베이스에 걸쳐있는 테이블을 통해 has_one ... through:
또는 has_many ... through:
를 사용함으로써 조인 쿼리가 발생할 수 있습니다. 이러한 조인은 때로는 disable_joins:true
를 추가함으로써 해결할 수 있습니다. 이는 Rail 기능인데 우리가 backport한 것입니다. 또한 disable_joins
를 사용하도록 활성화하는 람다 구문을 허용하는 기능 플래그로 확장했습니다. 이 기능을 사용하는 경우 중요한 시간이 적히도록 피처 플래그를 사용하는 것을 권장합니다.
이 접근 방법이 사용된 한 예는 https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66709/diffs에서 확인할 수 있습니다.
데이터베이스 쿼리에 대한 모든 변경 사항은 변경 전후의 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
그렇다면 다음 두 쿼리가 생성될 것입니다:
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 '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:
Unsupported cross-join across 'users, notification_settings' querying 'gitlab_main_clusterwide, gitlab_main_cell' discovered when executing query '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
경고: 현재 존재하는 ActiveRecord 관련성을 재정의하면 의도하지 않은 결과를 초래하거나 데이터 손실을 야기할 수 있습니다. 이러한 이유로 issue 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을 참조하세요.
교차 데이터베이스 트랜잭션 제거
여러 데이터베이스를 다룰 때 하나 이상의 데이터베이스를 영향을 주는 데이터 수정에 주의를 기울여야 합니다. Introduced 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
레코드는 다른 데이터베이스 서버에 있기 때문입니다.
더 많은 정보는
transaction guidelines
페이지를 참조하세요.
크로스 데이터베이스 트랜잭션 수정
데이터베이스 간의 트랜잭션은 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
# 네임스페이스와 루트/리디렉트_루트 간에 발생하는 크로스 데이터베이스 트랜잭션을 식별하고 무시하기 위해 Rails 콜백 내에서 사용합니다.
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에 대한 UPDATE
ci_build.project.update!(updated_at: Time.current) # Main DB에 대한 UPDATE
end
end
도움이 필요하다면 Pods 그룹 에 문의하세요.
dependent: :nullify
및 dependent: :destroy
의 크로스 데이터베이스 사용 방지
데이터베이스 간에 dependent: :nullify
또는 dependent: :destroy
를 사용하고 싶을 수 있습니다.
기술적으로 가능하지만 #destroy
호출로부터 외부 트랜잭션 컨텍스트에서 실행되는 이러한 후크는 문제가 발생할 수 있습니다.
이러한 방식으로 발생하는 크로스 데이터베이스 트랜잭션은 이제 분해된 상태로 전환하면 혼란스러운 결과를 초래할 수 있습니다. 외부 트랜잭션이 실패하는 동안 일부 쿼리가 일부만 적용되어 신기한 버그를 초래할 수 있습니다.
데이터베이스 외부에서 데이터를 정리해야 하는 중요한 객체의 경우 (예: 객체 저장소
), dependent: :restrict_with_error
설정을 사용하는 것을 권장합니다. 이러한 객체는 명시적으로 제거해야 합니다. dependent: :restrict_with_error
를 사용하면 부모 객체가 정리되지 않도록 금지됩니다.
PostgreSQL로부터 자식 레코드 자체를 정리해야 하는 경우, 느슨한 외래 키를 사용하는 것을 고려해보세요.
데이터베이스를 가로지르는 외래 키
두 데이터베이스 간에 참조하는 외래 키를 많이 사용합니다. 이는 두 개의 별도 PostgreSQL 데이터베이스로는 불가능하며, 우리는 효율적인 방식으로 PostgreSQL에서 얻는 동작을 복제해야 합니다. PostgreSQL에 의해 생성되는 부적합한 참조를 방지하는 데이터 보증을 복제할 필요는 없지만, 이제 고아 데이터가 생성되지 않도록 케스케이딩 삭제를 대체할 방법이 필요합니다. 따라서 우리는 “느슨한 외래 키”를 만들었습니다. 이는 부적절한 레코드를 정리하기 위한 비동기 프로세스입니다.
기존 크로스 데이터베이스 외래 키의 허용 목록
크로스 데이터베이스 외래 키를 식별하는 가장 쉬운 방법은 실패하는 파이프라인을 통해 식별하는 것입니다.
예를 들어, !130038에서 notification_settings
테이블을 gitlab_main_cell
스키마로 이동했습니다. 이를 db/docs/notification_settings.yml
파일에서 표시했습니다.
notification_settings.user_id
는 users
를 가리키는 열이지만 users
테이블은 다른 데이터베이스에 속하므로 이제 이것은 크로스 데이터베이스 외래 키로 간주됩니다.
이러한 경우에 대한 크로스 데이터베이스 외래 키를 캡처하는 특별한 사양이 있으며 no_cross_db_foreign_keys_spec.rb
에 실패하게 됩니다.
파이프라인을 성공시키기 위해 해당 크로스 데이터베이스 외래 키를 허용 목록에 추가해야 합니다.
이를 위해 동일한 사양에 예외로 추가하면 해당 사양은 실패하지 않습니다. 이후 이 외래 키는 !130080에서와 같이 느슨한 외래 키로 변환할 수 있습니다.
다중 데이터베이스 테스트
저희의 테스트 CI 파이프라인에서는 기본적으로 여러 데이터베이스를 설정하여 GitLab을 테스트합니다.
main
및 ci
데이터베이스를 모두 사용합니다. 그러나 MR(병합 요청)에서는 데이터베이스 관련 코드를 수정하거나 MR에 ~"pipeline:run-single-db"
레이블을 추가하는 경우에는
two other database modes
single-db
및 single-db-ci-connection
에서 추가로 테스트를 실행합니다.
저희는 특정 데이터베이스 모드에서 테스트가 실행되어야 하는 상황을 다루기 위해 몇 가지 RSpec 도우미를 가지고 있습니다. 테스트가 실행될 수 있는 모드를 제한하고 그 밖의 모드에서는 테스트를 건너뜁니다.
도우미 이름 | 테스트 실행 위치 |
---|---|
skip_if_shared_database(:ci)
| multiple databases에서만 |
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 및 multiple databases에서만 |
데이터베이스 스키마에 속하지 않는 테이블에 대한 쓰기 잠금
CI 데이터베이스가 확정되고 두 데이터베이스가 완전히 분리될 때, 분기 상황을 방지하기 위해 추가 보호장치로 gitlab:db:lock_writes
Rake 작업을 실행합니다. 이 명령은 다음 테이블에 대한 쓰기를 잠금 처리합니다.
- CI 데이터베이스의
gitlab_main
테이블. - 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 repository에 있습니다.
이 경고는 #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
# Main 데이터베이스에서 마이그레이션은 건너뛰어야 합니다
# 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 -- : Truncating legacy tables for the database 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