GitLab 확장성

이 섹션에서는 확장성 및 신뢰성과 관련된 GitLab의 현재 아키텍쳐에 대해 설명합니다.

참조 아키텍처 개요

참조 아키텍처 다이어그램

도표 출처 - GitLab 직원 전용

위의 도표는 50,000명의 사용자를 위해 확장된 GitLab 참조 아키텍처를 보여줍니다. 아래에서 각 구성 요소에 대해 논의합니다.

구성 요소

PostgreSQL

PostgreSQL 데이터베이스에는 프로젝트, 이슈, 병합 요청, 사용자 등의 모든 메타데이터가 저장됩니다. 스키마는 Rails 애플리케이션의 db/structure.sql을 통해 관리됩니다.

GitLab Web/API 서버 및 Sidekiq 노드는 레일즈 객체 관계형 모델(ORM)을 사용하여 데이터베이스와 직접 통신합니다. 대부분의 SQL 쿼리는 이 ORM을 사용하여 액세스되지만 성능을 위해 또는 고급 PostgreSQL 기능(예: 재귀 CTE 또는 LATERAL JOIN)을 활용하기 위해 일부 사용자 정의 SQL도 작성됩니다.

이 응용 프로그램은 데이터베이스 스키마와 긴밀하게 결합되어 있습니다. 응용 프로그램이 시작되면 Rails는 데이터베이스 스키마를 쿼리하여 요청된 데이터의 테이블과 열 유형을 캐싱합니다. 이 스키마 캐시로 인해 응용 프로그램이 실행되는 동안 열이나 테이블을 삭제하면 사용자에게 500 에러가 발생할 수 있습니다. 이것이 응용 프로그램이 실행되는 동안 열이나 테이블을 삭제하는 과정에서 다운타임을 피하는 방법이 필요한 이유입니다.

다중 테넌시

단일 데이터베이스를 사용하여 모든 고객 데이터를 저장합니다. 각 사용자는 많은 그룹이나 프로젝트에 속할 수 있으며 그룹 및 프로젝트에 대한 액세스 수준(게스트, 개발자 또는 유지 관리자 포함)이 사용자가 볼 수 있는 것과 액세스할 수 있는 것을 결정합니다.

관리자 액세스 권한이 있는 사용자는 모든 프로젝트에 액세스하거나 사용자를 표현할 수 있습니다.

샤딩 및 파티셔닝

현재 데이터베이스는 어떠한 방식으로도 분할되지 않습니다. 현재 모든 데이터가 여러 다른 테이블에서 하나의 데이터베이스에 있습니다. 이는 간단한 응용 프로그램에서는 작동하지만 데이터 집합이 커질수록 하나의 데이터베이스를 유지하고 지원하는 것이 더 어려워집니다.

이 문제를 해결하는 두 가지 방법이 있습니다.

  • 파티셔닝. 테이블 데이터를 로컬로 분할합니다.
  • 샤딩. 데이터를 여러 데이터베이스에 분산합니다.

파티셔닝은 PostgreSQL의 내장 기능이며 응용 프로그램에서는 최소한의 변경이 필요합니다. 그러나 이것은 PostgreSQL 11이 필요합니다.

예를 들어, 테이블을 날짜로 파티션화하는 자연스러운 방법이 있습니다. 예를 들어 eventsaudit_events 테이블은 이러한 유형의 파티셔닝에 대한 자연스러운 후보입니다.

샤딩은 더 어려울 가능성이 높으며 스키마와 응용 프로그램에 상당한 변경이 필요합니다. 예를 들어, 여러 다른 데이터베이스에 프로젝트를 저장해야 한다면 “다른 프로젝트 간에 데이터를 어떻게 검색할 수 있을까?”라는 질문에 즉시 부딪히게 됩니다. 이에 대한 하나의 대답은 데이터 액세스를 응용 프로그램에서 데이터베이스를 추상화하는 API 호출로 추상화하는 것인데, 이것은 상당한 작업량이 필요합니다.

응용 프로그램에서 샤딩을 어느 정도 추상화하는 데 도움을 줄 수 있는 해결책이 있습니다. 예를 들어 Citus Data를 주의 깊게 살펴보고자 합니다. Citus Data는 ActiveRecord 모델에 테넌트 ID를 추가하는 Rails 플러그인을 제공합니다.

샤딩은 특성 수직에 따라 수행될 수도 있습니다. 이것은 각 서비스가 경계된 컨텍스트를 나타내고 해당 서비스별 데이터베이스 클러스터에서 작동하는, 데이터 중심의 샤딩에 대한 마이크로서비스 접근 방식입니다. 이 모델에서 데이터는 내부 키(테넌트 ID와 같은)에 따라 나뉘는 것이 아니라 팀 및 제품 소유권을 기반으로 합니다. 그러나 이 방법은 전통적인 데이터 중심의 샤딩과 많은 도전 과제를 공유합니다. 예를 들어, 데이터 조인은 쿼리 레이어 대신 응용 프로그램 자체에서 발생해야 하며 (추가적인 레이어인 GraphQL이 이를 완화할 수 있음) 효율적으로 실행하려면 진정한 병렬성이 필요합니다(즉, 데이터 레코드를 수집한 다음 엮는 산포-수집 모델). 루비 기반 시스템에서 이것은 자체적으로 어려운 과제입니다.

데이터베이스 크기

최근의 데이터베이스 점검에서 GitLab.com의 테이블 크기 분해를 보여줍니다. merge_request_diff_files 테이블에는 1TB 이상의 데이터가 포함되어 있기 때문에 먼저 해당 테이블의 크기를 줄이거나 제거하려고 합니다. GitLab은 객체 저장소에 차이를 저장하는 기능을 지원하고 있으며, 우리는 GitLab.com에서 이를 수행하고자 합니다.

고가용성

고가용성과 장애 조치에 대한 여러 전략이 있습니다:

  • Write-ahead logs (WAL)은 객체 스토리지(예: S3 또는 Google Cloud Storage)로 스트리밍됩니다.
  • Read-복제본 (핫 백업).
  • 지연된 복제본.

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

GitLab.com에서는 Consul 및 Patroni가 함께 작동하여 read 복제본과의 장애 조치를 조정합니다. Omnibus에는 Patroni가 함께 제공됩니다.

부하 분산

GitLab EE에는 read 복제본을 사용하여 부하 분산을 지원하는 애플리케이션 기능이 있습니다. 이 부하 분산기는 일반적인 부하 분산기에서 사용할 수 없는 일부 작업을 수행합니다. 예를 들어, 해당 애플리케이션은 복제본을 고려할 때 유효합니다만 복제된 데이터의 지연이 낮을 때에만 복제본으로 고려합니다(예: WAL 데이터가 100MB 미만으로 따라가는 경우).

자세한 내용은 블로그 게시물에서 확인할 수 있습니다.

PgBouncer

PostgreSQL은 각 요청에 대해 백엔드 프로세스를 포크하기 때문에, 기본적으로 대략 300개의 연결을 지원할 수 있는 한정된 한도가 있습니다. PgBouncer와 같은 연결 풀러가 없으면 연결 제한에 도달할 수 있습니다. 제한에 도달하면 GitLab은 연결이 가능해질 때까지 대기하면서 오류를 생성하거나 속도가 느려질 수 있습니다.

고가용성

PgBouncer는 단일 스레드 프로세스입니다. 과부하로 인해 PgBouncer가 단일 코어를 포화시킬 수 있으며, 이는 백그라운드 작업 및/또는 웹 요청에 대한 응답 시간이 느려지는 결과를 낳을 수 있습니다. 이 한계를 해결하는 두 가지 방법이 있습니다:

  • 다중 PgBouncer 인스턴스 실행
  • 다중 스레드 연결 풀러 사용 (예: Odyssey)

몇몇 Linux 시스템에서는 동일한 포트에서 여러 PgBouncer 인스턴스를 실행하는 것이 가능합니다.

GitLab.com에서는 하나의 코어를 포화시키는 것을 피하기 위해 여러 PgBouncer 인스턴스를 다른 포트에서 실행합니다.

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

  • 서로 다른 가용 영역에 있는 여러 PgBouncer 인스턴스가 PostgreSQL 기본에 연결됩니다.
  • 여러 PgBouncer 프로세스가 PostgreSQL read 복제본과 공존합니다.

복제본의 경우 공존이 유리한데, 이는 네트워크 호합 및 지연이 줄어들기 때문입니다. 그러나 기본의 경우, 공존은 불리하며, PgBouncer는 단일 장애 지점이 되어 오류를 유발할 수 있습니다. 장애 조치가 발생하면 두 가지 사태 중 하나가 발생할 수 있습니다:

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

첫 번째 경우, 만약 PgBouncer가 기본과 공존한다면, 데이터베이스 연결은 타임아웃되거나 연결에 실패하여 다운타임이 발생할 수 있습니다. 기본을 향한 부하 분산기 앞에 여러 PgBouncer 인스턴스를 두어 이를 완화할 수 있습니다.

두 번째 경우, 새롭게 강등된 복제본으로의 기존 연결이 쓰기 쿼리를 실행할 수 있습니다. 장애 조치 중에는 기본과 통신하는 PgBouncer를 종료하여 더 이상 트래픽이 도착하지 않도록 하는 것이 유리할 수 있습니다. 대안으로는 애플리케이션이 장애 조치 이벤트를 인식하고 연결을 원활하게 종료하는 것이 있습니다.

Redis

GitLab에서 Redis를 사용하는 방법은 세 가지가 있습니다:

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

대규모로 운영되는 GitLab 인스턴스의 경우, Redis 사용을 별도의 Redis 클러스터로 분리하는 것이 두 가지 이유로 도움이 됩니다:

  • 각각에는 다른 지속성 요구사항이 있습니다.
  • 부하 격리.

예를 들어, 캐시 인스턴스는 maxmemory 구성 옵션을 설정하여 최근에 사용하지 않은(LRU) 캐시처럼 동작할 수 있습니다. 이 설정은 큐나 지속적인 클러스터에 대해 설정하면 않됩니다. 왜냐하면 데이터가 무작위로 메모리에서 퇴출되어 작업이 삭제될 수 있기 때문입니다. 이로 인해 병합이 실행되지 않거나 빌드가 업데이트되지 않는 등 여러 문제가 발생할 수 있습니다.

Sidekiq는 자체의 큐를 상당히 자주 폴링하며, 이 활동은 다른 쿼리가 느려지게 할 수 있습니다. 따라서, Sidekiq를 위한 전용 Redis 클러스터를 갖는 것은 성능을 향상시키고 Redis 프로세스에 대한 부하를 줄일 수 있습니다.

고가용성/위험

단일 코어: PgBouncer와 마찬가지로, 단일 Redis 프로세스는 하나의 코어만 사용할 수 있습니다. 병렬 처리를 지원하지 않습니다.

어리석은 복제본: Redis 복제본(또는 read replicas)은 실제로 어떠한 부하도 처리하지 않습니다. PostgreSQL 복제본과 달리 읽기 쿼리조차 서빙하지 않습니다. 이들은 기본으로부터 데이터를 복제하고 기본이 실패할 때만 인계받습니다.

Redis Sentinels

Redis Sentinel은 주요 서버를 모니터링하여 Redis의 고가용성을 제공합니다. 여러 Sentinel이 주요 서버가 없어졌다고 감지하면, 새 리더를 결정하기 위해 선거를 수행합니다.

Failure Modes

No leader: Redis 클러스터는 주요 서버가 없는 상태가 될 수 있습니다. 예를 들어, Redis 노드가 잘못된 노드를 따르도록 잘못 구성되어있는 경우에 발생할 수 있습니다. 때로는 REPLICAOF NO ONE 명령을 사용하여 한 노드를 강제로 주요 서버로 만들어야 할 수도 있습니다.

Sidekiq

Sidekiq는 루비 온 레일즈 어플리케이션에서 사용되는 멀티 스레드, 백그라운드 작업 처리 시스템입니다. GitLab에서 Sidekiq은 다음과 같은 많은 활동의 중요한 부분을 담당합니다:

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

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

Runaway Queues

Sidekiq 큐에 작업이 추가됨에 따라, Sidekiq 워커 스레드는 이러한 작업을 큐에서 가져 와서 추가되는 속도보다 빨리 완료해야 합니다. 데이터베이스나 느린 작업과 같은 균형이 깨지면(Sidekiq 큐가 비정상적으로 커지고 도망가는 큐로 이어질 수 있습니다).

최근들어, PostgreSQL, PgBouncer 및 Redis의 지연으로 인해 많은 큐가 비정상적으로 커졌습니다. 예를 들어, PgBouncer 포화는 몇 초 기다린 후 데이터베이스 연결을 얻도록 작업해 가능한 큰 지연으로 이어집니다. 기본적인 상호 연결을 최적화하는 것이 가장 먼저 필요합니다.

그러나 큐가 제때에 드레인되도록 보장하기 위한 몇 가지 전략이 있습니다:

  • 더 많은 처리 용량 추가. Sidekiq나 Sidekiq Cluster를 더 많이 실행하여 수행할 수 있습니다.
  • 작업을 더 작은 작업 단위로 분할. 예를 들어, PostReceive는 이제 푸시에서 각 커밋 메시지를 처리했지만, 이제 이를 ProcessCommitWorker로 분리하여 처리합니다.
  • 큐 유형별로 Sidekiq 프로세스를 재배치/재배포합니다. 장기 실행되는 작업(예: 프로젝트 가져오기와 관련된 작업)은 종종 빠르게 실행되는 작업(예: 이메일 전달과 관련된 작업)을 제한할 수 있습니다. 기존 Sidekiq 배포를 최적화하기위한 이 기술을 사용했습니다.
  • 작업 최적화. 불필요한 작업 제거, 네트워크 호출(예: SQL 및 Gitaly) 줄이기 및 프로세서 시간 최적화는 중요한 이점을 얻을 수 있습니다.

Sidekiq 로그에서 가장 빈번하게 실행되거나 가장 오래 실행되는 작업을 확인할 수 있습니다. 예를 들어, 다음 Kibana 시각화에서 가장 많은 총 시간을 소비하는 작업을 보여줍니다:

가장 시간이 많이 소비되는 Sidekiq 작업

시각화 출처 - GitLab 직원 전용

이 이미지는 가장 긴 기간 동안 실행되었던 작업을 보여줍니다:

가장 오래 실행된 Sidekiq 작업

시각화 출처 - GitLab 직원 전용