GitLab 확장성

이 섹션에서는 확장성 및 신뢰성과 관련된 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이 필요합니다.

예를 들어, 자연스럽게 파티셔닝하는 방법은 날짜별로 테이블을 파티셔닝하는 것입니다. 예를 들어, eventsaudit_events 테이블이 이러한 파티셔닝의 자연스러운 후보입니다.

샤딩은 더 어려울 가능성이 있으며 스키마 및 애플리케이션에 상당한 변경이 필요합니다. 예를 들어, 프로젝트를 여러 데이터베이스에 저장해야 한다면 즉시 “다른 프로젝트에서 데이터를 어떻게 검색할 수 있을까?”라는 질문에 직면하게 됩니다. 이 질문에 대한 한 가지 답은 데이터 액세스를 API 호출로 추상화하여 애플리케이션에서 데이터베이스를 분리하는 것이지만, 이는 상당한 양의 작업이 필요합니다.

애플리케이션에서 다소 추상화된 샤딩을 도와줄 수 있는 솔루션이 있습니다. 예를 들어, 우리는 Citus Data를 주의 깊게 살펴보려 합니다. Citus Data는 ActiveRecord 모델에 테넌트 ID를 추가하는 Rails 플러그인을 제공합니다.

샤딩은 기능 수직에 따라 수행될 수도 있습니다. 이것은 마이크로서비스 샤딩 접근 방식으로, 각 서비스가 제한된 컨텍스트를 나타내고 자체 서비스 전용 데이터베이스 클러스터에서 운영됩니다. 이 모델에서 데이터는 팀 및 제품 소유권에 따라 분산되지 않습니다. 그러나 이는 전통적인 데이터 중심 샤딩과 많은 도전 과제를 공유합니다. 예를 들어, 데이터 조인은 쿼리 레이어가 아닌 애플리케이션 자체에서 발생해야 하며(추가 레이어인 GraphQL이 이를 완화할 수 있지만), 효율적으로 실행하기 위해 진정한 병렬성이 필요합니다(즉, 데이터를 수집한 후 패킷화하는 스캐터-갇터 모델), 이는 Ruby 기반 시스템에서 본질적으로 도전 과제가 됩니다.

데이터베이스 크기

최근 데이터베이스 점검을 통해 GitLab.com의 테이블 크기 분석 결과가 나왔습니다.
merge_request_diff_files가 1TB 이상의 데이터를 포함하고 있기 때문에, 이 테이블을 먼저 줄이거나 없애고자 합니다. GitLab은 오브젝트 스토리지에 차이를 저장하는 것을 지원합니다.
그리고 우리는 GitLab.com에서 이를 구현하고자 합니다.

고가용성

고가용성과 중복성을 제공하기 위한 여러 전략이 있습니다:

  • 오브젝트 스토리지(S3, Google Cloud Storage 등)로 스트리밍되는 작성 전 로그(WAL).
  • 읽기 복제본(핫 백업).
  • 지연 복제본.

시간 특정 시점으로 데이터베이스를 복원하려면 사건 이전에 기본 백업을 수행해야 합니다. 백업에서 데이터베이스를 복원한 후, 데이터베이스는 WAL 로그를 순서대로 적용하여 목표 시점에 도달할 수 있습니다.

GitLab.com에서는 Consul과 Patroni가 함께 작동하여 읽기 복제본과의 장애 조치를 조정합니다. Omnibus에는 Patroni가 포함되어 있습니다.

로드 밸런싱

GitLab EE는 읽기 복제본을 이용한 로드 밸런싱에 대한 애플리케이션 지원을 제공합니다. 이 로드 밸런서는 전통적인 로드 밸런서에서는 제공되지 않는 몇 가지 작업을 수행합니다. 예를 들어, 애플리케이션은 복제 지연이 낮은 경우에만 복제본을 고려합니다(예: WAL 데이터가 100MB 미만 뒤쳐진 경우).

더 많은 상세 내용은 블로그 게시물에 있습니다.

PgBouncer

PostgreSQL은 각 요청에 대해 백엔드 프로세스를 포크하므로, PostgreSQL은 일반적으로 기본적으로 약 300개의 연결을 지원할 수 있는 한정된 연결 수를 가지고 있습니다. PgBouncer와 같은 연결 풀러 없이 연결 제한에 도달할 가능성이 높습니다. 제한에 도달하게 되면 GitLab은 연결이 가능해지길 기다리면서 오류를 발생시키거나 느려질 수 있습니다.

고가용성

PgBouncer는 단일 스레드 프로세스입니다. 트래픽이 많은 경우, PgBouncer가 단일 코어를 포화시킬 수 있으며, 이로 인해 백그라운드 작업 및 웹 요청에 대한 응답 시간이 느려질 수 있습니다. 이 제한을 해결하는 두 가지 방법이 있습니다:

  • 여러 개의 PgBouncer 인스턴스를 실행합니다.
  • 다중 스레드 연결 풀러를 사용합니다(예: Odyssey).

일부 리눅스 시스템에서는 같은 포트에서 여러 PgBouncer 인스턴스를 실행할 수 있습니다.

GitLab.com에서는 단일 코어를 포화시키지 않기 위해 서로 다른 포트에서 여러 PgBouncer 인스턴스를 실행합니다.

또한, 기본 및 보조와 통신하는 PgBouncer 인스턴스는 약간 다르게 설정됩니다:

  • 서로 다른 가용성 영역에 있는 여러 PgBouncer 인스턴스가 PostgreSQL 기본에 연결합니다.
  • 여러 PgBouncer 프로세스가 PostgreSQL 읽기 복제본과 함께 배치됩니다.

복제본의 경우, 배치가 유리한 이유는 네트워크 홉을 줄이고 지연 시간을 줄이기 때문입니다. 그러나 기본의 경우 배치는 불리합니다. 왜냐하면 PgBouncer가 단일 장애 지점이 되어 오류를 일으킬 수 있기 때문입니다. 장애 조치가 발생할 경우 두 가지 일이 발생할 수 있습니다:

  • 기본이 네트워크에서 사라집니다.
  • 기본이 복제본이 됩니다.

첫 번째 경우, PgBouncer가 기본과 함께 배치되어 있으면, 데이터베이스 연결이 시간 초과되거나 연결에 실패하게 되며, 다운타임이 발생합니다. 기본에 연결된 여러 PgBouncer 인스턴스가 로드 밸런서 앞에 있으면 이를 완화할 수 있습니다.

두 번째 경우, 새로 강등된 복제본에 대한 기존 연결이 쓰기 쿼리를 실행할 수 있지만, 이는 실패할 것입니다. 장애 조치 중에는 주 연결을 담당하는 PgBouncer를 종료하여 더 이상 트래픽이 유입되지 않도록 하는 것이 유리할 수 있습니다. 대안은 애플리케이션이 장애 조치 이벤트를 인지하고 연결을 정상적으로 종료하도록 하는 것입니다.

Redis

Redis는 GitLab에서 다음과 같이 세 가지 방법으로 사용됩니다:

  • 큐: Sidekiq 작업이 작업을 JSON 페이로드로 변환합니다.
  • 지속적인 상태: 세션 데이터 및 독점적인 임대.
  • 캐시: 리포지토리 데이터(브랜치 및 태그 이름과 같은) 및 뷰 부분.

규모에서 실행되는 GitLab 인스턴스의 경우, Redis 사용을 별도의 Redis 클러스터로 분할하면 두 가지 이유로 도움이 됩니다:

  • 각 클러스터는 서로 다른 지속성 요구 사항을 가지고 있습니다.
  • 부하 격리.

예를 들어, 캐시 인스턴스는 maxmemory 구성 옵션을 설정하여 가장 사용하지 않은 항목(LRU) 캐시처럼 작동할 수 있습니다. 이 옵션은 큐나 지속적인 클러스터에 대해 설정하지 않아야 합니다. 데이터가 랜덤한 시간에 메모리에서 퇴출될 수 있기 때문입니다. 이는 작업이 누락되는 원인이 될 수 있으며, 이는 병합이 실행되지 않거나 빌드가 업데이트되지 않는 등의 많은 문제를 일으킬 수 있습니다.

Sidekiq는 큐를 매우 자주 polling하므로, 이 활동이 다른 쿼리를 느리게 만들 수 있습니다. 이 때문에 Sidekiq 전용 Redis 클러스터를 두면 성능을 개선하고 Redis 프로세스에 대한 부하를 줄일 수 있습니다.

고가용성/위험

단일 코어: PgBouncer와 마찬가지로, 단일 Redis 프로세스는 하나의 코어만 사용할 수 있습니다. 다중 스레딩을 지원하지 않습니다.

무능력한 세컨더리: Redis 세컨더리(복제본이라고도 함)는 실제로 부하를 처리하지 않습니다. PostgreSQL 세컨더리와 달리, 읽기 쿼리를 제공하지도 않습니다. 주 데이터베이스에서 데이터를 복제하고, 주 데이터베이스가 실패할 때만 인수인계를 합니다.

Redis Sentinels

Redis Sentinel은 주 데이터베이스를 감시함으로써 Redis의 고가용성을 제공합니다. 여러 Sentinel이 주 데이터베이스가 사라진 것을 감지하면, Sentinel은 새로운 리더를 결정하기 위해 선거를 진행합니다.

실패 모드

리더 없음: Redis 클러스터는 주 데이터베이스가 없는 상태에 들어갈 수 있습니다. 예를 들어, Redis 노드가 잘못 구성되어 잘못된 노드를 따르는 경우 이 상황이 발생할 수 있습니다. 때때로, REPLICAOF NO ONE command를 사용하여 하나의 노드를 강제로 주로 만들어야 할 필요가 있습니다.

Sidekiq

Sidekiq는 Ruby on Rails 애플리케이션에서 사용되는 다중 스레드 배경 작업 처리 시스템입니다. GitLab에서 Sidekiq는 다양한 활동을 처리합니다:

  • 푸시 후 병합 요청 업데이트.
  • 이메일 메시지 전송.
  • 사용자 권한 업데이트.
  • CI 빌드 및 파이프라인 처리.

직접적인 작업의 전체 목록은 GitLab 코드베이스의 app/workersee/app/workers 디렉터리에서 찾을 수 있습니다.

runaway 큐

작업이 Sidekiq 큐에 추가됨에 따라, Sidekiq 작업자 스레드는 이 작업을 큐에서 가져와 추가된 속도보다 빠르게 완료해야 합니다. 불균형이 발생할 경우(예: 데이터베이스 지연 또는 느린 작업) Sidekiq 큐는 팽창하고 runaway 큐로 이어질 수 있습니다.

최근 몇 달 동안 PostgreSQL, PgBouncer 및 Redis의 지연으로 인해 이러한 큐가 많이 팽창했습니다. 예를 들어, PgBouncer 포화 상태는 작업이 데이터베이스 연결을 얻기 전에 몇 초 동안 대기하게 할 수 있으며, 이는 큰 지연으로 이어질 수 있습니다. 이러한 기본 상호 연결을 최적화하는 것이 우선입니다.

하지만 큐가 적시에 소비되도록 보장하는 데는 여러 가지 전략이 있습니다:

  • 처리 용량 추가. 이는 더 많은 Sidekiq 인스턴스를 실행하거나 Sidekiq Cluster로 수행할 수 있습니다.
  • 작업을 더 작은 작업 단위로 나누기. 예를 들어, PostReceive는 푸시의 각 커밋 메시지를 처리하는 데 사용되었으나, 이제는 이를 ProcessCommitWorker에 할당합니다.
  • 큐 유형으로 Sidekiq 프로세스 재분배/조정. 오랜 작업(예: 프로젝트 가져오기)과 관련된 작업은 종종 빠르게 실행되는 작업(예: 이메일 전송)을 압도할 수 있습니다. 우리는 기존 Sidekiq 배포를 최적화하기 위해 이 기술을 사용했습니다.
  • 작업 최적화. 불필요한 작업을 제거하고, 네트워크 호출(SQL 및 Gitaly 포함)을 줄이며, 프로세서 시간을 최적화하면 상당한 이점을 얻을 수 있습니다.

Sidekiq 로그에서 가장 자주 실행되거나 가장 오랜 시간이 걸리는 작업을 확인할 수 있습니다. 예를 들어, 이 Kibana 시각화는 총 시간을 가장 많이 소비하는 작업을 보여줍니다:

가장 많은 시간을 소모하는 Sidekiq 작업

시각화 출처 - GitLab 직원 전용

이것은 가장 긴 기간을 가진 작업을 보여줍니다:

가장 오래 실행되는 Sidekiq 작업

시각화 출처 - GitLab 직원 전용