GitLab 확장성
GitLab v19.1이 섹션에서는 확장성 및 안정성과 관련된 GitLab의 현재 아키텍처를 설명합니다. ](/19.1/development/img/reference_architecture_v12_8.png) 다이어그램 소스 - GitLab 직원 전용
이 섹션에서는 확장성 및 안정성과 관련된 GitLab의 현재 아키텍처를 설명합니다.
레퍼런스 아키텍처 개요#
[
](/19.1/development/img/reference_architecture_v12_8.png)
위 다이어그램은 50,000명의 사용자를 위해 확장된 GitLab 레퍼런스 아키텍처를 보여줍니다. 각 구성 요소에 대해 아래에서 설명합니다.
구성 요소#
PostgreSQL#
PostgreSQL 데이터베이스는 프로젝트, 이슈, 머지 리퀘스트, 사용자 등의 모든 메타데이터를 저장합니다. 스키마는 Rails 애플리케이션의 db/structure.sql에 의해 관리됩니다.
GitLab Web/API 서버와 Sidekiq 노드는 Rails 객체 관계형 모델(ORM)을 사용하여 데이터베이스에 직접 통신합니다. 대부분의 SQL 쿼리는 이 ORM을 통해 접근하지만, 성능 향상이나 고급 PostgreSQL 기능(재귀 CTE 또는 LATERAL JOIN 등)을 활용하기 위해 일부 커스텀 SQL도 작성됩니다.
애플리케이션은 데이터베이스 스키마와 긴밀하게 결합되어 있습니다. 애플리케이션이 시작될 때, Rails는 데이터베이스 스키마를 쿼리하여 요청된 데이터에 대한 테이블과 칼럼 유형을 캐싱합니다. 이 스키마 캐시로 인해, 애플리케이션이 실행 중인 동안 칼럼이나 테이블을 삭제하면 사용자에게 500 오류가 발생할 수 있습니다. 이것이 바로 칼럼 삭제 및 다운타임 없는 변경을 위한 프로세스가 있는 이유입니다.
멀티 테넌시#
단일 데이터베이스를 사용하여 모든 고객 데이터를 저장합니다. 각 사용자는 여러 그룹이나 프로젝트에 속할 수 있으며, 그룹 및 프로젝트에 대한 접근 수준(Guest, Developer, 또는 Maintainer 포함)에 따라 사용자가 볼 수 있는 것과 접근할 수 있는 것이 결정됩니다.
관리자 접근 권한이 있는 사용자는 모든 프로젝트에 접근하고 사용자를 가장(impersonate)할 수도 있습니다.
GitLab.com에서는 Cells 아키텍처가 각 셀이 조직의 하위 집합을 호스팅하는 물리적 멀티 테넌시를 제공합니다. GitLab Self-Managed와 GitLab Dedicated는 단일 셀로 실행됩니다. 조직은 본질적으로 하나의 셀에 격리되며, 모든 조직 데이터는 해당 조직을 호스팅하는 셀에 상주합니다.
샤딩 및 파티셔닝#
현재 데이터베이스는 분할되지 않았으며, 모든 데이터는 여러 테이블로 구성된 하나의 데이터베이스에 저장됩니다. 이는 단순한 애플리케이션에는 잘 작동하지만, 데이터 세트가 증가함에 따라 많은 행을 가진 테이블이 있는 하나의 데이터베이스를 유지 관리하고 지원하는 것이 더 어려워집니다.
이를 처리하는 두 가지 방법이 있습니다:
-
파티셔닝. 테이블 데이터를 로컬에서 분할합니다.
-
샤딩. 여러 데이터베이스에 걸쳐 데이터를 분산합니다.
파티셔닝은 PostgreSQL의 내장 기능으로, 애플리케이션 변경이 최소화됩니다. 그러나 PostgreSQL 11이 필요합니다.
예를 들어, 자연스러운 파티셔닝 방법은
날짜별로 테이블을 파티션하는 것입니다.
예를 들어, events 및 audit_events 테이블은 이런 종류의 파티셔닝에 자연스러운 후보입니다.
샤딩은 더 어렵고 스키마 및 애플리케이션에 상당한 변경이 필요합니다. 예를 들어, 프로젝트를 여러 다른 데이터베이스에 저장해야 한다면, 즉시 "어떻게 서로 다른 프로젝트에서 데이터를 가져올 수 있는가?"라는 질문에 직면하게 됩니다. 이에 대한 한 가지 답은 데이터 접근을 애플리케이션에서 데이터베이스를 추상화하는 API 호출로 추상화하는 것이지만, 이는 상당한 양의 작업입니다.
애플리케이션에서 어느 정도 샤딩을 추상화하는 데 도움이 될 수 있는 솔루션이 있습니다. 예를 들어, Citus Data를 면밀히 살펴보고자 합니다. Citus Data는 ActiveRecord 모델에 테넌트 ID를 추가하는 Rails 플러그인을 제공합니다.
샤딩은 기능 수직 계층을 기반으로 수행될 수도 있습니다. 이는 샤딩에 대한 마이크로서비스 접근 방식으로, 각 서비스가 제한된 컨텍스트를 나타내고 자체 서비스별 데이터베이스 클러스터에서 운영됩니다. 그 모델에서 데이터는 일부 내부 키(예: 테넌트 ID)에 따라 분산되는 것이 아니라 팀 및 제품 소유권을 기반으로 분산됩니다. 그러나 기존의 데이터 지향 샤딩과 많은 어려움을 공유합니다. 예를 들어, 데이터 조인은 쿼리 계층이 아닌 애플리케이션 자체에서 수행되어야 하며(GraphQL 같은 추가 레이어가 이를 완화할 수 있지만), 효율적으로 실행하려면 진정한 병렬성(즉, 데이터 레코드를 수집한 후 압축하는 scatter-gather 모델)이 필요한데, 이는 Ruby 기반 시스템에서 그 자체로 하나의 어려움입니다.
Cells 샤딩 키 접근 방식#
Cells 아키텍처는 데이터베이스 행을 단일 조직에 귀속시키는 샤딩 키 접근 방식을 도입합니다. 고객 데이터를 저장하는 모든 새로운 데이터베이스 테이블은 행을 조직에 연결하는 샤딩 키를 정의해야 합니다. 비고객 데이터는 셀 로컬(cell-local)로 표시되어야 하며, 이는 데이터가 셀 밖으로 나가지 않음을 의미합니다.
샤딩 키 선택 및 구현에 대한 지침은 샤딩 키를 참조하세요.
데이터베이스 크기#
최근의
데이터베이스 점검 결과 GitLab.com의 테이블 크기 분석 결과를 보여줍니다.
merge_request_diff_files가 1 TB 이상의 데이터를 포함하므로, 이 테이블을 먼저 줄이거나 제거하고자 합니다.
GitLab은 오브젝트 스토리지에 diff를 저장하는 기능을 지원하며,
GitLab.com에서 이를 적용하고자 합니다.
고가용성#
고가용성과 이중화를 제공하는 몇 가지 전략이 있습니다:
-
오브젝트 스토리지(예: S3 또는 Google Cloud Storage)로 스트리밍되는 Write-ahead 로그(WAL).
-
읽기 복제본(핫 백업).
-
지연된 복제본.
특정 시점에서 데이터베이스를 복원하려면, 해당 인시던트 이전에 기본 백업이 완료되어 있어야 합니다. 데이터베이스가 해당 백업에서 복원되면, 데이터베이스는 타깃 시간에 도달할 때까지 순서대로 WAL 로그를 적용할 수 있습니다.
GitLab.com에서는 Consul과 Patroni가 함께 작동하여 읽기 복제본과 함께 페일오버를 조정합니다. Omnibus는 Patroni와 함께 제공됩니다.
로드 밸런싱#
GitLab EE는 읽기 복제본을 사용하는 로드 밸런싱에 대한 애플리케이션 지원이 있습니다. 이 로드 밸런서는 표준 로드 밸런서에서 일반적으로 사용할 수 없는 일부 작업을 수행합니다. 예를 들어, 애플리케이션은 복제 지연이 낮은 경우에만(예: WAL 데이터가 100 MB 미만으로 뒤처진 경우) 복제본을 고려합니다.
더 많은 세부 사항은 블로그 포스트에서 확인할 수 있습니다.
PgBouncer#
PostgreSQL은 각 요청에 대해 백엔드 프로세스를 포크하므로, PostgreSQL은 지원할 수 있는 연결 수에 유한한 제한이 있으며, 기본적으로 약 300개입니다. PgBouncer와 같은 커넥션 풀러 없이는 연결 제한에 도달할 가능성이 높습니다. 제한에 도달하면 GitLab은 오류를 생성하거나 연결이 가능해질 때까지 기다리며 느려질 수 있습니다.
고가용성#
PgBouncer는 단일 스레드 프로세스입니다. 트래픽이 많을 때 PgBouncer는 단일 코어를 포화시킬 수 있으며, 이로 인해 백그라운드 job 및/또는 웹 요청에 대한 응답 시간이 느려질 수 있습니다. 이 제한을 해결하는 두 가지 방법이 있습니다:
-
여러 PgBouncer 인스턴스를 실행합니다.
-
멀티 스레드 커넥션 풀러를 사용합니다(예: Odyssey).
일부 Linux 시스템에서는 동일한 포트에서 여러 PgBouncer 인스턴스를 실행할 수 있습니다.
GitLab.com에서는 단일 코어가 포화되는 것을 방지하기 위해 다른 포트에서 여러 PgBouncer 인스턴스를 실행합니다.
또한, 프라이머리와 세컨더리와 통신하는 PgBouncer 인스턴스는 약간 다르게 설정됩니다:
-
다른 가용성 영역의 여러 PgBouncer 인스턴스가 PostgreSQL 프라이머리와 통신합니다.
-
여러 PgBouncer 프로세스가 PostgreSQL 읽기 복제본과 함께 배치됩니다.
복제본의 경우, 함께 배치하면 네트워크 홉을 줄이고 따라서 레이턴시가 감소하므로 유리합니다. 그러나 프라이머리의 경우, 함께 배치하면 PgBouncer가 단일 장애 지점이 되고 오류를 유발할 수 있으므로 불리합니다. 페일오버가 발생하면 두 가지 중 하나가 발생할 수 있습니다:
-
프라이머리가 네트워크에서 사라집니다.
-
프라이머리가 복제본이 됩니다.
첫 번째 경우, PgBouncer가 프라이머리와 함께 배치되어 있으면 데이터베이스 연결이 시간 초과되거나 연결에 실패하여 다운타임이 발생할 수 있습니다. 로드 밸런서 앞에 여러 PgBouncer 인스턴스를 두고 프라이머리와 통신하게 함으로써 이를 완화할 수 있습니다.
두 번째 경우, 새로 강등된 복제본에 대한 기존 연결이 쓰기 쿼리를 실행하려 할 수 있으며, 이는 실패합니다. 페일오버 중에는 프라이머리와 통신하는 PgBouncer를 종료하여 더 이상 트래픽이 도달하지 않도록 하는 것이 유리할 수 있습니다. 대안으로는 애플리케이션이 페일오버 이벤트를 인식하고 연결을 정상적으로 종료하도록 만드는 것입니다.
Redis#
GitLab에서 Redis가 사용되는 세 가지 방법이 있습니다:
-
큐: Sidekiq job은 job을 JSON 페이로드로 마샬링합니다.
-
영구 상태: 세션 데이터와 독점 임대.
-
캐시: 리포지터리 데이터(브랜치 및 태그 이름 등)와 뷰 파셜.
규모에 맞게 실행되는 GitLab 인스턴스의 경우, Redis 사용량을 별도의 Redis 클러스터로 분리하면 두 가지 이유로 도움이 됩니다:
-
각각 다른 영속성 요구 사항이 있습니다.
-
부하 격리.
예를 들어, 캐시 인스턴스는 maxmemory 구성 옵션을 설정함으로써 최근 사용 빈도가 낮은(LRU) 캐시처럼 동작할 수 있습니다.
큐 또는 영구 클러스터에는 이 옵션을 설정해서는 안 됩니다. 그렇지 않으면 데이터가 임의의 시간에 메모리에서 제거될 수 있습니다.
이렇게 되면 job이 처리되지 않고 사라져 많은 문제(머지가 실행되지 않거나 빌드가 업데이트되지 않는 등)가 발생합니다.
Sidekiq은 또한 큐를 매우 자주 폴링하며, 이 활동으로 인해 다른 쿼리가 느려질 수 있습니다. 이러한 이유로 Sidekiq을 위한 전용 Redis 클러스터를 두면 성능을 개선하고 Redis 프로세스의 부하를 줄이는 데 도움이 됩니다.
고가용성/위험#
단일 코어: PgBouncer와 마찬가지로 단일 Redis 프로세스는 하나의 코어만 사용할 수 있습니다. 멀티 스레딩을 지원하지 않습니다.
더미 세컨더리: Redis 세컨더리(복제본이라고도 함)는 실제로 어떤 부하도 처리하지 않습니다. PostgreSQL 세컨더리와 달리, 읽기 쿼리도 제공하지 않습니다. 프라이머리에서 데이터를 복제하고 프라이머리가 실패할 때만 인계받습니다.
Redis Sentinel#
Redis Sentinel은 프라이머리를 감시하여 Redis에 고가용성을 제공합니다. 여러 Sentinel이 프라이머리가 사라진 것을 감지하면, Sentinel은 새로운 리더를 결정하기 위해 선출을 수행합니다.
장애 모드#
리더 없음: Redis 클러스터는 프라이머리가 없는 모드에 들어갈 수 있습니다.
예를 들어, Redis 노드가 잘못된 노드를 팔로우하도록 잘못 구성된 경우 이런 일이 발생할 수 있습니다.
때로는 REPLICAOF NO ONE 명령을 사용하여 하나의 노드를 강제로 프라이머리로 만들어야 합니다.
Sidekiq#
Sidekiq은 Ruby on Rails 애플리케이션에서 사용되는 멀티 스레드 백그라운드 job 처리 시스템입니다. GitLab에서 Sidekiq은 다음을 포함한 많은 활동의 무거운 작업을 처리합니다:
-
푸시 후 머지 리퀘스트 업데이트.
-
이메일 메시지 전송.
-
사용자 인가(권한) 업데이트.
-
CI 빌드 및 파이프라인 처리.
job의 전체 목록은 GitLab 코드베이스의
app/workers
및
ee/app/workers
디렉터리에서 찾을 수 있습니다.
폭주 큐#
job이 Sidekiq 큐에 추가되면, Sidekiq 워커 스레드는 큐에서 이 job을 가져와 추가되는 속도보다 빠르게 완료해야 합니다. 불균형이 발생하면(예: 데이터베이스 지연 또는 느린 job), Sidekiq 큐가 팽창하여 폭주 큐로 이어질 수 있습니다.
최근 몇 달 동안 이러한 큐 중 많은 수가 PostgreSQL, PgBouncer 및 Redis의 지연으로 인해 팽창했습니다. 예를 들어, PgBouncer 포화는 job이 데이터베이스 연결을 얻기 전에 몇 초를 기다리게 할 수 있으며, 이는 대규모 속도 저하로 연쇄될 수 있습니다. 이러한 기본 상호 연결을 최적화하는 것이 우선입니다.
그러나 큐가 적시에 비워지도록 하는 몇 가지 전략이 있습니다:
-
처리 용량을 추가합니다. Sidekiq의 인스턴스를 더 많이 시작하거나 Sidekiq Cluster를 통해 수행할 수 있습니다.
-
job을 더 작은 작업 단위로 분할합니다. 예를 들어,
PostReceive는 이전에 푸시의 각 커밋 메시지를 처리했지만, 이제는 이를ProcessCommitWorker에 위임합니다. -
큐 유형별로 Sidekiq 프로세스를 재분배/게리맨더링합니다. 장기 실행 job(예: 프로젝트 임포트 관련)은 빠르게 실행되는 job(예: 이메일 전달)을 종종 밀어낼 수 있습니다. 이 기법을 사용하여 기존 Sidekiq 배포를 최적화했습니다.
-
job을 최적화합니다. 불필요한 작업 제거, 네트워크 호출 감소(SQL 및 Gitaly 포함), 프로세서 시간 최적화는 상당한 이점을 가져올 수 있습니다.
Sidekiq 로그에서 가장 자주 실행되거나 가장 오래 걸리는 job을 확인할 수 있습니다. 예를 들어, 이 Kibana 시각화는 가장 많은 총 시간을 소비하는 job을 보여줍니다:
[
](/19.1/development/img/sidekiq_most_time_consuming_jobs_v12_8.png)
이것은 가장 긴 실행 시간을 기록한 job을 보여줍니다:
[
](/19.1/development/img/sidekiq_longest_running_jobs_v12_8.png)