모노레포 관리
모노레포는 개발 팀의 워크플로우의 일상적인 부분이 되었습니다. 많은 장점이 있지만, GitLab에서 사용할 때 성능 문제를 야기할 수 있습니다. 따라서 알아야 할 사항은 다음과 같습니다:
- 성능에 영향을 줄 수 있는 리포지토리 특성.
- 모노레포를 최적화하기 위한 도구와 단계.
성능에 대한 영향
GitLab은 Git 기반 시스템이기 때문에, 대용량 리포지토리의 경우 Git과 유사한 성능 제약을 받습니다.
모노레포는 많은 이유로 인해 클 수 있습니다.
대규모 리포지토리는 특히 많은 클론이나 푸시를 하루에 받는 대형 모노레포에서 GitLab 사용 시 성능 리스크를 초래할 수 있습니다.
대규모 리포지토리와 Git 성능 문제
Git은 패크파일을 사용하여 객체를 저장하여 가능한 한 적은 공간을 차지하도록 합니다. 패크파일은 또한 클론, 패치, 또는 Git 클라이언트와 Git 서버 간의 전송 시 객체를 전송하는 데 사용됩니다. 패크파일을 사용하는 것은 디스크 공간과 네트워크 대역폭 요구량을 줄여주기 때문에 일반적으로 좋습니다.
하지만, 패크파일을 생성하려면 객체 내용을 압축하기 위해 많은 CPU와 메모리가 필요합니다. 따라서 리포지토리가 클 경우, 패크파일 생성을 요구하는 모든 Git 작업은 처리해야 할 더 많은 객체와 더 큰 객체로 인해 비용이 많이 들고 느려질 수 있습니다.
GitLab에 대한 결과
Gitaly는 Git 위에 구축된 우리의 Git 저장 서비스입니다. 이는 Git의 모든 제한이 Gitaly와 GitLab의 최종 사용자에게도 영향을 미친다는 것을 의미합니다.
모노레포는 하드웨어에 현저한 영향을 줄 수 있으며, 경우에 따라 수직 확장, 네트워크 또는 디스크 대역폭 한계와 같은 제한에 부딪힐 수 있습니다.
GitLab 설정 최적화
Gitaly 서버에서의 패치 수를 최소화하기 위해 다음 전략을 가능한 많이 사용해야 합니다.
이유
Git에서 가장 많은 자원을 소모하는 작업은
git-pack-objects
프로세스입니다. 이 프로세스는 클라이언트에게 보낼 모든 커밋 히스토리와 파일을 파악한 후 패크파일을 생성하는 역할을 합니다.
리포지토리가 클수록, 더 많은 커밋, 파일, 브랜치, 태그가 포함되어 있으며 이 작업은 더 비쌉니다. 이 작업 중에는 메모리와 CPU가 많이 사용됩니다.
대부분의 git clone
또는 git fetch
트래픽(서버에서 git-pack-objects
프로세스를 시작하게 되는)은 GitLab CI/CD 또는 기타 CI/CD 시스템과 같은 자동화된 지속적 통합 시스템에서 발생합니다. 이러한 트래픽이 많으면, 대규모 리포지토리에 대해 많은 클론을 시도할 경우 Gitaly 서버에 상당한 부담을 주게 됩니다.
Gitaly 패크-오브젝트 캐시
Gitaly 패크-오브젝트 캐시를 활성화하면 클론 및 패치 과정에서 서버가 수행해야 할 작업이 줄어듭니다.
이유
패크-오브젝트 캐시는 git-pack-objects
프로세스가 생성한 데이터를 캐시합니다. 이 응답은 클론 또는 패치를 시작하는 Git 클라이언트로 다시 전송됩니다. 여러 패치가 동일한 참조 세트를 요청할 때, Gitaly 서버의 Git은 각 클론이나 패치 호출에서 응답 데이터를 재생성할 필요가 없으며, 대신 Gitaly가 유지하는 메모리 캐시에서 해당 데이터를 제공합니다.
이는 단일 리포지토리에 대해 높은 비율의 클론이 있을 때 큰 도움이 될 수 있습니다.
자세한 내용은 패크-오브젝트 캐시를 참조하세요.
CI/CD에서 동시 클론 줄이기
CI/CD 로드는 파이프라인이 정해진 시간에 예약되기 때문에 동시 발생하는 경향이 있습니다.
그 결과, 이러한 시간 동안 레포지토리에 대한 Git 요청이 급증할 수 있으며, 이는 CI/CD와 사용자 모두의 성능 저하로 이어질 수 있습니다.
CI/CD 파이프라인의 동시성을 시간을 stagger하여 줄이세요.
예를 들어, 한 세트는 한 시간에 실행되고 다른 세트는 몇 분 후에 실행됩니다.
얕은 클론
CI/CD 시스템에서 git clone
또는 git fetch
호출 시 --depth
옵션을 설정하세요.
GitLab과 GitLab Runner는 기본적으로 얕은 클론을 수행합니다.
가능하다면, 클론 깊이를 10과 같은 작은 숫자로 설정하세요. 얕은 클론은 주어진 브랜치의 최신 변경 사항 집합만 요청합니다.
이는 Git 레포지토리에서 변경 사항을 가져오는 속도를 크게 높입니다, 특히 레포지토리가 여러 개의 큰 파일로 구성된 매우 긴 백로그를 가지고 있을 경우 데이터 전송량을 효과적으로 줄이기 때문입니다.
다음 GitLab CI/CD 파이프라인 구성 예는 GIT_DEPTH
를 설정합니다.
variables:
GIT_DEPTH: 10
test:
script:
- ls -al
개발을 위한 얕은 클론 피하기
얕은 클론은 변경 사항을 푸시하는 데 걸리는 시간을 크게 증가시키므로 개발을 위해 피하세요.
얕은 클론은 레포지토리의 내용이 체크아웃된 후 변경되지 않기 때문에 CI/CD 작업과 잘 작동합니다.
대신, 로컬 개발에는 부분 클론을 사용하여:
-
블롭 필터링:
git clone --filter=blob:none
-
트리 필터링:
git clone --filter=tree:0
자세한 내용은 클론 크기 줄이기를 참조하세요.
Git 전략
가능하다면 CI/CD 시스템에서 git clone
대신 git fetch
를 사용하여 레포지토리의 작업 복사본을 유지하세요.
기본적으로 GitLab은 fetch
Git 전략을 사용하도록 구성되어 있으며, 이는 대형 레포지토리에 권장됩니다.
이론
git clone
은 처음부터 전체 레포지토리를 가져오는 반면, git fetch
는 레포지토리에 이미 존재하지 않는 참조만 서버에 요청합니다.
따라서, git fetch
는 서버에 더 적은 작업을 요구합니다. git-pack-objects
는 모든 브랜치와 태그를 통과하여 응답으로 전송되는 모든 것을 롤업할 필요가 없습니다. 대신, 패킹할 참조의 하위 집합에 대해서만 신경 쓰면 됩니다. 이 전략은 전송해야 할 데이터 양도 줄여줍니다.
Git 클론 경로
GIT_CLONE_PATH
는 레포지토리를 클론할 위치를 제어할 수 있게 해줍니다.
이는 포크 기반 워크플로로 대형 레포지토리를 많이 사용하는 경우에 함의가 있을 수 있습니다.
GitLab Runner 관점에서 보면 포크는 별도의 레포지토리와 별도의 작업 트리로 저장됩니다. 이는 GitLab Runner가 작업 트리 사용을 최적화할 수 없음을 의미하며, GitLab Runner에게 이를 사용하도록 지시해야 할 수 있습니다.
이러한 경우, 이상적으로는 GitLab Runner 실행자가 주어진 프로젝트에만 사용되고 다른 프로젝트와 공유되지 않도록 하여 이 프로세스를 더욱 효율적으로 만들어야 합니다.
GIT_CLONE_PATH
는 $CI_BUILDS_DIR
로 설정된 디렉토리에 있어야 합니다. 디스크에서 아무 경로를 선택할 수 없습니다.
Git clean 플래그
GIT_CLEAN_FLAGS
는 각 CI/CD 작업에 대해 git clean
명령을 실행해야 할지 여부를 제어할 수 있습니다. 기본적으로 GitLab은 다음을 보장합니다:
- 주어진 SHA에서 작업 트리가 있습니다.
- 리포지토리는 깨끗합니다.
GIT_CLEAN_FLAGS
는 ‘none’으로 설정되면 비활성화됩니다. 매우 큰 리포지토리에서는 git clean
이 디스크 I/O 집약적이기 때문에 이를 원할 수 있습니다. GIT_CLEAN_FLAGS: -ffdx -e .build/
(예를 들어)로 제어하면 이후 실행 간 작업 트리에서 일부 디렉터리의 제거를 비활성화할 수 있어 증분 빌드를 더 빠르게 할 수 있습니다. 이는 기존 머신을 재사용하고 재사용할 수 있는 작업 트리가 있는 경우 가장 큰 영향을 미칩니다.
정확히 어떤 매개변수가 GIT_CLEAN_FLAGS
로 허용되는지는 git clean
문서를 참조하세요. 사용 가능한 매개변수는 Git 버전에 따라 다릅니다.
Git fetch 추가 플래그
GIT_FETCH_EXTRA_FLAGS
는 추가 플래그를 전달하여 git fetch
동작을 수정할 수 있습니다.
예를 들어, 프로젝트에 CI/CD 작업이 의존하지 않는 많은 태그가 포함된 경우, 추가 플래그로 --no-tags
를 추가하여 가져오는 속도를 더 빠르고 간결하게 만들 수 있습니다.
또한 리포지토리에 태그가 많지 않은 경우, --no-tags
는 일부 경우에 큰 차이를 만들 수 있습니다. CI/CD 빌드가 Git 태그에 의존하지 않는 경우 --no-tags
설정을 시도해 볼 가치가 있습니다.
자세한 내용은 GIT_FETCH_EXTRA_FLAGS
문서를 참조하세요.
Gitaly 협상 타임아웃 구성
가져오기 또는 아카이브를 시도할 때 fatal: the remote end hung up unexpectedly
오류가 발생할 수 있습니다:
- 대형 리포지토리.
- 많은 리포지토리를 병렬로.
- 동일한 대형 리포지토리를 병렬로.
기본 협상 타임아웃 값을 증가시켜 이 문제를 완화할 수 있습니다. 자세한 내용은 협상 타임아웃 구성을 참조하세요.
리포지토리 최적화
GitLab을 스케일 가능하도록 유지하는 또 다른 방법은 리포지토리 자체를 최적화하는 것입니다.
리포지토리 프로파일링
대형 리포지토리는 일반적으로 Git에서 성능 문제를 경험합니다. 리포지토리가 커지는 이유를 아는 것은 성능 문제를 피하기 위한 완화 전략 개발에 도움이 될 수 있습니다.
git-sizer
를 사용하여 리포지토리 특성의 스냅샷을 얻고 모노레포의 문제점을 발견할 수 있습니다.
리포지토리의 전체 복제를 얻으려면 모든 Git 참조가 존재하는지 확인하기 위해 전체 Git 미러 또는 베어 클론이 필요합니다. 리포지토리를 프로파일링하려면:
-
설치
git-sizer
. -
리포지토리의 전체 클론을 가져옵니다:
git clone --mirror <git_repo_url>
클론 후, 리포지토리는
git-sizer
와 호환되는 베어 Git 형식이 됩니다. -
Git 리포지토리 디렉터리에서 모든 통계로
git-sizer
를 실행합니다:git-sizer -v
처리 후, git-sizer
의 출력은 다음과 같이 보여야 하며 리포지토리의 각 측면에 대한 우려 수준이 표시됩니다:
Processing blobs: 1652370
Processing trees: 3396199
Processing commits: 722647
Matching commits to trees: 722647
Processing annotated tags: 534
Processing references: 539
| Name | Value | Level of concern |
| ---------------------------- | --------- | ------------------------------ |
| Overall repository size | | |
| * Commits | | |
| * Count | 723 k | * |
| * Total size | 525 MiB | ** |
| * Trees | | |
| * Count | 3.40 M | ** |
| * Total size | 9.00 GiB | **** |
| * Total tree entries | 264 M | ***** |
| * Blobs | | |
| * Count | 1.65 M | * |
| * Total size | 55.8 GiB | ***** |
| * Annotated tags | | |
| * Count | 534 | |
| * References | | |
| * Count | 539 | |
| | | |
| Biggest objects | | |
| * Commits | | |
| * Maximum size [1] | 72.7 KiB | * |
| * Maximum parents [2] | 66 | ****** |
| * Trees | | |
| * Maximum entries [3] | 1.68 k | * |
| * Blobs | | |
| * Maximum size [4] | 13.5 MiB | * |
| | | |
| History structure | | |
| * Maximum history depth | 136 k | |
| * Maximum tag depth [5] | 1 | |
| | | |
| Biggest checkouts | | |
| * Number of directories [6] | 4.38 k | ** |
| * Maximum path depth [7] | 13 | * |
| * Maximum path length [8] | 134 B | * |
| * Number of files [9] | 62.3 k | * |
| * Total size of files [9] | 747 MiB | |
| * Number of symlinks [10] | 40 | |
| * Number of submodules | 0 | |
이 예제에서는 몇 가지 항목이 높은 수준의 우려를 안고 있습니다. 다음 섹션에서 다음에 대한 정보를 확인하세요:
- 많은 수의 참조.
- 대형 블롭.
많은 참조
Git의 참조는 특정 커밋을 가리키는 브랜치와 태그 이름입니다. git for-each-ref
명령어를 사용하여 저장소에 있는 모든 참조를 나열할 수 있습니다. 저장소에 많은 참조가 있으면 명령어의 성능에 부정적인 영향을 미칠 수 있습니다. 그 이유를 이해하려면 Git이 참조를 저장하고 사용하는 방식을 알아야 합니다.
일반적으로 Git은 모든 참조를 저장소의 .git/refs
폴더에 느슨한 파일로 저장합니다. 참조의 수가 증가함에 따라 폴더 내 특정 참조를 찾는 탐색 시간도 증가합니다. 따라서 Git이 참조를 구문 분석할 때마다 파일 시스템의 탐색 시간이 추가되어 지연 시간이 증가합니다.
이 문제를 해결하기 위해 Git은 pack-refs를 사용합니다. 간단히 말해, Git은 각 참조를 단일 파일에 저장하는 대신 모든 참조를 포함하는 단일 .git/packed-refs
파일을 생성합니다. 이 파일은 저장 공간을 줄이는 동시에 성능을 향상시키는데, 단일 파일 내에서 찾는 것이 디렉토리 내 파일을 찾는 것보다 빠르기 때문입니다. 그러나 새로운 참조를 생성하거나 업데이트하는 것은 여전히 느슨한 파일을 통해 이루어지며 packed-refs
파일에 추가되지 않습니다. packed-refs
파일을 재생성하려면 git pack-refs
를 실행하십시오.
Gitaly는 정리 작업 동안 느슨한 참조를 packed-refs
파일로 이동하기 위해 git pack-refs
를 실행합니다. 이는 대부분의 저장소에 매우 유익하지만, 쓰기 중인 저장소에는 여전히 다음과 같은 문제가 있습니다:
- 참조를 생성하거나 업데이트하면 새로운 느슨한 파일이 생성됩니다.
- 참조를 삭제하면 기존 참조를 제거하기 위해 기존
packed-refs
파일을 완전히 수정해야 합니다.
이러한 문제들은 여전히 동일한 성능 문제를 유발합니다.
또한, 저장소에서의 가져오기 및 복제에는 서버에서 클라이언트로 누락된 객체를 전송하는 것이 포함됩니다. 참조가 많으면, Git은 모든 참조를 반복하고 각 참조에 대해 내부 그래프 구조를 탐색하여 클라이언트로 전송할 누락된 객체를 찾습니다. 반복 및 탐색은 CPU 집약적인 작업으로, 이러한 명령의 지연 시간을 증가시킵니다.
활동이 많은 저장소에서는 모든 작업이 느려지고 각 작업이 후속 작업을 지연시키기 때문에 이로 인해 도미노 효과가 발생하는 경우가 많습니다.
완화 전략
모노레포에서 많은 수의 참조가 미치는 영향을 완화하려면:
- 오래된 브랜치를 정리하는 자동화 프로세스를 만듭니다.
-
특정 참조가 클라이언트에 보이지 않아야 하는 경우,
transfer.hideRefs
구성 설정을 사용하여 숨깁니다. Gitaly는 서버상의 Git 구성 파일을 무시하므로/etc/gitlab/gitlab.rb
에서 Gitaly 구성을 직접 변경해야 합니다:gitaly['configuration'] = { # ... git: { # ... config: [ # ... { key: "transfer.hideRefs", value: "refs/namespace_to_hide" }, ], }, }
Git 2.42.0 이후에는 다양한 Git 작업이 객체 그래프 탐색 시 숨겨진 참조를 건너뛸 수 있습니다.
큰 Blob
Blob은 Git 객체로, 사용자가 Git 저장소에 커밋한 파일의 내용을 저장하고 관리하는 데 사용됩니다.
대용량 블롭 문제
대용량 블롭은 Git에 문제를 일으킬 수 있습니다. Git은 대용량 이진 데이터를 효율적으로 처리하지 않기 때문입니다. git-sizer
출력에서 10MB를 초과하는 블롭은 저장소에 대용량 이진 데이터가 있다는 의미일 가능성이 높습니다.
소스 코드는 일반적으로 효율적으로 압축할 수 있지만, 이진 데이터는 종종 이미 압축되어 있습니다. 이는 Git이 패키지 파일을 생성할 때 대용량 블롭을 압축하려고 시도해도 성공할 가능성이 낮다는 것을 의미합니다. 결과적으로 패키지 파일이 더 커지고 Git 클라이언트 및 서버에서 CPU, 메모리 및 대역폭 사용량이 증가합니다.
클라이언트 측에서는 Git이 블롭 콘텐츠를 패키지 파일(일반적으로 .git/objects/pack/
아래)과 일반 파일(작업 트리)에 저장하기 때문에, 소스 코드보다 훨씬 더 많은 디스크 공간이 필요합니다.
대용량 블롭에 LFS 사용
이진 파일 또는 블롭 파일(예: 패키지, 오디오, 비디오 또는 그래픽)을 대용량 파일 저장소(LFS) 객체로 저장합니다. LFS를 사용하면 객체가 외부에 저장되어 객체 수와 크기가 저장소에서 줄어듭니다. 외부 객체 저장소에 객체를 저장하면 성능이 향상될 수 있습니다.
자세한 내용은 Git LFS 문서를 참조하십시오.
참조 아키텍처
대규모 저장소는 일반적으로 많은 사용자가 있는 대규모 조직에서 발견됩니다. GitLab 테스트 플랫폼 및 지원 팀은 GitLab을 대규모로 배포하는 데 권장되는 몇 가지 참조 아키텍처를 제공합니다.
이러한 유형의 설정에서는 성능 향상을 위해 GitLab에서 사용하는 환경이 참조 아키텍처와 일치해야 합니다.