- 정의
- 영향 분석
- 성능 검토
- 상상의 범위를 넘어서 생각해보세요
- 데이터 집합
- 쿼리 계획 및 데이터베이스 구조
- 쿼리 횟수
- 가능한 경우 읽기 복제본 사용
- CTE를 현명하게 사용하기
- 캐시된 쿼리
- 루프에서 쿼리 실행
- 일괄 처리
- 시간 초과
- 데이터베이스 트랜잭션 최소화
- 일괄 로딩
- 메모리 사용
- UI 요소의 지연된 렌더링
- 캐싱의 사용
- 페이지네이션
- 배지 카운터
- 피처 플래그 사용
- 리포지터리
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 리포지터리의 파일 디렉터리을 필터링하려고 합니다. 여러분의 기능은 리포지터리에서 모든 파일 디렉터리을 요청하고 파일 집합을 검색합니다. 여기서 고려해야 할 사항은 다음과 같습니다:
- 어떤 리포지터리들을 지원할 계획인가요?
- 리눅스 커널과 같은 대형 리포지터리는 얼마나 걸릴까요?
- 이렇게 큰 데이터 집합을 처리하지 않고 다른 방법은 없을까요?
- 계산 복잡성을 제한하는 것이 더 좋다면, 계산 복잡성을 포함하는 안전장치 메커니즘을 구축해야 할까요? 보통 모든 사용자가 아닌 단일 사용자를 위해 서비스를 저하시키는 것이 더 좋습니다.
쿼리 계획 및 데이터베이스 구조
쿼리 계획을 통해 추가적인 인덱스가 필요한지, 또는 순차 검색과 같이 비용이 많이 드는 필터링이 필요한지 알 수 있습니다.
각 쿼리 계획은 상당한 양의 데이터 집합에 대해 실행되어야 합니다. 예를 들어, 특정 조건을 갖는 이슈를 찾는 경우, 몇 개에서는 쿼리를 확인하고 수천 개에서는 쿼리를 확인해야 합니다.
이것은 GitLab을 사용하는 사용자가 매우 큰 프로젝트에서 매우 관행적인 방법으로 사용한다는 것을 감안할 때 필요합니다. 즉, 정말 그런 큰 데이터 집합을 사용하는 것은 불가능해 보이지만, 여전히 가능성이 있는 거나 고객 중 한 명이 이 기능에 문제를 겪을 수 있는 가능성이 있습니다.
확장형 사용 패턴을 위해 기능을 최적화하기 위해 필요한 계획이나 이해를 미리 알아야 합니다.
모든 데이터베이스 구조는 쉬운 확장을 위해 최적화되어야 하며, 때로는 과도하게 설명되어야 합니다. 특정 시점 이후의 가장 어려운 부분은 데이터 이전입니다. 수백만 개의 행을 이동하는 것은 항상 문제가 될 수 있습니다. 데이터베이스 리뷰를 위한 Merge Request을 준비하는 방법에 대해 자세히 알아보려면 데이터베이스 리뷰를 위한 Merge Request을 준비하는 방법 섹션을 참조하세요.
쿼리 횟수
요약: Merge Request에서 반드시 필요하지 않은 한 실행된 전체 SQL 쿼리 횟수를 증가시키면 안 됩니다.
Merge Request에서 수정하거나 추가한 코드로 실행된 쿼리의 총 수는 반드시 필요한 경우를 제외하고는 증가해서는 안 됩니다. 새로운 기능을 개발할 때, 추가적인 쿼리가 필요할 수 있지만, 이를 최소화하려고 노력해야 합니다.
예를 들어, 동일한 값을 가진 데이터베이스 행의 수를 업데이트하는 기능을 도입한다고 가정해보겠습니다. 충분한 업데이트할 행이 있거나 병렬로 실행되는 코드의 경우 데이터베이스에 부하가 많이 걸릴 수 있으므로 다음과 같은 가짜 코드를 작성하는 것이 매우 유혹적이고 쉬울 수 있습니다.
objects_to_update.each do |object|
object.some_field = some_value
object.save
end
이는 업데이트할 객체마다 하나의 쿼리를 실행하는 것을 의미합니다. 충분한 행을 업데이트하는 경우 또는 이 코드의 여러 인스턴스가 병렬로 실행되는 경우 데이터베이스를 쉽게 과부하로 만들 수 있습니다. 이러한 문제를 일컬으며 “N+1 query problem”라고 합니다. 이를 감지하고 회귀를 방지하기 위해 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을 확인하세요. 이 Merge요청은 코스트가 많이 드는 쿼리를 복제본으로 리디렉션하는 방법의 예이기도 합니다.
CTE를 현명하게 사용하기
일괄 처리된 테이블에서 복잡한 쿼리에 대해 읽어보고 CTE를 사용하는 방법에 대한 고려 사항을 확인하십시오. 일부 상황에서 CTE가 문제가 될 수 있음을 발견했습니다 (위의 N+1 문제와 유사함). 특히 AuthorizedProjectsWorker의 CTE와 같은 계층적 재귀 CTE 쿼리는 최적화하기 매우 어려우며 확장이 어렵습니다. 계층 구조를 필요로 하는 새로운 기능을 구현할 때에는 이러한 쿼리를 피해야 합니다.
CTE는 많은 단순한 경우에서 최적화 방법으로 효과적으로 사용되어 왔으며, 다음 예시와 같은 경우가 있습니다. 현재 지원되는 PostgreSQL 버전에서는 MATERIALIZED
키워드로 최적화 방법을 활성화해야 합니다. 기본적으로 CTE는 기본적으로 내부에서 최적화됩니다.
CTE 문을 작성할 때, Gitlab::SQL::CTE
클래스를 사용하십시오.
기본적으로 이 Gitlab::SQL::CTE
클래스는 MATERIALIZED
키워드를 추가하여 자료화를 강제합니다.
캐시된 쿼리
요약: 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
각 빌드에 대해 프로젝트 개체를 다시 초기화하기 때문에 캐시된 SQL 쿼리를 피하고 동일한 프로젝트 개체를 메모리 상에서 재인스턴스화합니다.
이 특정 경우의 해결책은 상당히 간단합니다:
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 쿼리를 실행하면 반복 횟수에 따라 많은 쿼리가 실행될 수 있습니다. 이는 데이터가 적은 개발 환경에서는 잘 작동할 수 있지만, 운영 환경에서는 이는 빠르게 컨트롤을 벗어날 수 있습니다.
이를 필요로 하는 경우가 있는데, 이 경우 Merge Request 설명에 명확하게 언급해야 합니다.
일괄 처리
요약: 외부 서비스(예: PostgreSQL, Redis, 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" 이슈를 트리거했습니다.
일괄 로딩
요약: 여러 개의 행을 검색할 때는 항상 연결을 일괄 로드해야 합니다.
필요한 경우와 연관된 모든 데이터베이스 레코드를 검색할 때에는 필히 이러한 연결을 일괄 로드해야 합니다. 예를 들어, 블로그 게시물 디렉터리을 검색하고 작성자를 표시해야 하는 경우, 작성자 연관을 반드시 일괄로 로드해야 합니다.
다음의 예시처럼 이대로 하지 말고:
Post.all.each do |post|
puts post.author.name
end
다음의 예시처럼 하십시오:
Post.all.includes(:author).each do |post|
puts post.author.name
end
또한, 일괄 로딩 시 회귀를 방지하기 위해 QueryRecoder 테스트를 고려하십시오.
메모리 사용
요약: Merge Request은 절대적으로 필요한 경우를 제외하고 메모리 사용량을 증가해서는 안 됩니다.
Merge Request은 코드가 필요로 하는 절대 최소한의 메모리 사용량을 넘어서서는 안 됩니다. 예를 들어, 큰 문서(예: HTML 문서)를 구문 분석해야 하는 경우, 가능한 경우에는 스트림으로 구문 분석하는 것이 좋습니다. 때로는 이것이 불가능하기도 하며, 이 경우에는 Merge Request에서 명시적으로 명시되어야 합니다.
UI 요소의 지연된 렌더링
요약: 실제로 필요할 때만 UI 요소를 렌더링합니다.
특정 UI 요소는 항상 필요하지 않을 수 있습니다. 예를 들어, 차이 라인 위에 마우스를 올리면 새 댓글을 작성할 수 있는 작은 아이콘이 표시됩니다. 이러한 종류의 요소를 항상 렌더링하는 대신 필요한 경우에만 렌더링해야 합니다. 이렇게 함으로써 사용되지 않을 때 Haml/HTML을 생성하는 데 걸리는 시간을 절약할 수 있습니다.
캐싱의 사용
요약: 트랜잭션 중에 데이터가 여러 번 필요하거나 일정 시간 동안 유지되어야 할 경우, 데이터를 메모리나 Redis에 캐시합니다.
가끔은 트랜잭션 중에 데이터를 여러 곳에서 재사용해야 하는 경우가 있습니다. 이러한 경우에는 해당 데이터를 메모리에 캐시하여 데이터를 검색하는 복잡한 작업을 제거해야 합니다. 트랜잭션의 기간이 아닌 일정 기간 동안 데이터를 캐시해야 하는 경우 Redis를 사용해야 합니다.
예를 들어, 사용자 이름 언급을 포함하는 여러 텍스트 조각(예: Hello @alice
및 How are you doing @alice?
)를 처리한다고 가정해봅시다. 각 사용자 이름에 대해 사용자 객체를 캐시함으로써 @alice
를 언급할 때마다 동일한 쿼리를 실행하는 필요성을 제거할 수 있습니다.
각 트랜잭션별 데이터 캐싱은 RequestStore(RequestStore.active?
를 확인할 필요 없이 Gitlab::SafeRequestStore
사용)를 사용하여 수행할 수 있습니다. Redis에서 데이터를 캐싱하는 방법은 Rails 캐싱 시스템을 사용하여 수행할 수 있습니다.
페이지네이션
테이블로 항목 디렉터리을 렌더링하는 각 기능은 페이지네이션을 포함해야 합니다.
주요 페이지네이션 스타일은 다음과 같습니다:
- 오프셋 기반 페이지네이션: 사용자가 특정 페이지(예: 1)로 이동합니다. 사용자는 다음 페이지 번호와 전체 페이지 수를 볼 수 있습니다. 이 스타일은 GitLab의 모든 컴포넌트에서 완벽하게 지원됩니다.
- 오프셋 기반 페이지네이션(카운트 없음): 사용자가 특정 페이지(예: 1)로 이동합니다. 사용자는 다음 페이지 번호만 볼 수 있고 전체 페이지 수는 표시되지 않습니다.
- 키셋 기반 페이지네이션을 사용한 다음 페이지: 사용자는 다음 페이지로만 이동할 수 있습니다. 가능한 페이지 수를 알 수 없기 때문입니다.
- 무한 스크롤 페이지네이션: 사용자가 페이지를 스크롤하면 다음 항목이 비동기적으로 로드됩니다. 이는 이전 방법과 완전히 동일한 이점을 가지므로 이상적입니다.
페이지네이션의 최종적으로 확장 가능한 해결책은 키셋 기반 페이지네이션 사용입니다. 그러나 현재 GitLab에서는 이를 지원하지 않습니다. API: Keyset Pagination에서 진행 상황을 확인할 수 있습니다.
페이지네이션 전략을 선택할 때 다음 사항을 고려해야 합니다:
- 필터링을 통과하는 객체 수를 계산하는 것은 매우 비효율적이며 이 작업은 보통 몇 초가 걸릴 수 있고 타임아웃될 수 있습니다.
- 더 높은 순서의 페이지를 위한 항목을 가져오는 것은 매우 비효율적입니다. 데이터베이스는 모든 이전 항목을 정렬하고 반복해야 하며 이로 인해 데이터베이스에 상당한 부하가 발생할 수 있습니다.
페이지네이션과 관련된 유용한 팁은 페이지네이션 가이드라인에서 찾을 수 있습니다.
배지 카운터
카운터는 항상 줄여야 합니다. 정확한 수를 표시하는 대신 일정 임계값 이상의 수를 표시하고 싶지 않습니다. 이는 정확한 항목 수를 계산하려면 각 항목을 필터링해야 하기 때문입니다.
~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가 이동 작업을 수행할 수 있습니다. 동일한 대상에서의 이동 작업은 즉각적으로 이루어집니다. 시스템은 단순히 파일을 새 위치로 다시 연결하는 ‘복사’ 작업 대신 ‘이동’ 작업을 수행합니다.
애플리케이션에 추가 복잡성을 도입하므로 재구현하는 대신 (예: ObjectStorage
관심사) 이미 잘 확립된 패턴을 재사용해야 합니다.
공유 임시 리포지터리의 사용은 그 외 모든 용도에 대해 사용이 중단되었습니다.
지속적인 리포지터리
객체 리포지터리
지속적인 파일을 보유하는 모든 기능은 데이터를 객체 리포지터리에 저장할 수 있어야만 합니다. 노드 전체에 걸쳐 공유 볼륨으로 지속적인 리포지터리를 갖는 것은 확장 가능하지 않으며, 모든 노드에서 데이터 액세스에 경합을 일으킵니다.
GitLab은 공유 및 객체 리포지터리 기반 지속적인 리포지터리를 신속하게 지원하는 ObjectStorage 관심사를 제공합니다.
데이터 액세스
데이터 업로드를 수락하거나 다운로드를 허용하는 모든 기능은 Workhorse 직접 업로드를 사용해야 합니다. 이는 업로드가 Workhorse에 의해 바로 객체 리포지터리에 저장되어야 하며, 모든 다운로드는 Workhorse에 의해 제공되어야 함을 의미합니다.
Puma를 통한 업로드/다운로드는 전체 처리 슬롯(스레드)을 차단하여 실행 기간 동안 비용이 많이 듭니다.
또한 Puma를 통한 업로드/다운로드는 작업에 시간 초과가 발생할 수 있으며, 특히 느린 클라이언트의 경우 문제가 될 수 있습니다. 클라이언트가 오랜 시간을 업로드/다운로드하는 경우 요청 처리 제한 시간(보통 30초~60초)으로 인해 처리 슬롯이 종료될 수 있습니다.
위의 이유로 모든 파일 업로드 및 다운로드에 대해 Workhorse 직접 업로드가 구현되어야 합니다.