데이터베이스 케이스 스터디: 네임스페이스 리포지터리 통계
소개
그룹을 위한 리포지터리 및 제한 관리에서, 그룹이 사용한 리포지터리 양을 쉽게 볼 수 있도록 하는 방법을 용이하게 하고 쉬운 관리를 허용하고자 합니다.
제안
- 루트 네임스페이스에 속한 프로젝트의 통계를 집계 형태로 저장하기 위해 새로운 ActiveRecord 모델을 생성합니다.
- 이 모델에서 프로젝트가 해당 네임스페이스에 속할 때마다 통계를 새로고침합니다.
문제
GitLab에서는 프로젝트 리포지터리 통계를 프로젝트 저장 시마다 콜백을 통해 업데이트합니다.
그런 다음 네임스페이스별로 이러한 통계의 요약은 Namespaces#with_statistics
범위로 가져옵니다. 이 쿼리를 분석한 결과, 다음을 알 수 있었습니다:
- 15,000개 이상의 프로젝트가 있는 네임스페이스에는 최대
1.2
초가 소요됩니다. - ChatOps에서 시간이 초과되므로 이 쿼리를 분석할 수 없습니다.
게다가 현재 사용 중인 프로젝트 통계를 업데이트하는 패턴(콜백)은 충분히 확장 가능하지 않습니다. 현재 이것은 가장 시간이 많이 소요되는 프로덕션 데이터베이스 쿼리 트랜잭션 중 하나이며, 쿼리에 하나 더 추가하면 트랜잭션의 길이가 늘어납니다.
위와 같은 이유로 namespace
테이블이 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와 유사합니다: 공통 테이블 표현을 사용한 새로고침 전략을 통해 모델을 업데이트합니다. (Common Table Expression)
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를 포함하고 있기 때문입니다.
이 접근 방식의 단점은 Redis가 PostgreSQL과 같은 지속성/일관성 보장을 제공하지 않는다는 것이며, 이는 Redis 장애에서 손실할 수 없는 정보입니다.
시도 D: 루트 네임스페이스와 해당 하위 네임스페이스를 태그로 지정
루트 네임스페이스를 해당 하위 네임스페이스에 직접 연결하여 부모 없이 생성된 네임스페이스가 있을 때 다음과 같이 태그를 지정합니다:
ID | root ID | parent 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 작업 및 다른 트랜잭션을 통해 새로고침하는 것으로 구성됩니다:
-
namespace_aggregation_schedules
이라는 두 개의 열id
및namespace_id
를 가진 두 번째 테이블을 생성합니다. - 프로젝트 통계가 변경될 때마다
namespace_aggregation_schedules
에 행을 삽입합니다.- 루트 네임스페이스와 관련된 행이 이미 있는 경우 새 행을 삽입하지 않습니다.
-
project_statistics
를 업데이트하는 트랜잭션 길이를 유념하여, 삽입은 다른 트랜잭션에서 Sidekiq 작업을 통해 수행해야 합니다.
- 행을 삽입한 후, 다른 시각에 두 번째 워커가 비동기적으로 실행되도록 예약합니다:
- 즉시 실행을 위해 하나를 인큐하고, 다른 하나를
1.5시간
후에 예약합니다. - 루트 네임스페이스 ID를 기반으로한 Redis의 키에서
1.5시간
동안 임대를 얻을 수 있는 경우에만 작업을 예약합니다. - 임대권을 얻을 수 없는 경우, 이미 집계가 진행 중이거나
1.5시간
이 넘지 않고 예약되어 있다는 것을 나타냅니다.
- 즉시 실행을 위해 하나를 인큐하고, 다른 하나를
- 이 워커는 다음을 수행합니다:
- 모든 네임스페이스를 쿼리하여 루트 네임스페이스 리포지터리 통계를 업데이트합니다.
- 업데이트 후 관련
namespace_aggregation_schedules
를 삭제합니다.
-
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: 현재까지는 문제가 보고되지 않았습니다.