- 정의
- 영향 분석
- 성능 검토
- 상상력을 발휘하세요
- 데이터 세트
- 쿼리 계획 및 데이터베이스 구조
- 쿼리 횟수
- 가능한 경우 읽기 복제본 사용
- CTE(공통 테이블 표현식)를 현명하게 사용하세요
- 캐시된 쿼리
- 루프에서 쿼리 실행
- 일괄 처리
- 시간 제한
- 데이터베이스 트랜잭션 최소화
- Eager Loading
- 메모리 사용
- UI 요소의 지연 렌더링
- 캐싱 사용
- 페이지네이션
- 배지 카운터
- 기능 플래그의 사용
- 저장소
병합 요청 성능 지침
새로 도입된 각 병합 요청은 기본적으로 성능을 고려해야 합니다.
GitLab의 성능에 부정적인 영향을 미치지 않도록 각 병합 요청은 본 문서에 기술된 지침을 준수해야 합니다. 이 규칙에는 백엔드 유지 관리자 및 성능 전문가가 명시적으로 논의하고 합의하지 않는 한 예외는 없습니다.
또한 다음 가이드를 읽는 것이 매우 권장됩니다:
정의
RFC 2119에 따르면 SHOULD
용어는 다음을 의미합니다:
이 용어 또는 “권장됨” 형용사가 특정한 상황에서 특정한 항목을 무시할 수 있는 유효한 이유가 존재할 수 있지만, 다른 방향을 선택하기 전에 그 충분한 영향을 이해하고 심사숙고해야 합니다.
이상적으로 이러한 트레이드오프는 별도의 이슈에 문서화되어야 하며 해당 내용이 라벨이 지정되어 링크된 원본 이슈 및 에픽과 연결되어야 합니다.
영향 분석
요약: 병합 요청이 GitLab 설정을 유지 관리하는 사람들 및 성능에 미치는 영향을 고려해 보세요.
제출된 모든 변경 사항은 응용 프로그램 자체뿐만 아니라 해당 응용 프로그램을 유지 관리하고 실행하는 사람들(예: 프로덕션 엔지니어)에게도 영향을 줄 수 있습니다. 결과적으로 병합 요청이 응용 프로그램 뿐만 아니라 실행 및 유지 관리에 참여하는 사람들에게 미치는 영향을 주의 깊게 생각해야 합니다.
제출한 병합 요청이 잠재적으로 중요한 서비스를 중단하고 엔지니어들이 야간에 깨어나야 하는 일을 야기할 수 있습니까? 악의적인 사용자가 코드를 남용하여 GitLab 인스턴스를 중단시킬 수 있습니까? 제가 한 변경 사항이 특정 페이지의 로드를 늦추게 만들까요? 데이터베이스에 충분한 부하 또는 데이터가 주어졌을 때 실행 시간이 기하급수적으로 증가합니까?
이러한 모든 질문은 병합 요청을 제출하기 전에 스스로에게 물어보아야 할 것입니다. 때로는 영향을 평가하기 어려울 수 있으며, 이 경우 성능 전문가에게 코드를 검토해 달라고 요청해야 합니다. 자세한 내용은 아래의 “검토” 섹션을 참조하세요.
성능 검토
요약: 영향을 확신할 수 없다면 성능 전문가에게 코드 검토를 요청하세요.
병합 요청의 영향을 평가하는 것이 어려울 수 있습니다. 이 경우 병합 요청 리뷰어 중 한 명에게 변경 사항을 검토해 달라고 요청해야 합니다. (리뷰어 목록을 확인할 수 있습니다.) 리뷰어는 다시 성능 전문가에게 변경 사항을 검토해 달라고 요청할 수 있습니다.
상상력을 발휘하세요
각자가 새로운 기능을 사용하는 방식에 대해 각자의 인식을 갖고 있습니다. 언제나 사용자가 어떻게 기능을 사용할지를 고려하세요. 보통 사용자들은 우리의 특성을 매우 비표준적인 방식으로 시험합니다. 무차별 대입 또는 우리가 가진 극한 조건을 남용함으로써.
데이터 세트
병합 요청이 처리해야 하는 데이터 세트를 알고 문서화해야 합니다. 이 기능은 해당 기능이 처리해야 하는 예상 데이터 세트 및 발생할 수 있는 문제를 명확히 문서화해야 합니다.
다음 예제를 생각해 보면, 데이터 세트가 처리되는 데 강한 강조를 두고 있습니다. 이 문제는 간단합니다: Git 저장소에서 파일 목록을 필터링하려고 합니다. 해당 기능은 저장소에서 모든 파일 목록을 요청하고 파일 집합에 대한 검색을 수행합니다. 이 맥락에서 저자는 다음 사항을 고려해야 합니다.
- 어떤 저장소를 지원할 예정인가요?
- 리눅스 커널과 같은 대형 저장소는 얼마나 오래 걸릴까요?
- 이러한 큰 데이터 세트를 처리하지 않으려면 다른 방법이 있을까요?
- 모든 사용자 대신 단일 사용자에게 서비스의 복잡성을 포함시키는 안전장치 메커니즘을 구축해야 할까요? 일반적으로 모든 사용자 대신 특정 사용자에게 서비스의 복잡성을 떨어뜨리는 것이 더 나은 방법입니다.
쿼리 계획 및 데이터베이스 구조
쿼리 계획은 추가적인 인덱스가 필요한지 또는 비용이 많이 드는 필터링(예: 순차 스캔 사용)이 필요한지 알려줄 수 있습니다.
각 쿼리 계획은 상당한 크기의 데이터 세트에 대해 실행되어야 합니다. 예를 들어, 특정 조건의 이슈를 검색한다면 수백 개의 이슈와 100,000개의 이슈와 같은 적은 수와 수천 개 수의 결과에 대한 쿼리를 유효성 검사해야 합니다.
우리는 매우 큰 프로젝트에 대해 GitLab을 사용하는 사용자들과 매우 비표준적인 방식으로 사용하는 사용자들이 있기 때문에 이 작업이 필요합니다. 이러한 거대한 데이터 세트가 사용되기는 힘들어 보이더라도, 여전히 우리의 고객 중 한 명이 기능에 문제를 겪을 가능성은 있습니다.
이러한 동작이 미리 어떻게 스케일링되는지 이해하는 것은 항상 원하는 결과입니다. 우리는 항상 보다 높은 사용 패턴에 대한 기능을 최적화하기 위해 필요한 계획이나 이해를 가져야 합니다.
모든 데이터베이스 구조는 확장을 쉽게 하기 위해 최적화되어야 하며 때로는 과도하게 설명되어야 합니다. 어느 정도 넘어선 후에 가장 어려운 부분은 데이터 마이그레이션입니다. 수백만 개의 행을 마이그레이션하는 것은 항상 문제가 있을 뿐만 아니라 응용 프로그램에 부정적인 영향을 미칠 수 있습니다.
쿼리 계획 검토를 위한 도움을 얻는 방법을 더 잘 이해하려면 데이터베이스 검토용 병합 요청을 준비하는 방법 섹션을 참조하세요.
쿼리 횟수
요약: 병합 요청이 절대적으로 필요하지 않은 한 적용된 코드에 의해 실행된 SQL 쿼리의 총 수를 증가해서는 안 됩니다.
병합 요청에 의해 수정되거나 추가된 코드에 의해 실행된 쿼리의 총 수는 증가해서는 절대로 안 됩니다. 기능을 구축할 때 추가적인 쿼리가 필요한 경우가 있을 수 있지만, 최소한으로 유지하려고 노력해야 합니다.
예를 들어, 동일한 값으로 데이터베이스 행을 업데이트하는 기능을 도입한다고 가정해 봅시다. 충분한 업데이트할 행이 있거나 이 코드의 많은 인스턴스가 병렬로 장시간 실행될 수 있어 매우 유혹적(그리고 쉬운)일 수 있습니다. 이 특정 문제는 “N+1 쿼리 문제”로 알려져 있습니다. 이를 감지하고 재발을 방지하려면 QueryRecorder를 사용하여 테스트 코드를 작성할 수 있습니다.
특정 경우에는 해결책이 상당히 쉽습니다:
objects_to_update.update_all(some_field: some_value)
이렇게 하면 ActiveRecord의 update_all
메서드를 사용하여 단일 쿼리로 모든 행을 업데이트할 수 있습니다. 결과적으로 데이터베이스가 과부하가 걸릴 가능성이 훨씬 줄어듭니다.
가능한 경우 읽기 복제본 사용
DB 클러스터에서는 여러 개의 읽기 복제본과 하나의 프라이머리가 있습니다. DB를 확장하는 고전적인 방법 중 하나는 읽기 전용 동작을 복제본에서 수행하는 것입니다. 이 부하를 분산시키기 위해 로드 밸런싱을 사용합니다. 이는 복제본이 DB에 가해지는 압력이 커짐에 따라 늘어날 수 있도록 합니다.
기본적으로 쿼리는 읽기 전용 복제본을 사용하지만, 프라이머리 스틱로 인해 GitLab은 일정 시간 동안 프라이머리를 사용하고 이후에는 복제본으로 되돌아갑니다. 이렇게 하면 프라이머리에 불필요한 부하가 발생할 수 있습니다. 프라이먨리 변경을 방지하기 위해 병합 요청 56849에서 without_sticky_writes
블록이 소개되었습니다. 일반적으로 이 방법은 동일한 세션의 후속 쿼리에 영향을 미치지 않는 사소한 또는 미미한 쓰기 작업 후에 프라이머리의 고수성을 방지하기 위해 적용할 수 있습니다.
사용 타임스탬프 업데이트가 세션을 프라이머리에 고수하게 만드는 경우와 without_sticky_writes
를 사용하여 이를 방지하는 방법에 대해 알아보려면 병합 요청 57328을 참조하십시오.
without_sticky_writes
유틸리티의 상반된 기능으로 병합 요청 59167은 use_replicas_for_read_queries
를 소개했습니다. 이 방법은 현재 프라이머리 스틱성과 관계없이 해당 블록 내의 모든 읽기 전용 쿼리를 강제로 복제본에서 읽도록 합니다. 이 유틸리티는 쿼리가 복제 지연을 용인할 수 있는 경우에 사용됩니다.
내부적으로 저희의 데이터베이스 로드 밸런서는 주요 문 (select
, update
, delete
등)을 기준으로 쿼리를 분류합니다. 의심스러운 경우에는 쿼리를 프라이머리로 리디렉션합니다. 따라서 로드 밸런서가 불필요하게 쿼리를 프라이머리로 보내는 일반적 사례가 있습니다:
- 사용자 정의 쿼리 (via
exec_query
,execute_statement
,execute
등) - 읽기 전용 트랜잭션
- 인 플라이트 연결 구성 설정
- Sidekiq 백그라운드 작업
위와 같은 쿼리를 실행한 후에 GitLab은
프라이머리에 고수하게 됩니다.
내부 쿼리가 복제본을 사용하도록 설정하려면 병합 요청 59086이
fallback_to_replicas_for_ambiguous_queries
를 소개했습니다. 이 병합 요청은 비싼 시간이 소요되는 쿼리를 복제본으로 리디렉션하는 방법의 예시도 됩니다.
CTE(공통 테이블 표현식)를 현명하게 사용하세요
CTE를 사용하는 방법에 대한 고려 사항은 반복적으로 테이블에 복잡한 쿼리를 참조하세요. 일부 상황에서 CTE가 (앞서 언급된 N+1 문제와 유사하게) 문제가 될 수 있다는 것을 발견했습니다. 특히, AuthorizedProjectsWorker의 CTE와 같은 계층적 재귀 CTE 쿼리는 매우 최적화하기 어렵고 확장되지 않습니다. 계층 구조를 필요로 하는 새로운 기능을 구현할 때 이러한 쿼리를 피해야 합니다.
CTE는 예시와 같이 많이 단순한 경우에서 최적화 펜스로 사용된 바 있습니다. 지원되는 PostgreSQL 버전에서는 MATERIALIZED
키워드로 최적화 펜스 동작을 활성화해야 합니다. 기본적으로 CTE는 내장된 후 기본적으로 최적화됩니다.
CTE 문을 작성할 때 Gitlab::SQL::CTE
클래스를 사용하세요.
기본적으로 이 Gitlab::SQL::CTE
클래스는 MATERIALIZED
키워드를 추가하여 강제로 자료화를 수행합니다.
경고: GitLab 14.0으로 업그레이드하는 데에는 PostgreSQL 12 이상이 필요합니다.
캐시된 쿼리
요약: 병합 요청은 중복된 캐시된 쿼리를 실행해서는 안됩니다.
Rails는 요청의 지속 기간 동안 데이터베이스 쿼리 결과를 캐시하는 SQL 쿼리 캐시를 제공합니다.
캐시된 쿼리가 나쁜 것으로 간주되는 이유와 이를 감지하는 방법을 살펴보세요.
병합 요청에 의해 소개된 코드는 중복된 캐시된 쿼리를 실행해서는 안됩니다.
병합 요청에 의해 수정 또는 추가된 코드에서 실행된 쿼리의 총 수(캐시된 쿼리 포함)는
절대적으로 필요한 경우를 제외하고 증가해서는 안됩니다.
실행된 쿼리의 수(캐시된 쿼리 포함)는 컬렉션 크기에 의존해서는 안됩니다.
이를 감지하고 회귀를 방지하기 위해 QueryRecorder에 skip_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, Object Storage)로의 단일 프로세스 반복은 연결 오버헤드를 줄이기 위해 일괄 스타일로 실행되어야 합니다.
일괄 스타일로 여러 테이블에서 행을 가져올 때 Eager Loading 섹션을 참조하세요.
예: Object Storage에서 여러 파일 삭제
여러 파일을 Object Storage에서 삭제할 때 GCS와 같은, 단일 REST API 호출을 여러 번 실행하는 것은 상당히 비용이 많이 드는 프로세스입니다. 이 경우 S3와 같이 일괄 삭제 API가 제공되므로 이와 같은 방식을 고려하는 것이 좋은 아이디어일 수 있습니다.
FastDestroyAll
모듈은 이 상황에 도움이 될 수 있습니다. 이는 일괄 스타일로 데이터베이스 행과 관련 데이터를 제거할 때 사용되는 작은 프레임워크입니다.
시간 제한
개요: 시스템이 외부 서비스(예: Kubernetes)로 HTTP 호출을 시작할 때 합리적인 시간 제한을 설정해야 하며, 이는 Puma 스레드가 아닌 Sidekiq에서 실행되어야 합니다.
GitLab은 종종 Kubernetes 클러스터와 같은 외부 서비스와 통신해야 할 수 있습니다. 이 경우, 요청된 프로세스를 외부 서비스에서 완료하는 데 언제쯤이 될지 추정하기 어려울 수 있습니다. 예를 들어 사용자 소유의 클러스터가 어떤 이유로 비활성화된 경우 GitLab은 응답을 영원히 기다리게 될 수 있습니다 (예시). 이는 Puma 시간 제한을 초래할 수 있으며 가능한 경우 반드시 피해야 합니다.
합리적인 시간 제한을 설정하고, 예외를 정상적으로 처리하고, UI나 내부 로깅에 오류를 표시해야 합니다.
외부 데이터를 가져오는 가장 좋은 솔루션 중 하나는 ReactiveCaching
을 사용하는 것입니다.
데이터베이스 트랜잭션 최소화
개요: 데이터베이스 트랜잭션 중에 Gitaly와 같은 외부 서비스에 액세스하는 것을 피해야 합니다. 그렇지 않으면, 오픈된 트랜잭션이 기본적으로 PostgreSQL 백엔드 연결의 해제를 차단하므로 심각한 경합 문제로 이어질 수 있습니다.
가능한 한 트랜잭션을 최소화하기 위해 AfterCommitQueue
모듈 또는 after_commit
AR 후크를 사용하는 것을 고려해보세요.
여기에 한 예시가 있습니다. 트랜잭션 중 Gitaly 인스턴스로의 하나의 요청이 ~"priority::1" 이슈를 트리거했습니다.
Eager Loading
개요: 하나 이상의 행을 검색할 때 항상 연관성을 경매로 즉시 로드해야 합니다.
하나 이상의 행을 검색할 때 필요한 연관성을 즉시 로드해야 합니다. 예를 들어, 게시물 목록을 검색하고 해당 저자를 표시하려는 경우 저자 연관성을 즉시 로드해야 합니다.
즉, 이렇게 하는 대신:
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를 사용하여 회귀를 방지하는 것도 고려해야 합니다.
메모리 사용
개요: 병합 요청은 반드시 메모리 사용량을 절대 필요 이상으로 늘리면 안됩니다.
병합 요청은 코드에서 절대 필요한 최소한의 메모리 사용량을 초과해서는 안 됩니다. 즉, 큰 문서(예: HTML 문서)를 구문 분석해야 하는 경우 가능한 경우 스트림으로 구문 분석하는 것이 좋습니다. 때로는 이것이 불가능할 수 있으며, 이 경우 병합 요청에서 명시적으로 명시되어야 합니다.
UI 요소의 지연 렌더링
개요: 필요할 때에만 UI 요소를 렌더링해야 합니다.
일부 UI 요소는 항상 필요하지 않을 수 있습니다. 예를 들어, 차이 라인 위에 마우스를 올릴 때 새로운 코멘트를 작성하는 데 사용할 수 있는 작은 아이콘이 표시됩니다. 이러한 종류의 요소는 실제로 필요할 때에만 렌더링되어야 합니다. 이렇게 함으로써 사용되지 않을 때 Haml/HTML을 생성하는 시간을 절약할 수 있습니다.
캐싱 사용
개요: 트랜잭션 내에서 여러 번 필요한 데이터이거나 일정 기간 동안 보관되어야 하는 데이터는 메모리나 Redis에 캐싱해야 합니다.
가끔은 트랜잭션 중에 서로 다른 위치에서 데이터를 재사용해야 할 때가 있습니다. 이러한 경우 이러한 데이터는 복잡한 작업을 실행하지 않고도 데이터를 가져오기 위해 메모리에 캐시되어야 합니다. 데이터가 트랜잭션의 기간이 아닌 특정 기간 동안 캐시되어야 하는 경우 Redis를 사용해야 합니다.
예를 들어, 사용자 이름 언급을 포함하는 여러 조각의 텍스트를 처리해야 할 수 있습니다(Hello @alice
및 How are you doing @alice?
과 같은). 각 사용자 이름에 대한 사용자 객체를 캐시함으로써 @alice
를 언급할 때마다 동일한 쿼리를 실행할 필요없이 할 수 있습니다.
트랜잭션 당 데이터를 캐시하려면 RequestStore를 사용할 수 있습니다(RequestStore.active?
을 확인하지 않아도 되도록 Gitlab::SafeRequestStore
를 사용하세요). Redis에 데이터를 캐시하려면 Rails’ caching system을 사용할 수 있습니다.
페이지네이션
테이블로 항목 목록을 렌더링하는 각 기능은 페이지네이션을 포함해야 합니다.
주요 페이지네이션 스타일은 다음과 같습니다:
- 오프셋 기반 페이지네이션: 사용자가 특정 페이지(예: 1)로 이동합니다. 사용자는 다음 페이지 번호와 총 페이지 수를 볼 수 있습니다. 이 스타일은 GitLab의 모든 구성 요소에서 잘 지원됩니다.
- 오프셋 기반 페이지네이션, 그러나 카운트가 없는 경우: 사용자가 특정 페이지(예: 1)로 이동합니다. 사용자는 다음 페이지 번호만 볼 수 있으며 총 페이지 수는 표시되지 않습니다.
- 키셋 기반 다음 페이지: 사용자는 사용 가능한 페이지 수를 알 수 없기 때문에 다음 페이지로만 이동할 수 있습니다.
- 무한 스크롤 페이지네이션: 사용자가 페이지를 스크롤하면 다음 항목이 비동기적으로 로드됩니다. 이것이 이전 스타일과 완전히 동일한 이점이 있기 때문에 이것이 이상적입니다.
페이지네이션의 궁극적으로 확장 가능한 솔루션은 키셋 기반 페이지네이션을 사용하는 것입니다. 그러나 현재 GitLab에서는 이에 대한 지원이 없습니다. API: Keyset Pagination의 진행 상황을 확인할 수 있습니다.
페이지네이션 전략을 선택할 때 다음 사항을 고려해야 합니다:
- 필터링된 객체 수를 계산하는 것은 매우 비효율적입니다. 이 작업은 일반적으로 몇 초가 걸리며 시간이 초과될 수 있습니다.
- 더 높은 순서의 페이지를 위해 항목을 가져오는 것은 매우 비효율적입니다. 데이터베이스는 모든 이전 항목을 정렬하고 반복해야 하며, 이 작업은 일반적으로 데이터베이스에 상당한 부하를 일으킬 수 있습니다.
페이지네이션과 관련된 유용한 팁은 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)에서 쉽게 실행될 수 있도록 하기 위해 필요한 최종 솔루션입니다.
임시 저장소
프로덕션 노드상의 저장소는 실제로 매우 희박합니다. 애플리케이션은 매우 제한적인 임시 저장소에서 실행될 수 있도록 구축되어야 합니다. 소스 코드가 실행되는 시스템은 일시적 저장소의 1G-10G
전체를 가지고 있다고 예상할 수 있습니다. 그러나 이 저장소는 실제로 모든 실행 중인 작업을 통틀어 공유되는 것입니다. 작업이 이 공간의 100MB
보다 많이 사용해야 하는 경우, 사용한 접근 방식을 다시 고려해야 합니다.
당신의 요구 사항이 무엇이든지, 만약 파일을 처리해야 한다면 명확히 문서화해야 합니다. 100MB
보다 많이 필요하다면 관리자에게 도움을 요청하여 더 나은 해결책을 찾아볼 수 있도록 협력해야 합니다.
로컬 임시 저장소
로컬 저장소의 사용은 특히 우리가 쿠버네티스 클러스터로 애플리케이션을 배포하는 경우에 사용할 만한 솔루션입니다. Dir.mktmpdir
을 사용하고 싶을 때는 언제인가요? 예를 들어, 아카이브를 추출/생성하거나 기존 데이터를 체계적으로 조작하려는 경우입니다.
Dir.mktmpdir('designs') do |path|
# 경로에서 조작을 수행합니다.
# 블록을 벗어나면 해당 경로가 즉시 제거됩니다.
end
공유 임시 저장소
공유 임시 저장소의 사용은 디스크 기반 저장을 위해 파일을 영구적으로 저장하려는 경우에 필요합니다. Workhorse 직접 업로드가 파일을 수용할 때, 이는 공유 저장소에 파일을 작성한 뒤 나중에 GitLab Rails가 이동 작업을 수행할 수 있습니다. 동일한 대상에 대한 이동 작업은 순간적입니다. 시스템은 복사
작업 대신 파일을 새 위치에 다시 연결합니다.
이것은 추가 복잡성을 애플리케이션에 도입하기 때문에, 재구현하는 대신에 잘 알려진 패턴(예: ObjectStorage
concern)을 재사용해야 합니다.
그 외의 다른 용도로는 공유 임시 저장소의 사용은 더 이상 권장되지 않습니다.
영구 저장소
객체 저장소
모든 영구 파일을 보유한 기능이 객체 저장소로 데이터를 저장할 수 있어야 하는 것이 필요합니다. 노드 간에 공유된 볼륨 형태의 영구 저장소를 갖는 것은 확장 가능하지 않으며, 이는 모든 노드에 대한 데이터 액세스 경합을 일으킵니다.
GitLab은 공유 및 객체 기반 영구 저장소에 대한 원활한 지원을 구현하는 ObjectStorage concern을 제공합니다.
데이터 액세스
데이터 업로드를 수용하거나 다운로드하는 모든 기능은 Workhorse 직접 업로드를 사용해야 합니다. 즉, 업로드는 Workhorse에 의해 직접 객체 저장소에 저장되어야 하며, 모든 다운로드는 Workhorse에 의해 제공되어야 합니다.
Puma를 통한 업로드/다운로드는 비용이 많이 드는 작업입니다. 왜냐하면 업로드 기간 동안 전체 처리 슬롯(쓰레드)을 차단하기 때문입니다.
또한 업로드/다운로드를 Puma를 통해 수행하면 작업이 타임아웃될 수 있는 문제가 있습니다. 특히 느린 클라이언트의 경우에 문제가 됩니다. 클라이언트가 업로드/다운로드하는 데 오랜 시간이 걸린다면, 요청 처리 타임아웃(일반적으로 30초-60초 사이)으로 인해 처리 슬롯이 종료될 수 있습니다.
위의 이유로 모든 파일 업로드 및 다운로드에 대해 Workhorse 직접 업로드가 구현되어야 합니다.