- 정의
- 영향 분석
- 성능 리뷰
- 기존의 사고방식에서 벗어나 생각하세요
- 데이터 세트
- 쿼리 계획 및 데이터베이스 구조
- 쿼리 수
- 가능하면 읽기 복제본 사용
- CTE를 현명하게 사용하세요
- 캐시된 쿼리
- 루프에서 쿼리 실행
- 배치 프로세스
- 타임아웃
- 데이터베이스 트랜잭션 최소화
- Eager Loading
- 메모리 사용량
- UI 요소의 지연 렌더링
- 캐싱 사용
- 페이지네이션
- 배지 카운터
- 기능 플래그 사용
- 저장소
병합 요청 성능 가이드라인
각 신규 도입된 병합 요청 은 기본적으로 성능이 뛰어나야 합니다.
병합 요청이 GitLab의 성능에 부정적인 영향을 미치지 않도록 하기 위해서는
모든 병합 요청 은 이 문서에 설명된 가이드라인을 준수해야 합니다.
이 규칙에 대한 예외는 백엔드 유지 관리자가 및 퍼포먼스 전문가와 특별히 논의되고 합의된 경우 외에는 없습니다.
또한 다음 가이드를 읽는 것이 강력히 권장됩니다:
정의
SHOULD
라는 용어는 RFC 2119에 따라 다음과 같은 의미입니다:
이 단어 또는 형용사 “RECOMMENDED”는 특정 상황에서 특정 항목을 무시할 수 있는 타당한 이유가 존재할 수 있음을 의미하지만, 다른 경로를 선택하기 전에 전체 의미를 이해하고 신중하게 저울질해야 합니다.
이러한 트레이드오프는 이상적으로는 별도의 이슈로 문서화되어 적절히 라벨이 붙여지고 원래 이슈 및 에픽에 연결되어야 합니다.
영향 분석
요약: 귀하의 병합 요청이 성능에 미칠 수 있는 영향을 생각해 보세요
및 GitLab 설정을 유지 관리하는 사람들에 대한 영향입니다.
제출된 모든 변경 사항은 애플리케이션 자체뿐만 아니라 이를 유지 관리하는 사람들 및 이를 운영하는 사람들에게도 영향을 미칠 수 있습니다(예: 프로덕션 엔지니어).
결과적으로 귀하는 자신의 병합 요청이 단지 애플리케이션뿐만 아니라 이를 운영하는 사람들에게 미치는 영향에 대해 신중하게 생각해야 합니다.
쿼리가 잠재적으로 어떤 중요한 서비스를 중단시키고 엔지니어가 밤에 일어나는 결과를 초래할 수 있습니까?
악의적인 사용자가 코드를 악용하여 GitLab 인스턴스를 중단시킬 수 있습니까?
저의 변경 사항이 특정 페이지의 로딩을 느리게 만듭니까?
주어진 충분한 부하나 데이터가 데이터베이스에 있을 때 실행 시간이 기하급수적으로 증가합니까?
이 모든 질문은 병합 요청을 제출하기 전에 스스로에게 물어봐야 할 질문입니다.
가끔씩 영향을 평가하기 어려운 경우, 퍼포먼스 전문가에게 코드 리뷰를 요청해야 합니다.
자세한 내용은 아래의 “리뷰” 섹션을 참조하세요.
성능 리뷰
요약: 영향에 대해 확신이 없을 경우 성능 전문가에게 코드를 리뷰해 달라고 요청하세요.
가끔은 병합 요청의 영향을 평가하기 어려울 수 있습니다.
이 경우 병합 요청 검토자 중 한 명에게 변경 사항을 검토해 달라고 요청해야 합니다.
(검토자 목록이 제공됩니다.)
검토자는 변경 사항을 리뷰하기 위해 퍼포먼스 전문가에게 요청할 수 있습니다.
기존의 사고방식에서 벗어나 생각하세요
모두 자신만의 방식으로 새로운 기능을 사용하는 인식이 있습니다.
반드시 사용자가 기능을 사용하는 방식을 고려하세요.
대개 사용자는 우리의 기능을 매우 비정형적인 방법,
예를 들어 무차별 대입 공격이나 우리가 감안한 엣지 조건을 악용하여 테스트합니다.
데이터 세트
병합 요청이 처리하는 데이터 세트는 명확하게 알려지고 문서화되어야 합니다.
기능은 어떤 데이터 세트를 처리하는 것이 기대되는지 및 어떤 문제가 발생할 수 있는지를 명확하게 문서화해야 합니다.
다음 예제를 생각해 보세요.
이 예제는 처리되는 데이터 세트의 중요성을 강조합니다.
문제는 간단합니다: Git 저장소에서 파일 목록을 필터링하려고 합니다.
귀하의 기능은 저장소의 모든 파일 목록을 요청하고 파일 세트를 검색합니다.
작성자로서 귀하는 해당 문제에 대한 문맥에서 다음 사항을 고려해야 합니다:
- 어떤 저장소가 지원될 계획인가요?
- 리눅스 커널과 같은 큰 저장소는 얼마나 걸리나요?
- 이렇게 큰 데이터 세트를 처리하지 않기 위해 다른 방법이 있습니까?
- 계산 복잡성을 제한하기 위해 어떤 실패 방지 메커니즘을 구축해야 하나요?
일반적으로 모든 사용자보다 단일 사용자에 대해 서비스가 저하되는 것이 더 좋습니다.
쿼리 계획 및 데이터베이스 구조
쿼리 계획은 추가적인 인덱스가 필요한지, 또는 비싼 필터링(예: 순차 스캔 사용)이 필요한지를 알려줍니다.
각 쿼리 계획은 상당한 크기의 데이터 세트에 대해 실행되어야 합니다.
예를 들어, 특정 조건의 문제를 찾고 있다면, 작은 숫자(몇 백 개)와 큰 숫자(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를 확장하는 전형적인 방법은 읽기 전용 작업을 복제본에서 수행하는 것입니다.
우리는 로드 밸런싱 을 사용하여 이 로드를 분산합니다.
이렇게 하면 DB의 압력이 증가함에 따라 복제본이 증가할 수 있습니다.
기본적으로 쿼리는 읽기 전용 복제본을 사용하지만, 기본 스티킹 때문에 GitLab은 일정 시간 동안 기본을 사용하고, 복제본이 따라잡거나 30초 후에 복제본으로 되돌아갑니다.
이것은 기본에 불필요한 부하를 초래할 수 있습니다.
기본으로 전환을 방지하기 위해 병합 요청 56849에서는
without_sticky_writes
블록을 도입했습니다.
일반적으로, 이 메서드는 동일한 세션의 이후 쿼리에 영향을 미치지 않는 사소한 또는 중요하지 않은 쓰기 작업 후 기본 스티킹을 방지하는 데 적용될 수 있습니다.
사용 타임스탬프 업데이트가 세션이 기본에 고착될 수 있는 방법과 without_sticky_writes
를 사용하여 이를 방지하는 방법에 대해 알아보려면 병합 요청 57328 를 참조하세요.
without_sticky_writes
유틸리티의 대응으로,
병합 요청 59167에서는
use_replicas_for_read_queries
를 도입했습니다.
이 메서드는 모든 읽기 전용 쿼리가 현재 기본 스티킹과 관계없이 복제본을 읽도록 강제합니다.
이 유틸리티는 쿼리가 복제 지연을 감내할 수 있는 경우에만 예약되어 있습니다.
내부적으로, 우리의 데이터베이스 로드 밸런서는 쿼리를 주요 문(statement) (select
, update
, delete
등)에 따라 분류합니다.
의심스러운 경우, 쿼리를 기본 데이터베이스로 리디렉션합니다.
따라서 로드 밸런서가 쿼리를 불필요하게 기본으로 전송하는 일반적인 경우가 있습니다:
- 사용자 정의 쿼리 (
exec_query
,execute_statement
,execute
등 사용) - 읽기 전용 트랜잭션
- 진행 중인 연결 구성 설정
- Sidekiq 백그라운드 작업
위의 쿼리가 실행된 후, GitLab은 기본으로 고착됩니다.
쿼리가 복제본을 선호하도록 만들기 위해,
병합 요청 59086에서는
fallback_to_replicas_for_ambiguous_queries
를 도입했습니다.
이 MR은 또한 쿼리를 비싼, 시간 소모적인 쿼리에서 복제본으로 리디렉션한 예입니다.
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 쿼리 캐시를 제공합니다.
캐시된 쿼리가 나쁜 이유를 알아보세요 및 이를 감지하는 방법을 확인하세요.
병합 요청에 의해 도입된 코드는 중복된 캐시된 쿼리를 여러 번 실행해서는 안 됩니다.
병합 요청에 의해 수정되거나 추가된 코드에서 실행되는 쿼리의 총 수(캐시된 쿼리 포함)는 반드시 필요하지 않은 한 증가해서는 안 됩니다. 실행된 쿼리의 수(캐시된 쿼리 포함)는 컬렉션 크기에 따라 달라져서는 안 됩니다.
이것을 감지하고 회귀를 방지하기 위해 skip_cached
변수를 QueryRecorder로 전달하여 테스트를 작성할 수 있습니다.
예를 들어, CI 파이프라인이 있다고 가정해 보겠습니다. 모든 파이프라인 빌드는 동일한 파이프라인에 속하므로 동일한 프로젝트에 속합니다(pipeline.project
):
pipeline_project = pipeline.project
# 프로젝트 로드 (0.6ms) SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2
build = pipeline.builds.first
build.project == pipeline_project
# 캐시 프로젝트 로드 (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에서 여러 파일 삭제
GCS와 같은 Object Storage에서 여러 파일을 삭제할 때, 단일 REST API 호출을 여러 번 실행하는 것은 상당히 비용이 많이 드는 프로세스입니다. 이상적으로는, S3에서 제공하는 배치 삭제 API와 같이 배치 스타일로 수행하는 것이 좋습니다.
FastDestroyAll
모듈이 이 상황에서 도움이 될 수 있습니다. 이는 데이터베이스 행과 연관된 데이터를 배치 스타일로 제거할 때 사용하는 작은 프레임워크입니다.
타임아웃
요약: 시스템이 외부 서비스(예: Kubernetes)에 HTTP 호출을 할 때 합리적인 타임아웃을 설정해야 하며, Sidekiq에서 실행해야 하고 Puma 스레드에서는 실행해서는 안 됩니다.
종종 GitLab은 Kubernetes 클러스터와 같은 외부 서비스와 통신해야 합니다. 이 경우, 외부 서비스가 요청된 프로세스를 완료할 때까지의 시간을 예측하기 어렵습니다. 예를 들어, 어떤 이유로 활성화되지 않은 사용자가 소유한 클러스터의 경우, GitLab은 응답을 영원히 기다릴 수 있습니다 (예시). 이는 Puma 타임아웃을 초래할 수 있으며, 이는 모든 경우에 피해야 합니다.
합리적인 타임아웃을 설정하고, 예외를 우아하게 처리하며, UI나 내부 로깅에서 오류를 표출해야 합니다.
외부 데이터를 가져오는데 ReactiveCaching
사용하는 것이 가장 좋은 해결책 중 하나입니다.
데이터베이스 트랜잭션 최소화
요약: 데이터베이스 트랜잭션 중에는 Gitaly와 같은 외부 서비스에 접근하는 것을 피해야 합니다. 그렇지 않으면 열린 트랜잭션이 PostgreSQL 백엔드 연결 해제를 기본적으로 차단하여 심각한 경합 문제를 초래할 수 있습니다.
트랜잭션을 가능한 최소화하려면 AfterCommitQueue
모듈이나 after_commit
AR 훅을 사용하는 것을 고려하세요.
트랜잭션 중 Gitaly 인스턴스에 대한 요청 하나가 ~"priority::1" 문제를 유발한 예시가 있습니다.
Eager Loading
요약: 한 개 이상의 행을 검색할 때는 항상 연관된 항목을 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 테스트를 사용하는 것도 고려하세요.
메모리 사용량
요약: 병합 요청은 필요하지 않는 한 메모리 사용량을 증가시켜서는 안 됩니다.
병합 요청은 GitLab의 메모리 사용량을 코드에서 요구하는 최소한의 양 이상으로 증가시켜서는 안 됩니다. 이는 대규모 문서(예: HTML 문서)를 구문 분석해야 하는 경우, 입력 전체를 메모리에 로드하는 대신 가능한 경우 스트림으로 구문 분석하는 것이 가장 좋다는 것을 의미합니다. 때때로 이는 불가능할 수 있으며, 이 경우 병합 요청에서 명시적으로 언급해야 합니다.
UI 요소의 지연 렌더링
요약: 실제로 필요할 때만 UI 요소를 렌더링합니다.
일부 UI 요소는 항상 필요하지 않을 수 있습니다. 예를 들어, 차이(diff) 줄 위에 마우스를 올릴 때 새로운 댓글을 생성하는 데 사용할 수 있는 작은 아이콘이 표시됩니다. 이러한 요소를 항상 렌더링하는 대신 실제로 필요할 때만 렌더링해야 합니다. 이렇게 하면 사용되지 않을 때 Haml/HTML을 생성하는 데 시간을 낭비하지 않게 됩니다.
캐싱 사용
요약: 트랜잭션에서 여러 번 필요하거나 일정 기간 유지해야 하는 데이터를 메모리 또는 Redis에 캐시하세요.
때때로 특정 데이터 조각은 트랜잭션의 여러 장소에서 재사용해야 합니다. 이러한 경우, 데이터를 가져오기 위해 복잡한 작업을 수행할 필요를 없애기 위해 이 데이터를 메모리에 캐시해야 합니다. 데이터가 트랜잭션의 지속 시간 대신 일정 기간 동안 캐시되어야 하는 경우 Redis를 사용해야 합니다.
예를 들어, 사용자 이름 멘션이 포함된 여러 텍스트 조각(예: Hello @alice
및 How are you doing @alice?
)을 처리한다고 가정해 보세요. 모든 사용자 이름의 사용자 객체를 캐시함으로써 @alice
의 모든 멘션에 대해 동일한 쿼리를 실행할 필요를 없앨 수 있습니다.
트랜잭션별 데이터 캐싱은 RequestStore를 사용하여 수행할 수 있습니다(복잡한 연산을 수행하지 않도록 Gitlab::SafeRequestStore
를 사용해 RequestStore.active?
를 확인하는 것을 잊지 마세요). Redis에 데이터를 캐시하는 것은 Rails의 캐싱 시스템을 사용하여 수행할 수 있습니다.
페이지네이션
목록 항목을 테이블 형식으로 렌더링하는 각 기능은 페이지네이션을 포함해야 합니다.
주요 페이지네이션 스타일은 다음과 같습니다:
-
오프셋 기반 페이지네이션: 사용자가 특정 페이지(예: 1)로 이동합니다. 사용자는 다음 페이지 번호와 총 페이지 수를 봅니다. 이 스타일은 GitLab의 모든 구성 요소에서 잘 지원됩니다.
-
오프셋 기반 페이지네이션, 하지만 카운트 없이: 사용자가 특정 페이지(예: 1)로 이동합니다. 사용자는 다음 페이지 번호만 보고 총 페이지 수는 보지 않습니다.
-
키셋 기반 페이지네이션을 사용한 다음 페이지: 사용자는 다음 페이지만 이동할 수 있으며, 총 페이지 수에 대한 정보를 모릅니다.
-
무한 스크롤 페이지네이션: 사용자가 페이지를 스크롤하면 다음 항목이 비동기적으로 로드됩니다. 이는 이전 것과 같은 이점이 있으므로 이상적입니다.
페이지네이션을 위한 궁극적으로 확장 가능한 솔루션은 키셋 기반 페이지네이션을 사용하는 것입니다. 그러나 현재 GitLab에서는 이에 대한 지원이 없습니다. API: Keyset Pagination을 통해 진행 상황을 확인할 수 있습니다.
페이지네이션 전략을 선택할 때 다음 사항을 고려하십시오:
-
필터링을 통과하는 객체 수를 계산하는 것은 매우 비효율적이며, 이 작업은 일반적으로 몇 초가 걸릴 수 있으며, 시간 초과가 발생할 수 있습니다.
-
고차 페이지(예: 1000)의 항목을 가져오는 것은 매우 비효율적입니다. 데이터베이스는 모든 이전 항목을 정렬하고 반복해야 하며, 이 작업은 데이터베이스에 상당한 부담을 줄 수 있습니다.
페이지네이션 지침에서 페이지네이션과 관련된 유용한 팁을 찾을 수 있습니다.
배지 카운터
카운터는 항상 단축되어야 합니다. 이는 특정 임계값을 초과하는 정확한 숫자를 제시하고 싶지 않다는 의미입니다.
그 이유는 정확한 항목 수를 계산하고자 할 때, 실제로는 해당 항목을 필터링해야 하기 때문입니다.
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
이상이 필요한 경우, 유지 관리자의 도움을 요청하여 더 나은 솔루션을 찾아보는 것을 고려하세요.
로컬 임시 저장소
로컬 저장소의 사용은 특히 Kubernetes 클러스터에 애플리케이션을 배포하는 작업을 하므로 바람직한 솔루션입니다.
언제 Dir.mktmpdir
을 사용할까요? 예를 들어 아카이브를 추출/생성하거나 기존 데이터의 광범위한 조작을 수행하고자 할 때입니다.
Dir.mktmpdir('designs') do |path|
# 경로에서 조작 수행
# 블록을 벗어날 때 경로는 제거됩니다.
end
공유 임시 저장소
공유 임시 저장소의 사용은 디스크 기반 저장소에 파일을 지속적으로 저장할 의도가 있는 경우 필요하며, 객체 저장소는 아닙니다.
파일을 수락할 때 Workhorse direct upload는 공유 저장소에 쓸 수 있으며,
나중에 GitLab Rails는 이동 작업을 수행할 수 있습니다.
같은 목적지에서의 이동 작업은 즉시 이루어집니다.
시스템은 copy
작업을 수행하는 대신 파일을 새 위치에 재부착합니다.
이것은 애플리케이션에 추가적인 복잡성을 도입하므로, 재구현하는 대신 잘 확립된 패턴(예: ObjectStorage
concern)을 재사용하려고 해야 합니다.
그 외의 모든 용도로는 공유 임시 저장소의 사용이 더 이상 권장되지 않습니다.
지속 저장소
객체 저장소
지속 파일을 보유하는 모든 기능이 객체 저장소에 데이터를 저장하는 것을 지원해야 합니다.
노드 간의 공유 볼륨 형태로 지속 저장소를 두는 것은 확장성이 없으며,
모든 노드의 데이터 접근에서 경쟁을 초래합니다.
GitLab은 공유 저장소 및 객체 저장소 기반의 지속 저장소를 원활하게 지원하는 ObjectStorage concern을 제공합니다.
데이터 접근
데이터 업로드를 수락하거나 다운로드를 허용하는 각 기능은 Workhorse direct upload를 사용해야 합니다.
업로드는 Workhorse에 의해 객체 저장소에 직접 저장되어야 하며,
모든 다운로드는 Workhorse에 의해 제공되어야 합니다.
Puma를 통한 업로드/다운로드는 비용이 많이 드는 작업으로,
업로드 기간 동안 전체 처리 슬롯(스레드)을 차단합니다.
Puma를 통한 업로드/다운로드는 작업이 시간 초과될 수 있는 문제도 있으며,
특히 느린 클라이언트에 대해 문제가 됩니다. 클라이언트가 업로드/다운로드에 오랜 시간이 걸리면,
요청 처리 시간 초과로 인해 처리 슬롯이 종료될 수 있습니다(일반적으로 30초-60초).
위의 이유로 인해 모든 파일 업로드 및 다운로드에 대해 Workhorse direct upload를 구현해야 합니다.