InfoGrab DocsInfoGrab Docs

데이터베이스 사례 연구: 네임스페이스 스토리지 통계

요약

그룹의 스토리지 및 한도 관리에서, 우리는 그룹이 사용하는 스토리지 양을 쉽게 확인하고 간편하게 관리할 수 있는 방법을 제공하고자 합니다. 네임스페이스의 통계를 집계된 형태로 보관하는 새로운 ActiveRecord 모델을 생성합니다(루트 네임스페이스 전용).

소개#

그룹의 스토리지 및 한도 관리에서, 우리는 그룹이 사용하는 스토리지 양을 쉽게 확인하고 간편하게 관리할 수 있는 방법을 제공하고자 합니다.

제안#

  • 네임스페이스의 통계를 집계된 형태로 보관하는 새로운 ActiveRecord 모델을 생성합니다(루트 네임스페이스 전용).

  • 이 네임스페이스에 속하는 프로젝트가 변경될 때마다 이 모델의 통계를 새로 고칩니다.

문제#

GitLab에서는 프로젝트가 저장될 때마다 콜백을 통해 프로젝트 스토리지 통계를 업데이트합니다.

네임스페이스별 통계 요약은 Namespaces#with_statistics 스코프를 통해 조회됩니다. 이 쿼리를 분석한 결과:

  • 15k개 이상의 프로젝트가 있는 네임스페이스에서 최대 1.2초가 소요됩니다.

  • 타임아웃이 발생하기 때문에 ChatOps로 분석할 수 없습니다.

또한, 프로젝트 통계를 업데이트하는 데 현재 사용 중인 패턴(콜백)은 적절히 확장되지 않습니다. 현재 이 패턴은 전체적으로 가장 많은 시간이 소요되는 프로덕션의 대규모 데이터베이스 쿼리 트랜잭션 중 하나입니다. 트랜잭션 길이가 증가하므로 쿼리를 추가할 수 없습니다.

위의 모든 이유로 인해, namespaces 테이블이 GitLab.com에서 가장 큰 테이블 중 하나이기 때문에 네임스페이스 통계를 저장하고 업데이트하는 데 동일한 패턴을 적용할 수 없습니다. 따라서 성능이 우수한 대안적인 방법을 찾아야 했습니다.

시도#

시도 A: PostgreSQL 구체화된 뷰#

구체화된 뷰(materialized view)와 프로젝트 라우트 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 간에 차이가 있습니다. 이 기능을 개발하던 당시 GitLab은 여전히 MySQL을 지원했습니다.

  • Rails는 구체화된 뷰에 대한 네이티브 지원이 없습니다. 데이터베이스 뷰 관리를 처리하기 위해 전문 gem을 사용해야 하므로 추가 작업이 필요합니다.

시도 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을 사용할 수 있습니다. 이는 지루한 솔루션(boring solution)이면서 구현하기 가장 빠른 방법입니다. GitLab은 이미 Redis를 아키텍처의 일부로 포함하고 있습니다.

이 접근 방식의 단점은 Redis가 PostgreSQL과 동일한 지속성/일관성 보장을 제공하지 않는다는 것입니다. 이 정보는 Redis 장애 시 잃어버릴 수 없는 중요한 데이터입니다.

시도 D: 루트 네임스페이스와 하위 네임스페이스에 태그 지정#

루트 네임스페이스를 하위 네임스페이스와 직접 연결하여, 부모 없이 네임스페이스가 생성될 때마다 루트 네임스페이스 ID로 태그를 지정합니다:

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
)

이 접근 방식은 집계를 훨씬 쉽게 만들지만, 몇 가지 주요 단점이 있습니다:

  • 새 칼럼을 추가하고 채워서 모든 네임스페이스를 마이그레이션해야 합니다. 테이블의 크기 때문에 시간/비용을 처리하는 데 상당한 리소스가 필요합니다. 백그라운드 마이그레이션은 약 153h가 걸립니다. https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/29772 참조.

  • 백그라운드 마이그레이션은 한 릴리즈 이전에 배포되어야 하므로, 기능이 또 다른 마일스톤만큼 지연됩니다.

시도 E (최종): 네임스페이스 스토리지 통계를 비동기적으로 업데이트#

이 접근 방식은 이미 보유한 증분 통계 업데이트를 계속 사용하되, Sidekiq 작업과 별도의 트랜잭션을 통해 새로 고치는 방법으로 구성됩니다:

  • idnamespace_id 두 칼럼을 가진 두 번째 테이블(namespace_aggregation_schedules)을 생성합니다.

  • 프로젝트의 통계가 변경될 때마다 namespace_aggregation_schedules에 행을 삽입합니다.

루트 네임스페이스와 관련된 행이 이미 있으면 새 행을 삽입하지 않습니다.

  • project_statistics를 업데이트하는 트랜잭션의 길이(https://gitlab.com/gitlab-org/gitlab/-/issues/29070)를 고려하면, 삽입은 별도의 트랜잭션에서 Sidekiq Job을 통해 수행되어야 합니다.

  • 행을 삽입한 후, 서로 다른 두 시점에 비동기적으로 실행될 다른 워커를 예약합니다:

즉시 실행을 위해 대기열에 추가된 것과 1.5h 후에 예약된 것.

  • 루트 네임스페이스 ID를 기반으로 한 키로 Redis에서 1.5h 임대를 획득할 수 있을 때만 작업을 예약합니다.

  • 임대를 획득할 수 없으면, 이미 다른 집계가 진행 중이거나 1.5h 이내에 예약되어 있음을 나타냅니다.

  • 이 워커는 다음을 수행합니다:

서비스를 통해 모든 네임스페이스를 쿼리하여 루트 네임스페이스 스토리지 통계를 업데이트합니다.

  • 업데이트 후 관련 namespace_aggregation_schedules를 삭제합니다.

  • namespace_aggregation_schedules 테이블의 나머지 행을 순회하고 모든 대기 중인 행에 대한 작업을 예약하는 또 다른 Sidekiq 작업도 포함됩니다.

이 작업은 매일 밤(UTC) 실행되도록 cron으로 예약됩니다.

이 구현에는 다음과 같은 이점이 있습니다:

  • 모든 업데이트가 비동기적으로 수행되므로 project_statistics에 대한 트랜잭션 길이가 늘어나지 않습니다.

  • 단일 SQL 쿼리로 업데이트를 수행합니다.

  • PostgreSQL 및 MySQL과 호환됩니다.

  • 백그라운드 마이그레이션이 필요 없습니다.

이 접근 방식의 유일한 단점은 네임스페이스 통계가 변경 후 최대 1.5시간 후에 업데이트된다는 것으로, 통계가 정확하지 않은 시간 창이 존재합니다. 아직 스토리지 한도를 적용하지 않고 있으므로, 이는 큰 문제가 되지 않습니다.

결론#

스토리지 통계를 비동기적으로 업데이트하는 것이 루트 네임스페이스를 집계하는 방법 중 가장 문제가 적고 성능이 우수한 접근 방식이었습니다.

이 사례에 관한 모든 세부 사항은 다음에서 확인할 수 있습니다:

네임스페이스 스토리지 통계의 성능은 스테이징과 프로덕션(GitLab.com)에서 측정되었습니다. 모든 결과는 https://gitlab.com/gitlab-org/gitlab-foss/-/issues/64092에 게시되었습니다: 지금까지 문제가 보고되지 않았습니다.

데이터베이스 사례 연구: 네임스페이스 스토리지 통계

GitLab v19.1
원문 보기
요약

그룹의 스토리지 및 한도 관리에서, 우리는 그룹이 사용하는 스토리지 양을 쉽게 확인하고 간편하게 관리할 수 있는 방법을 제공하고자 합니다. 네임스페이스의 통계를 집계된 형태로 보관하는 새로운 ActiveRecord 모델을 생성합니다(루트 네임스페이스 전용).

소개#

그룹의 스토리지 및 한도 관리에서, 우리는 그룹이 사용하는 스토리지 양을 쉽게 확인하고 간편하게 관리할 수 있는 방법을 제공하고자 합니다.

제안#

  • 네임스페이스의 통계를 집계된 형태로 보관하는 새로운 ActiveRecord 모델을 생성합니다(루트 네임스페이스 전용).

  • 이 네임스페이스에 속하는 프로젝트가 변경될 때마다 이 모델의 통계를 새로 고칩니다.

문제#

GitLab에서는 프로젝트가 저장될 때마다 콜백을 통해 프로젝트 스토리지 통계를 업데이트합니다.

네임스페이스별 통계 요약은 Namespaces#with_statistics 스코프를 통해 조회됩니다. 이 쿼리를 분석한 결과:

  • 15k개 이상의 프로젝트가 있는 네임스페이스에서 최대 1.2초가 소요됩니다.

  • 타임아웃이 발생하기 때문에 ChatOps로 분석할 수 없습니다.

또한, 프로젝트 통계를 업데이트하는 데 현재 사용 중인 패턴(콜백)은 적절히 확장되지 않습니다. 현재 이 패턴은 전체적으로 가장 많은 시간이 소요되는 프로덕션의 대규모 데이터베이스 쿼리 트랜잭션 중 하나입니다. 트랜잭션 길이가 증가하므로 쿼리를 추가할 수 없습니다.

위의 모든 이유로 인해, namespaces 테이블이 GitLab.com에서 가장 큰 테이블 중 하나이기 때문에 네임스페이스 통계를 저장하고 업데이트하는 데 동일한 패턴을 적용할 수 없습니다. 따라서 성능이 우수한 대안적인 방법을 찾아야 했습니다.

시도#

시도 A: PostgreSQL 구체화된 뷰#

구체화된 뷰(materialized view)와 프로젝트 라우트 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 간에 차이가 있습니다. 이 기능을 개발하던 당시 GitLab은 여전히 MySQL을 지원했습니다.

  • Rails는 구체화된 뷰에 대한 네이티브 지원이 없습니다. 데이터베이스 뷰 관리를 처리하기 위해 전문 gem을 사용해야 하므로 추가 작업이 필요합니다.

시도 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을 사용할 수 있습니다. 이는 지루한 솔루션(boring solution)이면서 구현하기 가장 빠른 방법입니다. GitLab은 이미 Redis를 아키텍처의 일부로 포함하고 있습니다.

이 접근 방식의 단점은 Redis가 PostgreSQL과 동일한 지속성/일관성 보장을 제공하지 않는다는 것입니다. 이 정보는 Redis 장애 시 잃어버릴 수 없는 중요한 데이터입니다.

시도 D: 루트 네임스페이스와 하위 네임스페이스에 태그 지정#

루트 네임스페이스를 하위 네임스페이스와 직접 연결하여, 부모 없이 네임스페이스가 생성될 때마다 루트 네임스페이스 ID로 태그를 지정합니다:

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
)

이 접근 방식은 집계를 훨씬 쉽게 만들지만, 몇 가지 주요 단점이 있습니다:

  • 새 칼럼을 추가하고 채워서 모든 네임스페이스를 마이그레이션해야 합니다. 테이블의 크기 때문에 시간/비용을 처리하는 데 상당한 리소스가 필요합니다. 백그라운드 마이그레이션은 약 153h가 걸립니다. https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/29772 참조.

  • 백그라운드 마이그레이션은 한 릴리즈 이전에 배포되어야 하므로, 기능이 또 다른 마일스톤만큼 지연됩니다.

시도 E (최종): 네임스페이스 스토리지 통계를 비동기적으로 업데이트#

이 접근 방식은 이미 보유한 증분 통계 업데이트를 계속 사용하되, Sidekiq 작업과 별도의 트랜잭션을 통해 새로 고치는 방법으로 구성됩니다:

  • idnamespace_id 두 칼럼을 가진 두 번째 테이블(namespace_aggregation_schedules)을 생성합니다.

  • 프로젝트의 통계가 변경될 때마다 namespace_aggregation_schedules에 행을 삽입합니다.

루트 네임스페이스와 관련된 행이 이미 있으면 새 행을 삽입하지 않습니다.

  • project_statistics를 업데이트하는 트랜잭션의 길이(https://gitlab.com/gitlab-org/gitlab/-/issues/29070)를 고려하면, 삽입은 별도의 트랜잭션에서 Sidekiq Job을 통해 수행되어야 합니다.

  • 행을 삽입한 후, 서로 다른 두 시점에 비동기적으로 실행될 다른 워커를 예약합니다:

즉시 실행을 위해 대기열에 추가된 것과 1.5h 후에 예약된 것.

  • 루트 네임스페이스 ID를 기반으로 한 키로 Redis에서 1.5h 임대를 획득할 수 있을 때만 작업을 예약합니다.

  • 임대를 획득할 수 없으면, 이미 다른 집계가 진행 중이거나 1.5h 이내에 예약되어 있음을 나타냅니다.

  • 이 워커는 다음을 수행합니다:

서비스를 통해 모든 네임스페이스를 쿼리하여 루트 네임스페이스 스토리지 통계를 업데이트합니다.

  • 업데이트 후 관련 namespace_aggregation_schedules를 삭제합니다.

  • namespace_aggregation_schedules 테이블의 나머지 행을 순회하고 모든 대기 중인 행에 대한 작업을 예약하는 또 다른 Sidekiq 작업도 포함됩니다.

이 작업은 매일 밤(UTC) 실행되도록 cron으로 예약됩니다.

이 구현에는 다음과 같은 이점이 있습니다:

  • 모든 업데이트가 비동기적으로 수행되므로 project_statistics에 대한 트랜잭션 길이가 늘어나지 않습니다.

  • 단일 SQL 쿼리로 업데이트를 수행합니다.

  • PostgreSQL 및 MySQL과 호환됩니다.

  • 백그라운드 마이그레이션이 필요 없습니다.

이 접근 방식의 유일한 단점은 네임스페이스 통계가 변경 후 최대 1.5시간 후에 업데이트된다는 것으로, 통계가 정확하지 않은 시간 창이 존재합니다. 아직 스토리지 한도를 적용하지 않고 있으므로, 이는 큰 문제가 되지 않습니다.

결론#

스토리지 통계를 비동기적으로 업데이트하는 것이 루트 네임스페이스를 집계하는 방법 중 가장 문제가 적고 성능이 우수한 접근 방식이었습니다.

이 사례에 관한 모든 세부 사항은 다음에서 확인할 수 있습니다:

네임스페이스 스토리지 통계의 성능은 스테이징과 프로덕션(GitLab.com)에서 측정되었습니다. 모든 결과는 https://gitlab.com/gitlab-org/gitlab-foss/-/issues/64092에 게시되었습니다: 지금까지 문제가 보고되지 않았습니다.