GitLab 확장성
이 섹션에서는 확장성 및 신뢰성과 관련된 GitLab의 현재 아키텍처에 대해 설명합니다.
참조 아키텍처 개요
위 다이어그램은 50,000명의 사용자를 위해 확장된 GitLab 참조 아키텍처를 보여줍니다. 아래에서 각 구성 요소에 대해 설명합니다.
구성 요소
PostgreSQL
PostgreSQL 데이터베이스에는 프로젝트, 이슈, 병합 요청, 사용자 등의 모든 메타데이터가 저장됩니다. 스키마는 Rails 애플리케이션의 db/structure.sql에서 관리됩니다.
GitLab Web/API 서버 및 Sidekiq 노드는 Rails 객체 관계 모델(ORM)을 사용하여 데이터베이스와 직접 통신합니다. 대부분의 SQL 쿼리는 이 ORM을 사용하여 액세스되지만 성능 또는 고급 PostgreSQL 기능(예: 재귀 CTE 또는 LATERAL JOIN)을 활용하기 위해 일부 사용자 지정 SQL도 작성됩니다.
응용 프로그램은 데이터베이스 스키마에 밀접하게 연결됩니다. 응용 프로그램이 시작될 때 Rails는 데이터베이스 스키마를 쿼리하여 요청된 데이터의 테이블 및 열 유형을 캐시합니다. 이 스키마 캐시로 응용 프로그램이 실행 중에 열 또는 테이블을 삭제하면 사용자에게 500 오류가 발생할 수 있습니다. 이것이 릴리스 막음 시간 변경 사항을 방지하는 프로세스의 이유입니다.
다중 테넌시
단일 데이터베이스를 사용하여 모든 고객 데이터가 저장됩니다. 각 사용자는 여러 그룹 또는 프로젝트에 속할 수 있으며 그룹 및 프로젝트에 대한 액세스 수준(게스트, 개발자 또는 유지 관리자 포함)이 사용자가 볼 수 있는 것과 액세스할 수 있는 것을 결정합니다.
관리자 액세스 권한이있는 사용자는 모든 프로젝트에 액세스하거나 사용자를 가장할 수도 있습니다.
샤딩 및 파티셔닝
데이터베이스는 현재 어떤 방식으로도 분할되지 않습니다. 현재 모든 데이터가 여러 다른 테이블의 하나의 데이터베이스에 저장됩니다. 이 방식은 간단한 응용프로그램에는 동작하지만, 데이터 세트가 커질수록 하나의 데이터베이스를 유지 및 지원하는 것이 더 어려워집니다.
이를 처리하는 두 가지 방법이 있습니다.
- 파티셔닝. 로컬로 테이블 데이터를 분할합니다.
- 샤딩. 여러 데이터베이스에 데이터를 분산합니다.
파티셔닝은 PostgreSQL의 기본 기능이며 응용프로그램에서는 최소한의 변경이 필요합니다. 그러나 이를 위해 PostgreSQL 11이 필요합니다.
예를 들어, 파티션을 만드는 자연스러운 방법은 날짜별로 테이블을 파티션하는 것입니다. 예를 들어 events
및 audit_events
테이블은 이러한 유형의 파티셔닝에 적합한 후보입니다.
샤딩은 더 어렵고 스키마 및 응용 프로그램에 상당한 변경이 필요합니다. 예를 들어, 많은 다른 데이터베이스에 프로젝트를 저장해야하는 경우 “다른 프로젝트에서 데이터를 검색하는 방법”이라는 질문에 즉시 부딪칩니다. 이에 대한 한 가지 답변은 데이터 액세스를 응용 프로그램에서 데이터베이스를 추상화하는 API 호출로 추상화하는 것입니다. 그러나 이는 상당한 양의 작업이 필요합니다.
의도한 대로 응용 프로그램에서 샤딩을 추상화하는 데 도움이되는 솔루션이 있습니다. 예를 들어 Citus Data를 정밀히 살펴보기를 희망합니다. Citus Data는 ActiveRecord 모델에 테넌트 ID를 추가하는 Rails 플러그인을 제공합니다.
샤딩은 기능 세로로도 수행할 수 있습니다. 이것은 각 서비스가 경계된 컨텍스트를 나타내고 해당 서비스별 데이터베이스 클러스터에서 작동하는 마이크로 서비스 접근법입니다. 그 모델에서 데이터는 내부 키(예: 테넌트 ID와 같은)에 따라 분산되는 대신 팀 및 제품 소유권에 따라 분산됩니다. 그러나 이는 기존의 데이터 중심 샤딩과 많은 도전과 유사합니다. 예를 들어, 데이터 결합은 쿼리 레이어가 아닌 응용 프로그램 자체에서 발생해야하며(추가 계층, 예를 들어 GraphQL이 이를 완화할 수 있음), 효율적으로 실행하려면 진정한 병렬 처리가 필요합니다(즉, 데이터 레코드를 수집하고 묶는 분산-수집 모델), Ruby 기반 시스템에서는 이 자체가 어려운 작업입니다.
데이터베이스 크기
최근 데이터베이스 점검에서 GitLab.com의 테이블 크기 분해가 보여집니다. merge_request_diff_files
에는 1TB 이상의 데이터가 포함되어 있기 때문에 먼저이 테이블을 축소/제거하려고 합니다. GitLab은 오브젝트 스토리지에 차이를 저장하는 지원을 제공하며 이를 GitLab.com에서 수행하려고 합니다.
고가용성
고가용성 및 장애 조치를 제공하는 여러 가지 전략이 있습니다.
- 변경 기록(WAL)이 객체 스토리지(예: S3 또는 Google Cloud Storage)로 스트리밍됩니다.
- 읽기 복제본(핫 백업).
- 지연된 복제본.
특정 시점의 데이터베이스를 복원하려면 해당 사건 이전에 기본 백업이 있어야합니다. 데이터베이스가 해당 백업에서 복원 된 후, 데이터베이스는 목표 시간에 도달 할 때까지 순서대로 WAL 로그를 적용할 수 있습니다.
GitLab.com에서 Consul 및 Patroni는 읽기 복제본과 협력하여 장애 조치를 조정합니다. Omnibus는 Patroni가 함께 제공됩니다.
부하 분산
GitLab EE에는 읽기 복제본을 사용하여 부하 분산을 지원하는 응용 프로그램 지원이 포함되어 있습니다. 이 부하 분산 장치는 일반적인 부하 분산기에서 사용할 수 없는 일부 조치를 수행합니다. 예를 들어 응용 프로그램은 복제본을 고려합니다. 복제 지연이 낮은 경우에만(예: WAL 데이터가 100MB 미만으로 후행) 복제본을 고려합니다.
자세한 내용은 블로그 게시물에서 확인할 수 있습니다.
PgBouncer
PostgreSQL은 각 요청에 대해 백엔드 프로세스를 포크하므로 기본적으로 약 300개 정도의 연결을 지원 할 수있는 한계가 있습니다. PgBouncer와 같은 연결 풀러가 없으면 연결 한도에 도달 할 수 있습니다. 한도에 도달하면 GitLab은 연결이 사용 가능해질 때까지 기다리면서 오류를 생성하거나 속도가 느려질 수 있습니다.
고가용성
PgBouncer는 단일 스레드 프로세스입니다. 고트래픽 시에는 PgBouncer가 단일 코어를 포화시킬 수 있으며, 이는 백그라운드 작업 및/또는 웹 요청의 응답 시간이 느려지는 결과를 초래할 수 있습니다. 이 한계를 해결하는 두 가지 방법이 있습니다:
- 여러 개의 PgBouncer 인스턴스 실행.
- 멀티 스레드 연결 풀러 사용(예: Odyssey.
일부 리눅스 시스템에서는 같은 포트에서 여러 개의 PgBouncer 인스턴스 실행이 가능합니다.
GitLab.com에서는 단일 코어를 포화시키지 않기 위해 여러 개의 PgBouncer 인스턴스를 다른 포트에서 실행합니다.
또한, 주/보조와 통신하는 PgBouncer 인스턴스는 약간 다르게 설정됩니다:
- 다른 가용 영역의 여러 PgBouncer 인스턴스가 PostgreSQL 주요 부분과 통신합니다.
- 여러 PgBouncer 프로세스는 PostgreSQL 읽기 레플리카와 공존합니다.
레플리카의 경우 공존이 유리한데, 이는 네트워크 홉과 지연 시간을 줄일 수 있기 때문입니다. 그러나 주요 부분의 경우 공존은 불리한데, PgBouncer가 단일 장애 지점이 되어 오류를 초래할 수 있기 때문입니다. 장애 조치 중에는 두 가지 사항 중 하나가 발생할 수 있습니다:
- 주요 부분이 네트워크에서 사라집니다.
- 주요 부분이 레플리카가 됩니다.
첫 번째 경우, PgBouncer가 주요 부분과 공존할 경우 데이터베이스 연결 시간이 초과되거나 연결이 실패하여 다운타임이 발생합니다. 이를 완화하려면 주요 부분에 대상 로드 밸런서 앞에 여러 PgBouncer 인스턴스를 두는 것이 좋습니다.
두 번째 경우, 존재하는 새로 강등된 레플리카로의 연결은 쓰기 쿼리를 실행할 수 있으며 이는 실패합니다. 장애 조치 중에는 주요 부분과 통신하는 PgBouncer를 종료하여 추가 트래픽이 도착하지 않도록 하는 것이 유리할 수 있습니다. 대안은 응용 프로그램이 장애 조치 이벤트를 감지하고 연결을 스레드를 안전하게 종료하는 것일 수 있습니다.
Redis
GitLab에서 Redis가 사용되는 세 가지 방법이 있습니다:
- 큐: Sidekiq 작업은 작업을 JSON 페이로드로 변환합니다.
- 지속 상태: 세션 데이터 및 배타적 리스 생성.
- 캐시: 저장소 데이터(예: 브랜치 및 태그 이름) 및 뷰 파셜.
대규모로 실행되는 GitLab 인스턴스의 경우, Redis 사용을 별도의 Redis 클러스터로 분리하는 것이 두 가지 이유로 도움이 됩니다:
- 각각의 다른 지속성 요구사항이 있습니다.
- 부하 격리.
예를 들어, 캐시 인스턴스는 maxmemory
구성 옵션을 설정하여 LRU(최근에 사용되지 않은) 캐시처럼 동작할 수 있습니다. 이 옵션은 큐 또는 지속 클러스터에 설정해서는 안 되며, 이는 데이터가 무작위로 메모리에서 제거되기 때문에 작업이 취소될 수 있기 때문입니다(예: 병합이 실행되지 않거나 빌드가 업데이트되지 않음).
Sidekiq는 큐를 상당히 자주 조사합니다. 이 때문에 Sidekiq에 대한 전용 Redis 클러스터는 성능을 향상시키고 Redis 프로세스의 부하를 줄일 수 있습니다.
고가용성/위험
단일 코어: PgBouncer와 마찬가지로 Redis 단일 프로세스는 하나의 코어만 사용할 수 있습니다. 멀티 스레딩을 지원하지 않습니다.
덤 세컨더리: Redis 세컨더리(레플리카로도 알려짐)는 실제로 어떠한 부하도 처리하지 않습니다. PostgreSQL 레플리카와는 달리 읽기 쿼리조차 서빙하지 않습니다. 그들은 주요 부분에서 데이터를 복제하고 주요 부분이 실패할 때만 인수해 가집니다.
Redis Sentinel
Redis Sentinel은 주요 부분을 관찰하여 Redis의 고가용성을 제공합니다. 여러 Sentinel이 주요 부분의 사라짐을 감지하면 Sentinel은 새 리더를 결정하는 선거를 진행합니다.
실패 모드
리더 없음: Redis 클러스터는 주요 부분이 없는 모드로 진입할 수 있습니다. 예를 들어, Redis 노드가 잘못된 노드를 따르도록 잘못 구성된 경우에 발생할 수 있습니다. 이 경우, 때로는 하나의 노드를 REPLICAOF NO ONE
명령어를 사용하여 강제로 주요로 만들어야 할 수 있습니다.
Sidekiq
Sidekiq는 루비 온 레일 애플리케이션에서 사용되는 멀티 스레드 백그라운드 작업 처리 시스템입니다. GitLab에서 Sidekiq은 많은 활동을 수행하며, 여기에는 다음과 같은 작업이 포함됩니다:
- 푸시 후에 병합 요청 업데이트.
- 이메일 메시지 전송.
- 사용자 권한 업데이트.
- CI 빌드 및 파이프라인 처리.
전체 작업 목록은 GitLab 코드베이스의 app/workers
및 ee/app/workers
디렉터리에서 찾을 수 있습니다.
오버런 큐
Sidekiq 큐에 작업이 추가되면 Sidekiq 작업 스레드는 큐로부터 이러한 작업을 가져와 추가된 것보다 빠르게 완료해야 합니다. 불균형이 발생하면(예: 데이터베이스의 지연 또는 느린 작업), Sidekiq 큐는 팽창되어 오버런 큐로 이어질 수 있습니다.
최근 몇 달 동안 PostgreSQL, PgBouncer 및 Redis의 지연으로 인해 많은 큐가 팽창했습니다. 예를 들어, PgBouncer 포화는 데이터베이스 연결을 얻기 전에 몇 초를 기다리게 함으로써 큐를 팽창시킬 수 있으며, 이는 큰 지연으로 이어질 수 있습니다. 먼저 이러한 기본 상호 작용을 최적화하는 것이 중요합니다.
그러나 큐가 시간 내에 비워지도록 보장하는 여러 가지 전략이 있습니다:
- 더 많은 처리 용량 추가. 이것은 Sidekiq 또는 Sidekiq Cluster의 더 많은 인스턴스를 생성하여 수행할 수 있습니다.
- 작업을 더 작은 작업 단위로 분할. 예를 들어,
PostReceive
는 푸시에서 각 커밋 메시지를 처리했지만, 이제ProcessCommitWorker
로 이를 위임합니다. - 대기열 유형별로 Sidekiq 프로세스를 재배치/분할. 긴 실행 작업(예: 프로젝트 가져오기와 관련된 작업)은 보통 빠르게 실행되는 작업(예: 이메일 전달)을 밀어낼 수 있습니다. 기존 Sidekiq 배포를 최적화하기 위해 이 기술을 사용했습니다.
- 작업 최적화. 불필요한 작업을 제거하고, 네트워크 호출(예: SQL 및 Gitaly 포함)을 줄이고, 프로세서 시간을 최적화함으로써 상당한 이점을 얻을 수 있습니다.
Sidekiq 로그에서 가장 자주 실행되거나 가장 오래 실행되는 작업을 확인할 수 있습니다. 예를 들어, 이 Kibana 시각화는 가장 많은 총 시간을 소비하는 작업을 보여줍니다:
이것은 가장 긴 지속 시간을 가진 작업을 보여줍니다: