GitLab 확장성

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

참조 아키텍처 개요

참조 아키텍처 다이어그램

_ 다이어그램 소스 - GitLab 직원 전용 _

위 다이어그램은 5만 명의 사용자를 위해 확장된 GitLab 참조 아키텍처를 보여줍니다. 아래에서 각 컴포넌트를 설명합니다.

컴포넌트

PostgreSQL

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

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

응용 프로그램은 데이터베이스 스키마와 밀접하게 결합되어 있습니다. 응용 프로그램이 시작되면 Rails는 데이터베이스 스키마를 쿼리하여 요청된 데이터의 테이블과 열 유형을 캐시합니다. 이 스키마 캐시 때문에 응용 프로그램이 실행되는 동안 열이나 테이블을 삭제하면 사용자에게 500 에러가 발생할 수 있습니다. 이것이 열을 삭제하고 기타 다운타임 변경을 피하는 프로세스가 있는 이유입니다.

Multi-tenancy

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

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

샤딩 및 분할

데이터베이스는 현재 어떤 방식으로도 나뉘어지지 않습니다. 현재 모든 데이터가 여러 다른 테이블의 하나의 데이터베이스에 저장됩니다. 이는 간단한 응용 프로그램에는 작동하지만 데이터 집합이 커질수록 많은 행을 포함하는 테이블이 하나인 데이터베이스를 유지하고 지원하는 것이 더 어려워집니다.

이 문제를 다루는 데에는 두 가지 방법이 있습니다.

  • 분할. 테이블 데이터를 지역적으로 분할합니다.
  • 샤딩. 데이터를 여러 데이터베이스에 분산합니다.

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

예를 들어, 분할하는 자연스러운 방법은 날짜별로 테이블을 분할하는 것입니다. 예를 들어 eventsaudit_events 테이블은 이러한 유형의 분할에 적합한 후보입니다.

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

일부 솔루션은 애플리케이션에서 샤딩을 어느 정도 추상화하는 데 도움을 줄 수 있습니다. 예를 들어 우리는 Citus Data를 면밀히 살펴보고 싶습니다. Citus Data는 ActiveRecord 모델에 테넌트 ID를 추가하는 Rails 플러그인을 제공합니다.

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

데이터베이스 크기

최근 데이터베이스 점검에서 GitLab.com의 테이블 크기를 분해한 결과가 나왔습니다. merge_request_diff_files는 1TB 이상의 데이터를 포함하고 있기 때문에, 우리는 먼저 이 테이블을 줄이거나 제거하고 싶습니다. GitLab은 객체 리포지터리에 diffs를 저장하는 기능을 지원하기 때문에, 우리는 GitLab.com에서 이를 수행하고자 합니다.

고가용성(High availability)

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

  • 기록 전 로그(WAL)가 객체 리포지터리(예: S3 또는 Google Cloud Storage)로 스트리밍됩니다.
  • 읽기 복제본(핫 백업).
  • 지연 복제본.

특정 시점으로부터 데이터베이스를 복원하기 위해서는 해당 사건 이전에 기본 백업(base backup)이 있어야 합니다. 데이터베이스가 해당 백업으로부터 복원된 후, 데이터베이스는 해당 시점에 도달할 때까지 순서대로 WAL 로그를 적용할 수 있습니다.

GitLab.com에서는 Consul과 Patroni가 함께 작동하여 읽기 복제본을 사용한 장애 조치를 조정합니다. Omnibus에는 Patroni가 함께 제공됩니다.

부하 분산

GitLab EE에는 읽기 복제본을 사용하여 부하 분산을 지원하는 응용 프로그램 지원이 있습니다. 이 부하 분산기에는 전통적으로 표준 부하 분산기에서 사용할 수 없는 몇 가지 작업이 포함되어 있습니다. 예를 들어, 응용 프로그램은 복제된 데이터의 지연이 낮을 경우에만 해당 복제본을 고려합니다(예: WAL 데이터가 100MB 이하로 뒤쳐져 있을 경우).

더 많은 상세 정보는 블로그 게시물에 있습니다.

PgBouncer

PostgreSQL은 각 요청에 대해 백엔드 프로세스를 복제하기 때문에, 일반적으로 기본적으로 300회 정도의 연결을 지원할 수 있는 유한한 한계가 있습니다. PgBouncer와 같은 연결 풀러가 없으면 연결 제한 수에 도달하는 것이고, 한계에 도달하면 GitLab은 연결이 가능할 때까지 기다리거나 오류가 발생하거나 느려질 수 있습니다.

고가용성

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

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

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

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

또한, 기본 및 이중화와 통신하는 PgBouncer 인스턴스는 약간 다르게 설정됩니다.

  • 서로 다른 가용 영역에서 여러 PgBouncer 인스턴스가 PostgreSQL 기본과 통신합니다.
  • PostgreSQL 읽기 복제본과 동일한 곳에 PgBouncer 프로세스가 함께 있습니다.

복제본의 경우, 병렬 배치가 유리하기 때문에 동일한 위치에 두는 것이 네트워크 호핑 및 지연을 줄이는 이점이 있습니다. 그러나 기본의 경우, 동일한 위치에 두는 것은 PgBouncer가 단일 장애 지점이 되어 오류를 발생시키고 다운타임이 발생할 수 있기 때문에 불리합니다. 장애 조치가 발생하면 두 가지 사항 중 하나가 발생할 수 있습니다.

  • 기본이 네트워크에서 사라진다.
  • 기본이 복제본이 된다.

첫 번째 경우에는, PgBouncer가 기본과 같이 있으면 데이터베이스 연결이 시간 초과되거나 연결할 수 없게 되어 다운타임이 발생할 수 있습니다. 기본이 복제본이 되는 경우, 기존 연결은 기존의 쓰기 쿼리를 실행할 수 있는데, 이는 실패할 수 있습니다. 장애 조치 과정에서 기본과 통신하는 PgBouncer를 종료하여 해당 트래픽이 더 이상 오지 않도록 하는 것이 유리할 수 있습니다. 다른 대안은 응용 프로그램을 장애 조치 이벤트를 알리고 연결을 정상적으로 종료시키는 것입니다.

Redis

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

  • 대기열: Sidekiq 작업은 작업을 JSON 페이로드로 마샬합니다.
  • 지속 상태: 세션 데이터 및 배타적 임대권.
  • 캐시: 리포지터리 데이터 (브랜치 및 태그 이름과 같은) 및 뷰 부분.

규모에 맞게 실행되는 GitLab 인스턴스의 경우 Redis 사용을 분리된 Redis 클러스터로 나누는 것이 두 가지 이유로 도움이 됩니다:

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

예를 들어, 캐시 인스턴스는 maxmemory 구성 옵션을 설정하여 가장 최근에 사용하지 않은 (LRU) 캐시처럼 동작할 수 있습니다. 이 옵션은 대기열이나 지속 클러스터에 설정해서는 안 되며, 그렇게 하면 데이터가 무작위로 메모리에서 제거됩니다. 이로 인해 작업이 거부되어 많은 문제(Merge 실행 안 됨 또는 빌드 업데이트 안 됨 등)가 발생할 수 있습니다.

또한, Sidekiq는 자주 대기열을 폴링하며 이 활동은 다른 쿼리를 느리게 할 수 있습니다. 이유로 인해 Sidekiq 전용 Redis 클러스터를 사용하면 성능이 향상되고 Redis 프로세스의 부하가 감소할 수 있습니다.

고가용성/리스크

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

멍청한 보조: Redis 보조(또는 복제본이라고도 함)은 실제로 어떤 부하도 처리하지 않습니다. PostgreSQL 보조와는 달리 읽기 쿼리조차도 처리하지 않습니다. 기본적으로 주(primary)로부터 데이터를 복제하고 주(primary)가 실패할 때만 대체합니다.

Redis Sentinel

Redis Sentinel은 기본 주(primary)를 감시하여 Redis의 고가용성을 제공합니다. 여러 Sentinel이 기본 주(primary)가 없어졌다고 감지하면 새 리더를 결정하기 위해 선거를 수행합니다.

실패 모드

리더가 없음: Redis 클러스터가 주(primary)가 없는 상태로 들어갈 수 있습니다. 예를 들면 Redis 노드가 잘못된 노드를 따르도록 잘못 구성된 경우에 발생할 수 있습니다. 때로는 이 명령어(REPLICAOF NO ONE command)를 사용하여 한 노드를 주(primary)로 만들도록 강제해야 할 수도 있습니다.

Sidekiq

Sidekiq는 루비 온 Rails 애플리케이션에서 사용되는 멀티 스레드 백그라운드 작업 처리 시스템입니다. GitLab에서 Sidekiq는 많은 활동(예: 푸시 후 Merge Request 업데이트, 이메일 메시지 전송, 사용자 권한 업데이트, CI 빌드 및 파이프라인 처리 등)의 중요 작업을 수행합니다.

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

도망 대기열

작업이 Sidekiq 대기열에 추가되면 Sidekiq 작업자 스레드는 이러한 작업들을 큐에서 끌어와 추가되는 속도보다 더 빨리 완료해야 합니다. 불균형이 발생하면(예: 데이터베이스에서의 지연 또는 작업이 느릴 때) Sidekiq 대기열이 부풀어 올라 도망 대기열로 이어질 수 있습니다.

최근 몇 달 동안 PostgreSQL, PgBouncer 및 Redis의 지연으로 인해 이러한 대기열 중 많은 크기가 커졌습니다. 예를 들어, PgBouncer 포화는 작업이 데이터베이스 연결을 얻기까지 몇 초를 기다리도록 하여 큰 지연으로 이어질 수 있습니다. 기본적인 상호 연결을 최적화하는 것이 가장 먼저입니다.

그러나 대기열이 적시에 비워지도록 보장하기 위한 몇 가지 전략이 있습니다:

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

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

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

시각화 소스 - GitLab 직원 전용

이는 가장 오랜 시간 실행된 작업을 보여줍니다:

가장 오랜 시간 실행된 Sidekiq 작업

시각화 소스 - GitLab 직원 전용