데이터베이스 사례 연구: 네임스페이스 리포지터리 통계
소개
그룹을 위한 리포지터리 및 제한 관리에서는 그룹이 사용한 리포지터리 용량을 쉽게 확인하고 쉽게 관리할 수 있는 방법을 제공하고자 합니다.
제안
- 새로운 ActiveRecord 모델을 만들어 네임스페이스의 통계를 집계 형태로 저장합니다(루트 네임스페이스에 대해서만).
- 이 모델의 통계를 해당 네임스페이스에 속한 프로젝트가 변경될 때마다 업데이트합니다.
문제
GitLab에서는 프로젝트 리포지터리 통계를 프로젝트를 저장할 때마다 콜백을 통해 업데이트합니다.
네임스페이스 단위의 통계 요약은 Namespaces#with_statistics
범위로 검색됩니다. 이 쿼리를 분석한 결과:
- 15,000개가 넘는 프로젝트를 가진 네임스페이스에서 최대
1.2
초가 소요됩니다. - ChatOps로 분석할 수 없을 정도로 시간이 초과됩니다.
추가로, 현재 프로젝트 통계를 업데이트하는 데 사용 중인 패턴(콜백)이 적절하게 확장되지 않습니다. 현재 이것은 프로덕션 환경에서 가장 시간이 많이 소요되는 데이터베이스 쿼리 트랜잭션 중 하나로, 이것을 길게 만들면 더 이상의 쿼리를 추가할 수 없습니다.
위의 모든 이유로, 네임스페이스 통계를 저장하고 업데이트하는 데 동일한 패턴을 적용할 수 없습니다. 왜냐하면 네임스페이스
테이블은 GitLab.com에서 가장 큰 테이블 중 하나이기 때문입니다. 따라서 성능이 좋고 대체 방법을 찾아야 했습니다.
시도
시도 A: PostgreSQL 재집계 뷰
모델은 프로젝트 경로 SQL을 기반으로 갱신 전략으로 업데이트될 수 있으며 재집계 뷰를 사용합니다:
SELECT split_part("rs".path, '/', 1) as root_path,
COALESCE(SUM(ps.storage_size), 0) AS storage_size,
COALESCE(SUM(ps.repository_size), 0) AS repository_size,
COALESCE(SUM(ps.wiki_size), 0) AS wiki_size,
COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size,
COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size,
COALESCE(SUM(ps.pipeline_artifacts_size), 0) AS pipeline_artifacts_size,
COALESCE(SUM(ps.packages_size), 0) AS packages_size,
COALESCE(SUM(ps.snippets_size), 0) AS snippets_size,
COALESCE(SUM(ps.uploads_size), 0) AS uploads_size
FROM "projects"
INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'
INNER JOIN project_statistics ps ON ps.project_id = projects.id
GROUP BY root_path
그런 다음 다음과 같이 쿼리를 실행할 수 있습니다.:
REFRESH MATERIALIZED VIEW root_namespace_storage_statistics;
이것은 단일 쿼리로 업데이트될 수 있지만 아래와 같은 단점이 있습니다:
- 재집계 뷰 구문은 PostgreSQL과 MySQL에서 다릅니다. 이 기능은 작업되었지만, MySQL은 여전히 GitLab에서 지원됩니다.
- Rails는 재집계 뷰를 네이티브로 지원하지 않습니다. 데이터베이스 뷰의 관리를 담당하기 위해 전문화된 젬을 사용해야 하며, 이는 추가 작업을 의미합니다.
시도 B: CTE를 통한 업데이트
시도 A와 유사하며, 공통 테이블 식를 사용하여 모델 업데이트를 갱신 전략으로 수행합니다.
WITH refresh AS (
SELECT split_part("rs".path, '/', 1) as root_path,
COALESCE(SUM(ps.storage_size), 0) AS storage_size,
COALESCE(SUM(ps.repository_size), 0) AS repository_size,
COALESCE(SUM(ps.wiki_size), 0) AS wiki_size,
COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size,
COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size,
COALESCE(SUM(ps.pipeline_artifacts_size), 0) AS pipeline_artifacts_size,
COALESCE(SUM(ps.packages_size), 0) AS packages_size,
COALESCE(SUM(ps.snippets_size), 0) AS snippets_size,
COALESCE(SUM(ps.uploads_size), 0) AS uploads_size
FROM "projects"
INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'
INNER JOIN project_statistics ps ON ps.project_id = projects.id
GROUP BY root_path)
UPDATE namespace_storage_statistics
SET storage_size = refresh.storage_size,
repository_size = refresh.repository_size,
wiki_size = refresh.wiki_size,
lfs_objects_size = refresh.lfs_objects_size,
build_artifacts_size = refresh.build_artifacts_size,
pipeline_artifacts_size = refresh.pipeline_artifacts_size,
packages_size = refresh.packages_size,
snippets_size = refresh.snippets_size,
uploads_size = refresh.uploads_size
FROM refresh
INNER JOIN routes rs ON rs.path = refresh.root_path AND rs.source_type = 'Namespace'
WHERE namespace_storage_statistics.namespace_id = rs.source_id
시도 A와 동일한 이점과 단점이 있습니다.
시도 C: 모델을 제거하고 통계를 Redis에 저장
집계 형태로 통계를 저장하는 모델을 제거하고 대신 Redis Set을 사용할 수 있습니다. 이것은 이미 GitLab 아키텍처의 일부로 포함되어 있기 때문에 구현하는 데 가장 빠르고 지루한 해결책입니다.
이 접근 방식의 단점은 Redis가 PostgreSQL과 동일한 지속성/일관성 보증을 제공하지 않는다는 것이며, Redis 장애에서 이 정보를 잃을 수 없는 정보입니다.
시도 D: 루트 네임스페이스 및 하위 네임스페이스 태그 지정
루트 네임스페이스를 그의 하위 네임스페이스와 직접 관련시켜서, 부모가 없이 생성된 네임스페이스는 루트 네임스페이스 ID로 태그가 지정됩니다.
ID | 루트 ID | 부모 ID |
---|---|---|
1 | 1 | NULL |
2 | 1 | 1 |
3 | 1 | 2 |
네임스페이스 내에서 통계를 집계하기 위해 다음과 같이 실행할 수 있습니다.
SELECT COUNT(...)
FROM projects
WHERE namespace_id IN (
SELECT id
FROM namespaces
WHERE root_id = X
)
이러한 접근 방식은 집계를 훨씬 쉽게 만들지만, 주요 단점이 몇 가지 있습니다:
- 모든 네임스페이스를 이동하려면 새로운 열을 추가하고 채워야 합니다. 테이블의 크기 때문에 시간/비용이 상당히 많이 들 것입니다. 백그라운드 마이그레이션에는 약 153시간이 소요될 것으로 예상됩니다. https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/29772를 참조하세요.
- 백그라운드 마이그레이션은 다른 마일스톤까지 기능을 지연시킬 수 있도록 이전 릴리스에서 배포되어야 합니다.
시도 E (최종): 네임스페이스 리포지터리 통계 비동기적으로 업데이트
이 접근 방식은 이미 있는 증분 통계 업데이트를 계속 사용하는 것으로, 그러나 이를 Sidekiq 작업을 통해 새롭게 갱신합니다. 또한 다른 트랜잭션과 별도로 다음과 같이 증분 통계 업데이트를 수행합니다:
- 두 개의 열
id
와namespace_id
를 가진 두 번째 테이블(namespace_aggregation_schedules
)을 생성합니다. - 프로젝트의 통계가 변경될 때마다
namespace_aggregation_schedules
에 행을 삽입합니다.- 루트 네임스페이스와 이미 관련된 행이 있는 경우 새로운 행을 삽입하지 않습니다.
-
project_statistics
의 업데이트를 포함하는 트랜잭션의 길이를 고려해야 하므로, 삽입은 다른 트랜잭션 및 Sidekiq Job을 통해 수행되어야 합니다.
- 행을 삽입한 후, 다음과 같이 두 가지 다른 시점에 대해 비동기적으로 실행할 수 있도록 다른 Worker를 예약합니다:
- 즉시 실행을 위해 대기열에 넣고, 1.5시간 후에 예약합니다.
- 루트 네임스페이스 ID를 기반으로 한 Redis의 키에 대해
1.5시간
동안의 임대를 얻을 수 있다면 작업을 예약합니다. - 임대를 얻을 수 없는 경우, 이미 다른 집계가 진행 중이거나
1.5시간
이내에 예약된 것을 나타냅니다.
- 이 Worker는 다음을 실행합니다:
- 서비스를 통해 모든 네임스페이스를 조회하여 루트 네임스페이스의 리포지터리 통계를 업데이트합니다.
- 업데이트 후에 관련된
namespace_aggregation_schedules
를 삭제합니다.
- 남아있는 행을 살펴보고 각 대기 중인 행에 대해 작업을 예약하는 또 다른 Sidekiq 작업이 포함됩니다.
- 이 작업은 매일 밤(UTC)에 cron으로 예약되어 실행됩니다.
이 구현은 다음과 같은 이점을 가지고 있습니다:
- 모든 업데이트는 비동기적으로 수행되므로
project_statistics
의 트랜잭션 길이가 증가하지 않습니다. - 업데이트를 단일 SQL 쿼리로 수행합니다.
- PostgreSQL 및 MySQL과 호환됩니다.
- 백그라운드 마이그레이션이 필요하지 않습니다.
이 접근 방식의 유일한 단점은 네임스페이스의 통계가 변경된 후 최대 1.5
시간까지 업데이트되므로 통계가 정확하지 않을 수 있는 시간적 윈도우가 있다는 것입니다. 여전히 리포지터리 한도를 강제하지 않고 있기 때문에 이것은 주요 문제가 아닙니다.
결론
네임스페이스의 리포지터리 통계를 비동기적으로 업데이트하는 것은 루트 네임스페이스를 집계하는 가장 문제가 적고 성능이 우수한 방법이었습니다.
이 사용 사례에 대한 모든 세부 정보는 다음에서 확인할 수 있습니다:
- https://gitlab.com/gitlab-org/gitlab-foss/-/issues/62214
- 구현된 Merge Request: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/28996
네임스페이스 리포지터리 통계의 성능은 스테이징 및 프로덕션(GitLab.com)에서 메트릭되었습니다. 모든 결과는 https://gitlab.com/gitlab-org/gitlab-foss/-/issues/64092에 게시되었으며 지금까지 보고된 문제는 없습니다.