Merge Request 성능 지침

새롭게 도입된 Merge Request은 기본적으로 성능을 고려해야 합니다.

GitLab의 성능에 부정적인 영향을 미치지 않도록 하기 위해, 모든 Merge Request은 본 문서에 개요된 지침을 준수해야 할 것입니다. 백앤드 유지보수자 및 성능 전문가들과 특별히 논의하고 합의한 경우를 제외하고는 이 규칙에는 예외가 없습니다.

또한, 다음 가이드를 읽는 것을 강력히 권장합니다:

정의

RFC 2119에 따라 SHOULD 용어는 다음과 같은 의미를 가집니다:

이 용어 또는 “권장” 형용사는 특정 상황에서 특정 사항을 무시할 수 있는 유효한 이유가 존재할 수 있지만, 다른 방법을 선택하기 전에 전체적인 영향을 충분히 이해하고 신중히 고려해야 합니다.

이상적으로 각각의 이러한 타협점은 별도의 문제로 문서화 되어야 하며, 해당 문제에 맞게 레이블이 지정되고 원본 문제 및 큰 이슈에 링크되어야 합니다.

영향 분석

요약: Merge Request이 GitLab 설정을 유지하는 사람들과 성능에 미치는 영향에 대해 고려해 보세요.

제출된 모든 변경은 애플리케이션 자체뿐만 아니라 유지 관리하고 실행하는 사람들(예: 프로덕션 엔지니어)에게도 영향을 줄 수 있습니다. 결과적으로 Merge Request이 애플리케이션 뿐만 아니라 운영 및 유지에 참여하는 사람들에게 미치는 영향에 대해 신중히 고려해야 합니다.

사용된 쿼리가 잠재적으로 중요한 서비스를 다운시키고 엔지니어들이 밤에 깨어날 수 있는 가능성이 있습니까? 악의적 사용자가 코드를 악용하여 GitLab 인스턴스를 다운시킬 수 있습니까? 내 변경 사항은 특정 페이지 로딩을 느리게 만들어버릴 수 있습니까? 데이터베이스에 충분한 부하 또는 데이터가 있는 경우 실행 시간이 지수적으로 증가합니까?

이러한 질문들은 Merge Request을 제출하기 전에 스스로에게 물어봐야 하는 질문들입니다. 때로는 영향을 평가하는 것이 어려울 때가 있으며, 그럴 경우 성능 전문가에게 코드를 검토해 달라고 요청해야 합니다. 자세한 내용은 아래의 “검토” 섹션을 참조하세요.

성능 검토

요약: 영향을 확신할 수 없다면 성능 전문가에게 코드 검토를 요청하세요.

Merge Request이 어떤 영향을 미칠지 식별하는 것이 어려울 수 있습니다. 이 경우, Merge Request 리뷰어 중 한 명에게 변경 사항을 검토해 달라고 요청해야 합니다. (리뷰어 디렉터리은 여기에서 확인할 수 있습니다.) 리뷰어는 차례로 성능 전문가에게 변경 사항을 검토해 달라고 요청할 수 있습니다.

상상력을 발휘하세요

모든 사람은 새로운 기능을 사용하는 방법에 대한 자신만의 인식을 갖고 있습니다. 항상 사용자가 해당 기능을 어떻게 사용할 수 있는지에 대해 고려하세요. 일반적으로 사용자들은 매우 비전통적인 방법으로 기능을 테스트합니다. 예를 들어, 브루트 포싱이나 우리가 가진 가장자리 조건을 부당하게 사용합니다.

데이터 세트

Merge Request이 처리하는 데이터 세트를 알고 문서화해야 합니다. 해당 기능이 처리할 예상 데이터 세트와 발생할 수 있는 문제가 명확히 문서화되어야 합니다.

파일 디렉터리을 필터링하는 다음 예제에 대해 생각해보세요. 문제는 간단합니다: Git 리포지터리에서 파일 디렉터리을 필터링하려고 합니다. 본인으로서는 다음과 같은 문제를 고려해야 합니다.

  1. 어떤 리포지터리를 지원할 계획입니까?
  2. 리눅스 커널과 같은 대형 리포지터리는 얼마나 걸릴까요?
  3. 이러한 대형 데이터 세트를 처리하지 않도록 다른 방법이 있을까요?
  4. 모든 사용자가 아닌 사용자에게 서비스를 악화시키는 안전 장치를 구축해야 할까요?

쿼리 계획과 데이터베이스 구조

쿼리 계획을 통해 추가적인 인덱스나 비용이 많이 드는 필터링(예: 순차 검색 사용)이 필요한지 알 수 있습니다.

각각의 쿼리 계획은 상당한 양의 데이터 세트에 대해 실행되어야 합니다. 예를 들어, 특정 조건을 갖는 이슈를 찾는 경우, 수백 개의 이슈에 대한 쿼리를 확인하고 100,000개의 이슈에 대한 쿼리를 확인해야 합니다. 결과가 적은 경우와 몇 천 개인 경우 쿼리의 동작을 확인하세요.

이는 GitLab을 매우 큰 프로젝트에서 매우 비전통적인 방법으로 사용하는 사용자들이 있기 때문에 필요합니다. 큰 데이터 세트가 사용되리라고 생각하지 않더라도 우리의 고객 중 한 명이 해당 기능에서 문제를 겪을 수도 있기 때문에 이는 그럴듯한 결과입니다. 사전에 이것이 어떻게 확장될지 이해하는 것은 바람직한 결과입니다. 항상 더 높은 사용 패턴을 위해 기능을 최적화하기 위해 필요한 것이 무엇인지에 대한 계획 또는 이해를 가져야 합니다.

모든 데이터베이스 구조는 더 높은 확장성을 위해 최적화되어야 하며 때로는 지나친 설명이 필요할 수도 있습니다. 어느 순간 이후에 가장 어려운 부분은 데이터 이전입니다. 수백만 행의 데이터를 이전하는 것은 언제나 골치 아픈 문제이며 애플리케이션에 부정적 영향을 미칠 수 있습니다.

쿼리 계획 검토를 위한 도움을 받는 방법에 대해 자세히 알아보려면 마이그레이션 중 데이터베이스 검토를 위한 Merge Request 준비 방법 섹션을 참조하세요.

쿼리 횟수

요약: Merge Request은 반드시 필요한 경우를 제외하고 실행된 SQL 쿼리의 총 횟수를 줄이지 않아야 합니다.

Merge Request에 의해 수정되거나 추가된 코드에 의해 실행된 총 쿼리 횟수는 반드시 필요한 경우를 제외하고 증가하면 안 됩니다. 기능을 개발할 때 추가적인 쿼리가 필요한 경우가 있을 수 있지만 최소한으로 유지해야 합니다.

예를 들어, 동일한 값으로 데이터베이스 행의 갯수를 업데이트하는 기능을 도입할 경우 이를 다음과 같은 의사코드로 작성하는 것이 매우 유혹적일 수 있습니다.

objects_to_update.each do |object|
  object.some_field = some_value
  object.save
end

이는 각각의 오브젝트를 업데이트하기 위해 쿼리를 실행하는 것을 의미합니다. 충분한 행을 업데이트할 경우 이 코드는 데이터베이스에 부하를 줄 수 있습니다. 이러한 문제는 “N+1 쿼리 문제”로 알려져 있습니다. 이러한 문제를 감지하고 회귀를 방지하기 위해 QueryRecorder를 사용한 테스트를 작성할 수 있습니다.

이러한 특이한 경우에는 다음과 같은 해결책이 있습니다:

objects_to_update.update_all(some_field: some_value)

이는 ActiveRecord의 update_all 메소드를 사용하여 단일 쿼리로 모든 행을 업데이트합니다. 결과적으로 이 코드는 데이터베이스에 부하를 많이 주기 어렵게 만듭니다.

가능한 경우 읽기 복제본 사용

DB 클러스터에서는 여러 개의 읽기 복제본과 하나의 주 복제본이 있습니다. DB 확장의 고전적인 사용 사례는 읽기 전용 작업을 복제본에서 수행하는 것입니다. 우리는 로드 밸런싱을 사용하여 이 부하를 분산시킵니다. 이를 통해 복제본이 DB에 가해지는 압력에 따라 성장할 수 있습니다.

기본적으로 쿼리는 읽기 전용 복제본을 사용하나 주 복제본 고정 때문에 GitLab은 일정 시간 동안 주 복제본을 사용하고 30초 후에 이전의 결과나 후술된 결과를 캐치업한 후에 보조 복제본으로 되돌아갑니다. 이로 인해 주 복제본에 불필요한 부하가 걸리는 경우가 많습니다.
이를 방지하기 위해 Merge Request 56849에서 without_sticky_writes 블록이 도입되었습니다. 이 방법은 일반적으로 세션 내에서 다음 쿼리에 영향을 주지 않는 사소하거나 무의미한 쓰기 후에 주 복제본 고정을 방지하기 위해 적용될 수 있습니다.

사용 타임스탬프 업데이트로 세션이 주 복제본 고정을 유도하고 이를 without_sticky_writes로 방지하는 방법에 대해 알아보려면 Merge Request 57328을 참조하세요.

without_sticky_writes 유틸리티의 반대로 Merge Request 59167에서 use_replicas_for_read_queries가 도입되었습니다. 이 방법은 해당 블록 내의 모든 읽기 전용 쿼리가 현재 주 복제본 고정에 관계없이 복제본을 읽도록 강제합니다. 이 유틸리티는 쿼리가 복제 지연을 용인할 수 있는 경우에 사용됩니다.

내부적으로 우리의 데이터베이스 로드 밸런서는 주 명세서 (select, update, delete 등)에 따라 쿼리를 분류합니다. 의심스러울 때, 쿼리를 주 데이터베이스로 리디렉션합니다. 따라서 로드 밸런서가 쿼리를 주 복제본에 불필요하게 보내는 일반적인 경우가 있습니다:

  • 사용자 정의 쿼리 (exec_query, execute_statement, execute 등)
  • 읽기 전용 트랜잭션
  • 비행 중인 연결 구성 설정
  • Sidekiq 백그라운드 작업

위의 쿼리들을 실행한 후에 GitLab은 주 복제본에 고정합니다. 내부 쿼리가 복제본을 사용하도록 설정하기 위해 Merge Request 59086이 도입되었습니다. 이 MR은 또한 비용이 많이 드는 시간이 오래 걸리는 쿼리를 복제본으로 리디렉트하는 방법의 예시입니다.

CTE(공통 테이블 표현)를 현명하게 사용하세요

CTE의 사용 방법에 대한 고려 사항은 반복적으로 전체 테이블을 조사하는 복잡한 쿼리에서 확인하세요. 우리는 일부 상황에서 CTE가 사용 중에 문제가 될 수 있다는 것을 발견했습니다 (위의 N+1 문제와 유사). 특히 AuthorizedProjectsWorker의 CTE와 같은 계층적 재귀 CTE 쿼리는 최적화하기가 매우 어려우며 확장이 어렵습니다. 이러한 계층적 구조를 필요로 하는 새로운 기능을 구현할 때는 피해야 합니다.

CTE는 예시의 경우와 같이 많은 간단한 경우에서 최적화 울타리로 효과적으로 사용되었습니다. 현재 지원되는 PostgreSQL 버전에서는 최적화 울타리 동작을 MATERIALIZED 키워드로 활성화해야 합니다. 기본적으로 CTE는 인라인화되어 기본적으로 최적화됩니다.

CTE 문을 작성할 때는 GitLab 13.11에서 도입된 Gitlab::SQL::CTE 클래스를 사용하세요. 기본적으로 이 Gitlab::SQL::CTE 클래스는 MATERIALIZED 키워드를 추가하여 자료화를 강제합니다.

caution
GitLab 14.0으로 업그레이드하려면 PostgreSQL 12 또는 그 이상이 필요합니다.

캐시된 쿼리

요약: Merge Request은 중복 캐시된 쿼리를 실행해서는 안 됩니다.

Rails는 요청 기간 동안 데이터베이스 쿼리 결과를 캐시하는 SQL 쿼리 캐시를 제공합니다.

캐시된 쿼리가 왜 나쁜 것으로 간주되는지에 대해 알아보고 어떻게 검출하는지 확인하세요.

Merge Request에 의해 도입된 코드는 중복된 캐시된 쿼리를 실행해서는 안됩니다.

Merge Request에 의해 수정되거나 추가된 코드에 의해 실행된 쿼리의 총 개수(캐시된 것 포함)는 절대로 필요한 경우가 아니라면 증가해서는 안 됩니다. 실행된 쿼리의 개수(캐시된 쿼리 포함)는 컬렉션 크기에 의존해서는 안 됩니다. 이를 검출하고 회귀를 방지하기 위해 skip_cached 변수를 QueryRecorder에 전달하여 테스트를 작성할 수 있습니다.

예를 들어 CI 파이프라인이 있다고 가정해 봅시다. 모든 파이프라인 빌드는 동일한 파이프라인에 속하므로 동일한 프로젝트(pipeline.project)에 속합니다:

pipeline_project = pipeline.project
# Project Load (0.6ms)  SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2
build = pipeline.builds.first

build.project == pipeline_project
# CACHE Project Load (0.0ms)  SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2
# => true

build.project를 호출하면 데이터베이스를 조회하지 않고 캐시된 결과를 사용하지만 동일한 메모리 내 프로젝트 객체를 재인스턴스화합니다. 연결된 객체가 같은 인메모리 객체를 가리키지 않는 것으로 나타납니다.

각 빌드를 직렬화하려고 시도하면:

pipeline.builds.each do |build|
  build.to_json(only: [:name], include: [project: { only: [:name]}])
end

각 빌드마다 프로젝트 객체를 재인스턴스화합니다. 이러한 특정 경우에 대한 해결책은 다음과 같습니다:

ActiveRecord::Associations::Preloader.new(records: pipeline, associations: [builds: :project]).call

pipeline.builds.each do |build|
  build.to_json(only: [:name], include: [project: { only: [:name]}])
end

ActiveRecord::Associations::Preloader는 동일한 메모리 내 객체를 사용하여 캐시된 SQL 쿼리를 회피하고 각 빌드마다 프로젝트 객체를 다시 생성하지 않습니다.

루프에서 쿼리 실행

Summary: SQL 쿼리는 꼭 필요할 때를 제외하고는 루프에서 실행해서는 안 됩니다.

루프에서 SQL 쿼리를 실행하면 루프의 반복 횟수에 따라 많은 쿼리가 실행될 수 있습니다. 개발 환경에서는 데이터가 적기 때문에 잘 작동할 수 있지만, 프로덕션 환경에서는 상황이 금방 고립될 수 있습니다.

이 경우에 필요한 경우도 있을 수 있습니다. 이 경우에는 Merge Request 설명에 명확히 언급해야 합니다.

일괄 처리

Summary: 외부 서비스(예: PostgreSQL, Redis, 객체 리포지터리)로의 단워크플로 반복은 연결 오버헤드를 줄이기 위해 일괄 처리로 실행되어야 합니다.

일괄 처리 방식으로 여러 테이블에서 행을 가져오려면 Eager Loading 섹션을 참조하세요.

예: 객체 리포지터리에서 여러 파일 삭제

객체 리포지터리에서 여러 파일을 삭제할 때 GCS와 같은 곳에서 단일 REST API 호출을 여러 번 실행하는 것은 상당히 비싼 프로세스입니다. 이런 경우에는 S3가 일괄 삭제 API를 제공하므로 이러한 방식을 고려하는 것이 좋은 생각일 것입니다.

FastDestroyAll 모듈은 이 상황에 도움이 될지도 모릅니다. 이것은 일괄 처리로 데이터베이스 행을 한꺼번에 제거할 때 작은 프레임워크입니다.

타임아웃

Summary: 시스템이 외부 서비스(예: 쿠버네티스)로 HTTP 호출을 실행할 때 합리적인 타임아웃을 설정해야 하며, 이는 Puma 스레드가 아닌 Sidekiq에서 실행되어야 합니다.

GitLab은 종종 쿠버네티스 클러스터와 같은 외부 서비스와 통신해야 합니다. 이 경우에는 외부 서비스가 요청 프로세스를 완료하는 데 얼마나 걸릴지 추정하기 어려울 수 있습니다. 예를 들어 사용자 소유의 클러스터가 어떤 이유로 비활성화된 경우, GitLab은 영원히 응답을 기다려야 할 수 있습니다(예시). 이로 인해 Puma 타임아웃이 발생할 수 있으며, 반드시 피해야 합니다.

합리적인 타임아웃을 설정하고 예외를 정상적으로 처리하고 UI에서 또는 내부 로깅에서 오류를 표시해야 합니다.

외부 데이터를 가져오기 위한 최상의 솔루션 중 하나는 ReactiveCaching를 사용하는 것입니다.

데이터베이스 트랜잭션을 최소화

Summary: 데이터베이스 트랜잭션 중에 Gitaly와 같은 외부 서비스에 액세스하는 것은 심각한 경합 문제로 이어질 수 있으므로 피해야 합니다. 왜냐하면 열린 트랜잭션이 기본적으로 PostgreSQL 백엔드 연결을 블록하기 때문입니다.

트랜잭션을 최대한 최소화하기 위해 AfterCommitQueue 모듈이나 after_commit AR 후크를 사용하는 것을 고려해야 합니다.

여기 예시가 있는데, 트랜잭션 중에 Gitaly 인스턴스로 한 요청이 ~"priority::1" 이슈를 트리거했습니다.

Eager Loading

Summary: 두 개 이상의 행을 검색할 때 항상 관련 항목을 eager load해야 합니다.

여러 데이터베이스 레코드를 검색할 때 관련 항목을 사용해야 하는 경우에는 이러한 관련 항목을 eager load해야 합니다. 예를 들어, 블로그 글 디렉터리을 검색하고 저자를 표시하려면 저자 관련 항목을 eager load해야 합니다.

즉, 다음과 같이 하는 대신:

Post.all.each do |post|
  puts post.author.name
end

이렇게 해야 합니다:

Post.all.includes(:author).each do |post|
  puts post.author.name
end

또한 eager loading시에 회귀를 방지하려면 QueryRecoder tests를 사용하는 것도 고려해야 합니다.

메모리 사용

Summary: Merge Request은 꼭 필요한 경우가 아니라면 메모리 사용량을 증가시키면 안 됩니다.

Merge Request은 코드에서 절대적으로 필요한 최소한의 메모리 사용량 이상으로 GitLab 메모리 사용량을 증가시켜서는 안 됩니다. 즉, 큰 문서(예: HTML 문서)를 파싱해야 하는 경우에는 가능하다면 메모리로 전체 입력을 로드하는 대신에 스트림으로 파싱하는 것이 좋습니다. 때로는 이것이 불가능할 수 있으며, 이 경우에는 Merge Request에서 명시적으로 명시되어야 합니다.

UI 요소의 지연된 렌더링

Summary: 필요한 경우에만 UI 요소를 렌더링해야 합니다.

특정 UI 요소들은 항상 필요하지는 않을 수 있습니다. 예를 들어, 차이 라인을 가리킬 때 새로운 코멘트를 생성하는 데 사용할 수 있는 작은 아이콘이 표시됩니다. 이러한 종류의 요소는 항상 렌더링하는 대신 실제로 필요한 경우에만 렌더링되어야 합니다. 이렇게 하면 사용되지 않는 경우에는 Haml/HTML을 생성하는 시간을 절약할 수 있습니다.

캐싱 사용

Summary: 한 번의 트랜잭션에서 데이터가 여러 번 필요하거나 일정 기간 동안 유지되어야 하는 경우 메모리나 Redis에 데이터를 캐시해야 합니다.

때로는 한 번의 트랜잭션에서 동일한 데이터를 다른 곳에서 재사용해야 할 수 있습니다. 이러한 경우에는 이러한 데이터를 검색하는 복잡한 작업을 제거하기 위해 이러한 데이터를 메모리에 캐시해야 합니다. 데이터가 트랜잭션의 기간이 아닌 특정 기간 동안 캐시되어야 한다면 Redis를 사용해야 합니다.

예를 들어, 사용자 이름 언급이 포함된 여러 텍스트 조각을 처리해야 할 때(Hello @alice'와 같은) 각 사용자 이름에 대한 사용자 객체를 캐시함으로써 @alice‘를 언급할 때마다 동일한 쿼리를 실행할 필요를 없앨 수 있습니다.

트랜잭션 당 데이터를 캐싱하는 것은 RequestStore를 사용하여 수행할 수 있습니다(RequestStore.active?를 확인할 필요 없이 하려면 Gitlab::SafeRequestStore를 사용하세요). Redis에 데이터를 캐싱하는 것은 Rails 캐싱 시스템을 사용하여 수행할 수 있습니다.

페이지네이션

테이블 형식으로 항목 디렉터리을 렌더링하는 각 기능은 페이지네이션을 포함해야 합니다.

주요 페이지네이션 스타일은 다음과 같습니다:

  1. 오프셋 기반 페이지네이션: 사용자가 특정 페이지(예: 1)로 이동합니다. 사용자는 다음 페이지 번호와 전체 페이지 수를 볼 수 있습니다. 이 스타일은 GitLab의 모든 컴포넌트에서 잘 지원됩니다.
  2. 오프셋 기반 페이지네이션, 그러나 카운트 없이: 사용자가 특정 페이지(예: 1)로 이동합니다. 사용자는 다음 페이지 번호만 보고 전체 페이지 수는 표시되지 않습니다.
  3. 키셋 기반 페이지의 다음 페이지: 사용자는 사용 가능한 페이지 수를 모르기 때문에 다음 페이지로만 이동할 수 있습니다.
  4. 무한 스크롤 페이지네이션: 사용자가 페이지를 스크롤하면 다음 항목이 비동기적으로로드됩니다. 이것은 이전 스타일과 똑같은 이점을 가져오기 때문에 이상적입니다.

페이지네이션의 궁극적으로 확장 가능한 솔루션은 키셋 기반 페이지네이션을 사용하는 것입니다. 그러나 현재 GitLab에서는 그것에 대한 지원이 없습니다. API: Keyset Pagination의 진행 상황을 확인할 수 있습니다.

페이지네이션 전략을 선택할 때 다음을 고려해야 합니다:

  1. 필터링을 통과하는 객체의 수를 계산하는 것은 매우 비효율적입니다. 이 작업은 일반적으로 몇 초가 걸리며 타임아웃될 수 있습니다.
  2. 더 높은 서열의 페이지에 대한 항목 검색은 매우 비효율적입니다(예: 1000). 데이터베이스는 모든 이전 항목을 정렬 및 반복해야 하며 이 작업은 일반적으로 데이터베이스에 상당한 부하를 줄 수 있습니다.

페이지네이션에 관련된 유용한 팁은 pagination guidelines에서 찾을 수 있습니다.

뱃지 카운터

카운터는 항상 트랙되어야 합니다. 정확한 수를 표시하는 것보다 목표로 하는 임계값을 넘지 않는 것이 좋습니다. 그 이유는 정확한 항목 수를 계산하려면 각 항목을 필터링해야 하기 때문입니다.

UX 관점에서는 1000개 이상의 파이프라인이라고 표시하는 것이 좋지만 40000개 이상의 파이프라인을 표시하는 것보다는 페이지를 2초 더 오래로드하는 것과의 교환이 필요합니다.

이러한 패턴의 예로는 파이프라인과 잡의 디렉터리이 있습니다. 우리는 숫자를 1000+로 줄이지만 가장 흥미로운 정보인 실행 중인 파이프라인의 정확한 숫자를 표시합니다.

이러한 목적으로 사용할 수 있는 NumbersHelper.limited_counter_with_delimiter이라는 도우미 메서드가 있습니다. 이 메서드는 계산 행의 상한선을 받습니다.

어떤 경우에는 뱃지 카운터가 비동기적으로로드되도록하는 것이 바람직합니다. 이는 초기 페이지 로드를 가속화하고 전반적으로 더 나은 사용자 경험을 제공할 수 있습니다.

특징 플래그의 사용

성능에 중요한 요소가 있는 기능이나 알려진 성능 결함이 있는 기능마다 특징 플래그를 사용하여 비활성화되어야 합니다.

특징 플래그는 우리 팀이 시스템을 모니터링하고 사용자가 문제를 인지하기 전에 빠르게 대응할 수 있도록합니다.

성능 결함은 초기 변경 사항을 Merge한 후 즉시 해결해야 합니다.

GitLab 개발에서 특징 플래그 사용에 언제 어떻게 특징 플래그를 사용해야하는지 자세히 알아보십시오.

리포지터리

다음 유형의 리포지터리를 고려할 수 있습니다:

  • 로컬 임시 리포지터리 (매우 짧은 기간의 저장) 이 유형의 리포지터리는 /tmp 폴더와 같이 시스템에서 제공하는 리포지터리입니다. 모든 임시 작업에 이 유형의 리포지터리를 이상적으로 사용해야 합니다. 각 노드가 고유한 임시 리포지터리를 갖고 있다는 사실은 확장을 상당히 용이하게 만듭니다. 이 리포지터리는 매우 자주 SSD 기반으로 구성되어 있어 상당히 빠릅니다. 임시 리포지터리는 TMPDIR 변수를 사용하여 응용 프로그램에 쉽게 구성할 수 있습니다.

  • 공유 임시 리포지터리 (단기 리포지터리) 이 유형의 리포지터리는 일반적으로 공통 NFS 서버에서 실행되는 네트워크 기반의 임시 리포지터리입니다. 2020년 2월 현재 대부분의 구현에서 이 유형의 리포지터리를 사용합니다. 위의 제한을 상당히 크게 허용하지만, 실제로 더 많이 사용할 수 있다는 의미는 아닙니다. 공유 임시 리포지터리는 모든 노드에서 공유됩니다. 따라서 그 공간의 상당한 양을 사용하는 작업 또는 많은 작업을 수행하는 것은 전체 GitLab의 안정성에 영향을 줄 수 있습니다. 이 점을 고려해주시기 바랍니다.

  • 공유 지속적 리포지터리 (장기 리포지터리) 이 유형의 리포지터리는 공유 네트워크 기반 리포지터리(예: NFS)를 사용합니다. 이 솔루션은 일반적으로 몇 개의 노드로 구성된 소규모 설치를 실행하는 고객들이 주로 사용합니다. 공유 리포지터리의 파일은 쉽게 접근할 수 있지만, 데이터를 업로드하거나 다운로드하는 모든 작업은 모든 다른 작업들에 대한 중요한 경합을 만들 수 있습니다. 이것은 Omnibus가 기본적으로 사용하는 접근 방식이기도 합니다.

  • 객체 기반 지속적 리포지터리 (장기 리포지터리) 이 유형의 리포지터리는 AWS S3와 같은 외부 서비스를 사용합니다. 객체 리포지터리는 무한한 확장성과 중복성을 갖는 것으로 간주될 수 있습니다. 이 리포지터리에 액세스하려면 일반적으로 파일을 다운로드하여 조작해야 합니다. 객체 리포지터리는 파일의 무제한 동시 업로드 및 다운로드를 처리할 수 있다고 가정할 수 있기 때문에 궁극적인 솔루션으로 간주될 수 있습니다. 이것은 응용 프로그램이 컨테이너화된 배포(Kubernetes)에서 쉽게 실행될 수 있도록 보장해야 하는 필수적인 해결책입니다.

임시 리포지터리

프로덕션 노드의 리포지터리는 실제로 매우 한정적입니다. 응용 프로그램은 매우 제한된 임시 리포지터리에서 실행되도록 구성되어야 합니다. 코드가 실행되는 시스템은 일반적으로 1G-10G의 총 임시 리포지터리를 가지고 있을 것으로 예상할 수 있습니다. 그러나 이 리포지터리는 실제로 모든 실행 중인 작업을 통틀어 공유되기 때문에 만약 작업이 이 공간의 100MB 이상을 사용해야 한다면 채택한 접근 방식을 재고해야 합니다.

어떤 필요가 있는지에 관계없이 파일을 처리해야 한다면 문서화해야 합니다. 100MB를 초과하는 것이 필요하다면 유지자와 협력하여 더 나은 해결책을 발견할 수 있도록 지원을 요청하는 것이 좋습니다.

로컬 임시 리포지터리

로컬 리포지터리의 사용은 특히 Kubernetes 클러스터에 응용 프로그램을 배포하는 작업을 할 때 바람직한 해결책입니다. 예를 들어 Dir.mktmpdir를 사용하고 싶을 때는 언제입니까? 기존 데이터의 추출/작성, 기존 데이터의 범위 작업 등을 수행하려는 상황에서 사용하면 좋습니다.

Dir.mktmpdir('designs') do |path|
  # 경로에 대한 조작 수행
  # 블록을 벗어남과 동시에 
  # 경로가 제거될 것입니다
end

공유 임시 리포지터리

공유 임시 리포지터리의 사용은 디스크 기반 저장을 위해 파일을 영속적으로 사용하려면 필요합니다. Workhorse 직접 업로드는 파일을 받도록 할 때 공유 리포지터리에 기록하고 나중에 GitLab Rails가 이동 작업을 수행할 수 있습니다. 동일한 대상에 대한 이동 작업은 즉시 진행됩니다. copy 작업을 수행하는 대신 시스템은 파일을 새 위치로 다시 연결합니다.

이것은 응용 프로그램에 추가 복잡성을 도입하므로 이를 재구현하는 대신 잘 알려진 패턴(예: ObjectStorage concern)을 재사용하는 것이 바람직합니다.

그 외의 다른 용도로의 공유 임시 리포지터리의 사용은 사용이 중지되었습니다.

지속적 리포지터리

객체 리포지터리

모든 영구 파일을 보유하는 기능은 데이터를 객체 리포지터리에 저장할 수 있어야합니다. 노드 전체에 걸쳐 공유 볼륨으로 지속적 리포지터리를 보유하는 것은 확장 가능하지 않으며 모든 노드에 데이터 접근에 중요한 경합을 만들게 됩니다.

GitLab은 공유 및 객체 리포지터리 기반의 지속적 리포지터리를 원활하게 지원하는 ObjectStorage concern를 제공합니다.

데이터 접근

데이터 업로드를 허용하거나 다운로드를 허용하는 모든 기능은 Workhorse 직접 업로드를 사용해야합니다. 즉 업로드는 Workhorse에 의해 직접 객체 리포지터리에 저장되어야하며 모든 다운로드는 Workhorse에서 제공되어야합니다.

Puma를 통한 데이터 업로드/다운로드는 처리 슬롯(스레드) 전체를(를) 블로킹하기 때문에 비용이 많이 듭니다.

또한 Puma를 통한 데이터 업로드/다운로드에는 작업이 시간 초과되는 문제가 있습니다. 사용자가 업로드/다운로드에 많은 시간이 걸리면 요청 처리 시간(보통 30초-60초 사이)이 경과하여 처리 슬롯(스레드)가 종료될 수 있습니다.

위의 이유로 모든 파일 업로드 및 다운로드에 대해 Workhorse 직접 업로드가 구현되어야합니다.