병합 요청 성능 지침

각 새로운 도입된 병합 요청은 기본적으로 성능이 우수해야 합니다.

GitLab의 성능에 부정적인 영향을 미치지 않도록 하기 위해 모든 병합 요청은 이 문서에 기술된 지침을 준수해야 합니다. 백엔드 유지 관리자와 성능 전문가와 구체적으로 논의하고 합의된 경우를 제외하고는 이 규칙의 예외가 없습니다.

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

정의

SHOULD 용어는 RFC 2119에 따라 다음을 의미합니다:

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

이상적으로, 이러한 트레이드오프 각각은 별도의 문제로 문서화되어 각각에 해당하는 레이블이 지정되고 원본 문제와 에픽에 링크되어야 합니다.

영향 분석

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

제출된 변경 사항은 응용 프로그램 그 자체뿐만 아니라 그것을 유지하는 사람들에게도 영향을 줄 수 있습니다(예를 들어, 프로덕션 엔지니어). 따라서, 병합 요청이 응용 프로그램 뿐만 아니라 운영 중인 사람들에게 미치는 영향에 대해 신중하게 고려해야 합니다.

사용된 쿼리가 잠재적으로 중요한 서비스를 중단하고 밤에 엔지니어들을 깨울 수 있습니까? 악의적인 사용자가 코드를 남용하여 GitLab 인스턴스를 중단시킬 수 있습니까? 내 변경 사항이 특정 페이지를 로드하는 데 느리게 만들까요? 데이터베이스의 로드나 데이터 양에 비례해서 실행 시간이 기하급수적으로 증가합니까?

이러한 모든 질문들은 병합 요청을 제출하기 전에 자신에게 물어봐야 할 문제입니다. 예측하기 어려울 때도 있는데, 그런 경우에는 성능 전문가에게 코드를 검토해 달라고 요청해야 합니다. 자세한 내용은 아래의 “검토” 섹션을 참조하세요.

성능 검토

요약: 영향에 대해 확신이 없는 경우 성능 전문가에게 코드 검토를 요청하세요.

병합 요청의 영향을 평가하는 것이 어려울 때가 있습니다. 이 경우, 병합 요청 검토자 중 한 명에게 변경 사항을 검토하도록 요청해야 합니다. (여기에서 검토자 목록을 확인할 수 있습니다.) 검토자는 다시 성능 전문가에게 변경 사항을 검토해 달라고 요청할 수 있습니다.

상상의 나래를 펼치세요

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

데이터 세트

병합 요청이 처리하는 데이터 세트를 알아야 합니다. 이 기능은 명확히 이 기능이 처리하는 예상 데이터 세트와 문제가 될 수 있는 것을 문서화해야 합니다.

다음 예를 생각해보면 데이터 세트가 처리되고 있는 점을 강조합니다. 문제는 간단합니다: Git 리포지토리의 목록을 필터링하려고 합니다. 이 기능은 리포지토리에서 모든 파일의 목록을 요청하고 파일 집합을 검색합니다. 작성자로서는 그러한 문제의 맥락에서 다음 사항을 고려해야 합니다:

  1. 어떤 저장소를 지원할 계획인가요?
  2. 리눅스 커널과 같이 큰 저장소는 얼마나 걸릴까요?
  3. 이렇게 큰 데이터 세트를 처리하지 않고 다른 방법이 없을까요?
  4. 모든 사용자 대신에 단일 사용자를 위해 서비스의 복잡성을 억제하는 방법을 구축해야 할까요?

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

쿼리 계획은 추가적인 색인이 필요한지, 순차 검색과 같이 비용이 많이 드는 필터링이 필요한지 알려줄 수 있습니다.

각 쿼리 계획은 상당한 크기의 데이터 세트에서 실행되어야 합니다. 예를 들어, 특정 조건으로 이슈를 검색하는 경우, 몇 개(수백 개)와 수천 개(100,000 개)의 이슈에 대한 쿼리를 검증해야 합니다. 결과가 몇 개인지, 몇 천 개인지에 따라 쿼리의 동작 방식을 확인하세요.

이는 매우 큰 프로젝트와 매우 비전통적인 방식으로 GitLab을 사용하는 사용자가 있기 때문에 필요합니다. 그런 큰 데이터 세트가 사용될 가능성은 거의 없는 것처럼 보이더라도, 여전히 우리의 고객 중 한 명이 그 기능에 문제를 겪을 수 있는 여지가 있습니다.

사전에 규모에 맞게 어떻게 동작하는지를 이해하는 것은 항상 원하는 결과입니다. 우리는 항상 더 높은 사용 패턴을 위해 기능을 최적화하기 위해 필요한 것이 무엇인지에 대한 계획이나 이해를 가져야 합니다.

모든 데이터베이스 구조는 쉽게 확장할 수 있도록 최적화되어야 하며 때로는 과도하게 설명되어야 합니다. 어느 시점 이후에는 데이터 이관이 가장 어렵습니다. 수백만 행을 이관하는 것은 항상 문제가 되며 애플리케이션에 부정적인 영향을 미칠 수 있습니다.

쿼리 계획 검토에 대한 도움을 받는 방법에 대한 자세한 정보는 마이그레이션 중 데이터베이스 검토용 병합 요청을 준비하는 방법을 참조하세요.

쿼리 횟수

요약: 병합 요청은 절대로 필요하지 않은 경우가 아니라면 실행된 SQL 쿼리의 총 수를 증가시키면 안 됩니다.

병합 요청에 의해 수정되거나 추가된 코드에 의해 실행된 쿼리의 총 수는 반드시 필요한 경우가 아니라면 증가해서는 안 됩니다. 기능을 구축하는 경우 추가로 몇 가지 쿼리가 필요할 수 있지만, 최소한으로 유지하려고 노력해야 합니다.

예를 들어, 동일한 값을 갖는 데이터베이스 행의 수를 업데이트하는 기능을 도입한다고 가정해 보겠습니다. 아래의 의사 코드를 사용하여 이를 쉽게 작성할 수 있지만 매우 유혹적이며(그리고 쉽습니다):

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의 확장을 위한 클래식한 사용입니다. 이 작업을 분산시키기 위해 부하 분산을 사용합니다. 이렇게 하면 부하가 데이터베이스에 가해질수록 복제본이 증가할 수 있습니다.

기본적으로 쿼리는 읽기 전용 복제본을 사용하지만 primary sticking 때문에 GitLab은 일정 시간 동안 프라이머리를 사용하고 30초 이후에는 세컨더리로 돌아갑니다. 이렇게 하면 프라이머리에 불필요한 부하가 생길 수 있습니다. 프라이머리로 전환을 방지하려면 병합 요청 56849에서 without_sticky_writes 블록이 도입되었습니다. 일반적으로 이 방법은 동일 세션의 후속 쿼리에 영향을 주지 않는 사소하거나 미미한 쓰기 이후에 프라이머리의 정체를 방지하기 위해 적용할 수 있습니다.

사용 타임스탬프 업데이트가 세션을 프라이머리에 고정시킬 수 있는 경우 및 without_sticky_writes를 사용하여 이를 방지하는 방법에 대해 알아보려면 병합 요청 57328을 참조하세요.

without_sticky_writes 유틸리티의 대응물로 병합 요청 59167에서 use_replicas_for_read_queries가 도입되었습니다. 이 방법은 블록 내의 모든 읽기 전용 쿼리를 현재 프라이머리 정체 상태에 관계없이 복제본에서 읽도록 강제합니다. 이 유틸리티는 쿼리가 복제 지연을 견딜 수 있는 경우에 사용됩니다.

내부적으로 데이터베이스 부하 분산기는 주 명령문(select, update, delete 등)에 따라 쿼리를 분류합니다. 의심스러운 경우, 쿼리를 프라이머리로 리디렉트합니다. 따라서 부하 분산기가 다음과 같은 일반적인 경우에 쿼리를 불필요하게 프라이머리로 보냅니다:

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

위의 쿼리가 실행된 후, GitLab은 프라이머리에 고정합니다. 내부 쿼리를 복제본 사용을 선호하도록 만들려면 병합 요청 59086이 도입되었으며, 이는 시간이 많이 걸리고 비용 소모적인 쿼리를 복제본으로 리디렉트하는 방법의 예입니다.

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

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

CTE는 예제와 같이 보다 간단한 상황에서 최적화 펜스로 효과적으로 사용되었습니다. 현재 지원되는 PostgreSQL 버전에서는 최적화 펜스 동작이 MATERIALIZED 키워드로 활성화되어야 합니다. 기본적으로 CTE는 기본적으로 인라인화된 다음 기본적으로 최적화됩니다.

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

경고: GitLab 14.0으로 업그레이드하려면 PostgreSQL 12 이상이 필요합니다.

캐시된 쿼리

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

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

캐시된 쿼리가 왜 나쁜 것으로 간주되는지에 대한 내용은 여기와 캐시된 쿼리를 어떻게 감지하는지에 대한 내용은 여기에서 확인하세요.

병합 요청에 의해 도입된 코드는 중복된 캐시된 쿼리를 여러 번 실행해서는 안 됩니다.

병합 요청에 의해 수정되거나 추가된 코드에 의해 실행된 (캐시된 것 포함) 전체 쿼리 수는 절대적으로 필요한 경우를 제외하고 증가해서는 안 됩니다. 실행된 쿼리 수(캐시된 쿼리 포함)는 컬렉션 크기에 의존해서는 안 됩니다. 이를 검출하고 회귀를 방지하기 위해 QueryRecorderskip_cached 변수를 전달하여 테스트를 작성할 수 있습니다.

예를 들어 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 쿼리를 피하고 각 빌드마다 프로젝트 객체를 다시 인스턴스화하는 것도 피할 수 있습니다.

반복문에서 쿼리 실행

요약: SQL 쿼리는 반복문에서 절대적으로 실행되어서는 안됩니다.

반복문에서 SQL 쿼리를 실행하면 반복문의 반복 횟수에 따라 많은 쿼리가 실행될 수 있습니다. 이는 데이터가 적은 개발 환경에서는 잘 작동할 수 있지만, 프로덕션 환경에서는 빠르게 제어를 벗어날 수 있습니다.

이를 필요로 하는 경우가 몇 가지 있습니다. 이런 경우에는 병합 요청 설명에 명확히 언급되어야 합니다.

일괄 처리

요약: 외부 서비스(예: PostgreSQL, Redis, 객체 저장소)에 대한 단일 프로세스를 반복적으로 실행할 때, 연결 오버헤드를 줄이기 위해 일괄 스타일로 실행해야 합니다.

일괄 스타일로 여러 테이블에서 행을 가져오는 방법은 Eager Loading 섹션을 참조하세요.

예시: 객체 저장소에서 여러 파일 삭제

같은 GCS와 같은 객체 저장소에서 여러 파일을 삭제할 때, 단일 REST API 호출을 여러 번 실행하는 것은 꽤 비용이 많이 드는 프로세스입니다. 이를 일괄 스타일로 수행하는 것이 좋은 방법일 수 있으므로, 예를 들어 S3는 일괄 삭제 API를 제공하기 때문에 이러한 접근 방식을 고려하는 것이 좋은 아이디어일 수 있습니다.

FastDestroyAll 모듈은 이러한 상황을 도와줄 수 있습니다. 이는 일괄 스타일로 데이터베이스 행과 해당 데이터를 제거할 때 작은 프레임워크입니다.

시간 초과

요약: 시스템이 외부 서비스(예: 쿠버네티스)로 HTTP 호출을 하는 경우, 합리적인 시간 초과를 설정해야 하며, 이는 Puma 스레드가 아니라 Sidekiq에서 실행되어야 합니다.

GitLab은 종종 쿠버네티스 클러스터와 같은 외부 서비스와 통신해야 합니다. 이 경우 외부 서비스가 요청된 프로세스를 완료하는 데 얼마나 오래 걸릴지 예측하기가 어려울 수 있습니다. 예를 들어, 사용자 소유 클러스터가 일시적으로 비활성화된 경우, GitLab은 계속해서 응답을 기다리게 될 수 있으며 (예시). 이는 Puma 시간 초과를 초래할 수 있으며 반드시 피해야 합니다.

합리적인 시간 초과를 설정하고, 예외를 정상적으로 처리하고, UI에서 오류를 표시하거나 내부 로깅에서 오류를 공개해야 합니다.

ReactiveCaching를 사용하는 것은 외부 데이터를 가져오기 위한 가장 좋은 해결책 중 하나입니다.

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

요약: 데이터베이스 트랜잭션 중에 Gitaly와 같은 외부 서비스에 액세스하는 것은 피해야 합니다. 그렇지 않으면 오픈 트랜잭션은 사실상 PostgreSQL 백엔드 연결의 릴리스를 차단하게 됩니다.

가능한 한 트랜잭션을 최소화하기 위해 AfterCommitQueue 모듈이나 after_commit AR 후크를 사용하는 것을 고려해보세요.

이 예시는 트랜잭션 중에 Gitaly 인스턴스로의 요청이 ~"priority::1" 이슈를 트리거했습니다.

이그러로딩

개요: 둘 이상의 행을 검색할 때 항상 관련 항목들을 이그러로드하십시오.

하나 이상의 연관 항목을 검색할 때는 이러한 연관 항목들을 반드시 이그러로딩해야 합니다. 예를 들어, 블로그 글 목록을 가져와서 작성자를 표시하려는 경우 작성자 연관 항목들을 이그러로딩 해야 합니다.

다른 말로, 이렇게 하는 대신:

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

다음을 사용해야 합니다:

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

또한, 이그러로딩 시 회귀를 방지하기 위해 QueryRecoder tests를 사용하는 것도 고려해보십시오.

메모리 사용

개요: 병합 요청은 반드시 메모리 사용량을 절대적으로 필요한 경우를 제외하고는 증가시켜서는 안 됩니다.

병합 요청은 GitLab의 메모리 사용량을 코드에서 절대적으로 필요한 최소한으로만 증가시켜야 합니다. 즉, 어떤 큰 문서(예: HTML 문서)를 구문 분석해야 하는 경우, 가능한 경우에는 스트림으로 구문 분석하는 것이 좋습니다. 때로는 이것이 불가능할 수 있으며, 그 경우에는 병합 요청에 명시적으로 설명해야 합니다.

UI 요소의 지연된 렌더링

개요: 실제로 필요할 때에만 UI 요소를 렌더링하십시오.

일부 UI 요소는 항상 필요하지는 않을 수 있습니다. 예를 들어, 차이가 나는 행 위에 마우스를 올리면 새 주석을 달 수 있는 작은 아이콘이 표시됩니다. 이러한 종류의 요소를 항상 렌더링하는 대신 실제로 필요할 때에만 렌더링해야 합니다. 이렇게 함으로써 사용하지 않을 때에는 Haml/HTML을 생성하는 데 시간을 소비하지 않도록 합니다.

캐싱 사용

개요: 트랜잭션 내에서 여러 번 필요한 데이터는 메모리나 Redis에 캐싱해야 하거나 특정 기간 동안 유지되어야 합니다.

가끔은 트랜잭션 중에 여러 곳에서 데이터를 재사용해야 하는 경우가 있습니다. 이러한 경우에는 이러한 데이터가 복잡한 작업을 실행하지 않고 메모리에 캐싱되어야 합니다. 트랜잭션의 지속 기간 동안이 아니라 일정 기간 동안 데이터를 캐싱해야 하는 경우에는 Redis를 사용해야 합니다.

예를 들어, Hello @aliceHow are you doing @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라는 도우미 메소드가 있습니다. 이는 카운팅 행의 상한을 받아들이는데 사용될 수 있습니다.

일부 경우에는 배지 카운터가 비동기적으로 로드되는 것이 원하는 바일 수 있습니다. 이로써 초기 페이지 로드를 가속화하고 전반적으로 더 나은 사용자 경험을 제공할 수 있습니다.

기능 플래그의 사용

성능에 중요한 요소가 있는 기능이나 알려진 성능 결함이 있는 각 기능은 비활성화하기 위한 기능 플래그가 필요합니다.

기능 플래그 덕분에 우리 팀은 시스템을 모니터링하고 사용자가 문제를 인지하지 못하도록 빠르게 대응할 수 있어 더욱 행복합니다.

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

기능 플래그의 사용 시기와 방법에 대해 자세히 알아보려면 GitLab 개발에서의 기능 플래그를 참조하세요.

저장소

다음 유형의 저장소를 고려할 수 있습니다:

  • 로컬 임시 저장소 (매우 짧은 기간의 저장) 이 유형의 저장소는 /tmp 폴더와 같이 시스템에서 제공하는 저장소입니다. 모든 일시적 작업에 이 유형의 저장소를 이상적으로 사용해야 합니다. 각 노드가 자체 임시 저장소를 가지고 있기 때문에 확장이 훨씬 쉽습니다. 또한 이 저장소는 대부분 SSD 기반이므로 상당히 빠릅니다. 이 로컬 저장소는 TMPDIR 변수를 사용하여 애플리케이션에 쉽게 구성할 수 있습니다.

  • 공유 임시 저장소 (단기 저장) 이 유형의 저장소는 네트워크 기반의 임시 저장소로, 일반적으로 공통 NFS 서버에서 실행됩니다. 2020년 2월 기준으로 대부분의 구현에는 여전히 이 유형의 저장소를 사용합니다. 위의 제한을 상당히 크게 할 수 있다고 해도 실제로 그만큼 더 많이 사용할 수 있는 것은 아닙니다. 공유 임시 저장소는 모든 노드에서 공유되므로 해당 공간을 상당히 많이 사용하거나 많은 작업을 수행하는 작업이 다른 작업 및 요청 전체 응용 프로그램의 실행에 대한 충돌을 만들기 쉽게 만들면 전체 GitLab의 안정성에 영향을 줄 수 있습니다. 이를 고려해 주십시오.

  • 공유 영구 저장소 (장기 저장) 이 유형의 저장소는 공유 네트워크 기반 저장소(예: NFS)를 사용합니다. 소규모 노드 구성으로 구동되는 고객이 주로 사용하는 솔루션입니다. 공유 저장소의 파일은 쉽게 액세스할 수 있지만 데이터를 업로드하거나 다운로드하는 작업은 다른 모든 작업에 심각한 충돌을 일으킬 수 있습니다. 이는 Omnibus에서 기본적으로 사용되는 방식입니다.

  • 객체 기반 영구 저장소 (장기 저장) 이 유형의 저장소는 AWS S3와 같은 외부 서비스를 사용합니다. 객체 저장소는 무한한 확장성과 불확실성을 가지고 있는 것으로 간주될 수 있습니다. 이 저장소에 액세스하려면 일반적으로 파일을 다운로드하여 조작해야 합니다. 객체 저장소는 무제한 동시 업로드 및 파일 다운로드를 처리할 수 있다고 가정할 수 있으므로 이는 애플리케이션이 컨테이너화된 배포(Kubernetes)에서 쉽게 실행될 수 있도록 보장하기 위해 필요한 궁극적인 솔루션으로 간주됩니다.

임시 저장소

운영 노드의 저장소는 실제로 매우 한정적입니다. 애플리케이션은 매우 제한된 임시 저장소에서 실행할 수 있도록 구축되어야 합니다. 코드를 실행하는 시스템이 전체적으로 약 1GB~10GB의 임시 저장소만 가지고 있다고 생각할 수 있습니다. 그러나 이 저장소는 실제로 실행 중인 모든 작업에서 공유됩니다. 작업이 이 공간의 100MB 이상을 사용해야 한다면 선택한 방법을 다시 고려해야 합니다.

귀하의 요구 사항이 무엇이든 파일을 처리해야 한다면 명확하게 문서화해야 합니다. 100MB 이상이 필요하다면 유지관리자로부터 도움을 요청하여 가능한 더 나은 해결책을 찾는 데 도움을 받는 것을 고려해야 합니다.

로컬 임시 저장소

로컬 저장소의 사용은 특히 우리가 Kubernetes 클러스터에 애플리케이션을 배포하는 경우에 사용할 수 있는 이상적인 해결책입니다. 예를 들어 Dir.mktmpdir를 사용하려면 언제 사용해야 하나요? 예를 들어 아카이브를 추출/생성하거나 기존 데이터를 철저히 조작해야 하는 경우에 사용할 수 있습니다.

Dir.mktmpdir('designs') do |path|
  # path를 조작합니다.
  # 블록을 벗어나면 path가 자동으로 제거됩니다.
end

공유 임시 저장소

공유 임시 저장소의 사용은 디스크 기반 저장을 위해 파일을 영구적으로 저장하려는 경우에 필요합니다. 나중에 GitLab Rails가 이동 작업을 수행할 수 있습니다. 같은 대상으로의 이동 작업은 즉시 발생합니다. 애플리케이션에 추가 복잡성이 도입되기 때문에, 재구현하는 대신 잘 알려진 패턴(예: ObjectStorage 관련)을 새로 구현하는 대신 재사용해야 합니다.

기타 모든 용도에 대해 공유 임시 저장소의 사용은 현재 제한되어 있습니다.

영구 저장소

객체 저장소

영구 파일을 보유하는 모든 기능은 데이터를 객체 저장소에 저장할 수 있어야 합니다. 노드 간에 공유된 볼륨을 통한 영구 저장은 확장 가능하지 않으며 모든 노드에서 데이터 액세스를 위한 충돌을 만듭니다.

GitLab은 공유 및 객체 저장소 기반의 영구 저장을 매끄럽게 지원하는 ObjectStorage 관련을 제공합니다.

데이터 액세스

데이터를 업로드 받거나 다운로드할 수 있는 각 기능은 Workhorse 직접 업로드를 사용해야 합니다. 이는 업로드가 작업자에 의해 직접 객체 저장소에 저장되어야 하고, 모든 다운로드는 작업자에 의해 제공되어야 한다는 것을 의미합니다.

Puma를 통한 업로드/다운로드는 비용이 많이 드는 작업이며, 업로드 기간 동안 전체 처리 슬롯(스레드)을 차단합니다.

또한 Puma를 통한 업로드/다운로드는 작업이 시간 초과될 수 있는 문제가 있으며, 특히 느린 클라이언트의 경우에는 문제가 될 수 있습니다. 클라이언트가 오랜 시간을 소비하여 업로드/다운로드를 하는 경우 요청 처리 시간 초과로 인해 처리 슬롯이 종료될 수 있습니다(보통 30초에서 60초 사이).

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