업데이트 간의 역호환성

GitLab 배포는 여러 컴포넌트로 분해될 수 있습니다. GitLab을 업데이트하는 것은 원자적이지 않습니다. 따라서 여러 컴포넌트가 역호환되어야하는 경우가 많습니다.

일반적인 함정

어떤 의미에서 이러한 시나리오들은 모두 일시적인 상태입니다. 그러나 실제로는 라이브 및 운영 환경에서 여러 시간 동안 지속될 수 있습니다. 따라서 우리는 이를 영구 상태처럼 취급해야 합니다.

Sidekiq 워커를 수정하는 경우

예를 들어 인수를 변경할 때:

  • 이전 서명으로 작업이 대기열에 들어가지만 새로운 월간 릴리스에서 실행되는 경우 괜찮을까요?
  • 새 서명으로 작업이 대기열에 들어가지만 이전 월간 릴리스에서 실행되는 경우 괜찮을까요?

새 Sidekiq 워커를 추가하는 경우

Sidekiq 노드가 아직 업데이트되지 않았기 때문에 이러한 작업이 여러 시간 동안 실행되지 않아도 상관없을까요?

JavaScript를 수정하는 경우

브라우저에 새 JavaScript 코드가 있지만 Rails 코드가 이전 월간 릴리스에서 실행될 때 괜찮을까요?

  • REST API에서?
  • GraphQL API에서?
  • 컨트롤러 내부 API에서?

배포 전 마이그레이션을 추가하는 경우

배포 전 마이그레이션이 실행되었지만 웹, Sidekiq 및 API 노드는 이전 릴리스를 실행 중일 때 괜찮을까요?

배포 후 마이그레이션을 추가하는 경우

모든 GitLab 노드가 업데이트되었지만, 배포 후 마이그레이션이 몇 일 후에 실행되어도 상관없을까요?

백그라운드 마이그레이션을 추가하는 경우

모든 노드가 업데이트되었고, 그 후 배포 후 마이그레이션이 몇 일 후에 실행되어도 상관없을까요? 그리고 백그라운드 마이그레이션이 완료되기까지 일주일이 걸려도 상관없을까요?

Rails와 같은 의존성을 업그레이드하는 경우

몇 개의 노드가 새로운 Rails 버전을 사용해도, 일부 노드가 이전 Rails 버전을 사용해도 괜찮을까요?

업데이트의 예시

업데이트 중의 역호환성 문제는 종종 매우 미묘합니다. 이것은 다음을 숙지하는 가치가 있기 때문입니다.

이러한 문제가 발생하는 방식을 설명하기 위해 다음 예시를 살펴보세요:

  • 🚢 새 버전
  • 🙂 이전 버전

이 예시에서 월간 릴리스별로 업데이트된다고 상상할 수 있습니다. 그러나 코드가 얼마나 오랫동안 역호환되어야 하는가?를 참조하세요.

업데이트 단계 PostgreSQL DB Web 노드 API 노드 Sidekiq 노드 호환성 문제
초기 상태 🙂 🙂 🙂 🙂  
배포 전 마이그레이션 실행 🚢 단, 배포 후 마이그레이션 제외 🙂 🙂 🙂 🙂에서의 Rails 코드가 🚢으로 DB 호출을 함
Web 노드 업데이트 🚢 단, 배포 후 마이그레이션 제외 🚢 🙂 🙂 🚢의 JavaScript가 🙂로 API 호출을 함. 🚢의 Rails 코드가 🙂의 Sidekiq 노드에서 실행되는 작업을 대기열에 넣음
API 및 Sidekiq 노드 업데이트 🚢 단, 배포 후 마이그레이션 제외 🚢 🚢 🚢 🚢의 Rails 코드가 배포 후 및 백그라운드 마이그레이션 없이 DB 호출을 함
배포 후 마이그레이션 실행 🚢 🚢 🚢 🚢 🚢의 Rails 코드가 백그라운드 마이그레이션 없이 DB 호출을 함
백그라운드 마이그레이션 완료 🚢 🚢 🚢 🚢  

이 예시는 완전하지 않습니다. GitLab은 여러 가지 다양한 방법으로 배포될 수 있습니다. 또한 각 업데이트 단계도 원자적이지 않습니다. 예를 들어 롤링 배포에서는 그룹 내의 노드가 임시로 다른 버전을 실행합니다. 다소 많은 시간이 업데이트 단계 사이에 경과한다고 가정해야 합니다. 이는 GitLab.com에서도 종종 사실입니다.

코드는 얼마나 오랫동안 역호환되어야 하는가?

다운타임 없는 업데이트 지침을 따르는 사용자의 경우 답은 한 달에 한 번의 릴리스입니다. 예를 들어:

  • 13.11 => 13.12
  • 13.12 => 14.0
  • 14.0 => 14.1

GitLab.com의 경우, 하루에 여러 번의 작은 버전 업데이트가 있을 수 있으므로 변경 사항이 얼마나 역호환되어야 하는가에 대한 제한이 없습니다.

많은 사용자들이 몇몇 월간 릴리스를 건너뛰기도 합니다, 예를 들어:

  • 13.0 => 13.12

이러한 사용자들은 업데이트 중에 다운타임을 수용합니다. 유감스럽게도 이러한 경우를 완전히 무시할 수는 없습니다. 예를 들어, 13.12에서 13.0의 Sidekiq 작업이 실행될 수도 있으며, 이는 주요 릴리스 전까지 작업에서 인수를 제거하는 것을 피하는 이유를 보여주는 예시입니다. 주된 질문은: 업데이트 완료 후 배포가 좋은 상태에 도달하게 될까요?

GitLab은 어떤 컴포넌트로 분해될 수 있을까?

1000 RPS 또는 50,000 사용자 참조 아키텍처에서는 48개 이상의 노드에서 GitLab을 실행합니다. GitLab.com은 그보다 큽니다만, 일부 인프라는 Kubernetes에서 실행됩니다또한 업데이트를 먼저 받는 “카나리아” 스테이지가 있습니다.

그러나 문제는 많은 노드가 있는 것만이 아닙니다. 더 큰 문제는 배포가 다른 문맥으로 나눌 수 있다는 것입니다. 그리고 GitLab.com뿐만 아니라 다른 곳에서도 마찬가지입니다. 가능한 분할은 다음과 같습니다:

  • “카나리아 웹 앱 노드”: 일부 사용자의 비-API 요청 처리
  • “Git 앱 노드”: Git 요청 처리
  • “웹 앱 노드”: 웹 요청 처리
  • “API 앱 노드”: API 요청 처리
  • “Sidekiq 앱 노드”: Sidekiq 작업 처리
  • “PostgreSQL 데이터베이스”: 내부 PostgreSQL 호출 처리
  • “Redis 데이터베이스”: 내부 Redis 호출 처리
  • “Gitaly 노드”: 내부 Gitaly 호출 처리

업데이트 중에는 다른 문맥에서 실행되는 두 가지 다른 버전의 GitLab이 있을 것입니다. 예를 들어, 웹 노드가 이전 Sidekiq 노드에서 실행되는 작업을 대기열에 넣을 수 있습니다.

업데이트 단계의 순서는 중요한가요?

네! 다운타임 없는 업데이트를 위한 구체적인 지침이 있기 때문에 업데이트 단계의 일부 순열을 무시할 수 있습니다. 이것이 Rails 코드가 이전 PostgreSQL 데이터베이스 스키마에 대한 DB 호출을 걱정하지 않는 이유입니다.

발견한 잠재적인 역호환성 문제, 이에 대해 어떻게 대처할 수 있나요?

조정

메이저 또는 마이너 버전 업데이트에 대해 Rails 또는 Puma:

  • 품질 팀을 참여하여 MR을 철저히 테스트합니다.
  • Merge 전 MR을 @gitlab-org/release/managers에 알립니다.

피처 플래그

피처 플래그는 역호환성 문제를 처리하기 위한 도구입니다.

예를 들어, 새로운 기능과 프론트엔드 및 API 변경 사항을 기본값으로 비활성화한다면 안전합니다. 이것은 다수의 MR을 어떤 순서로든 Merge할 수 있습니다. 모든 변경 사항이 GitLab.com에 배포된 후에는 기능을 ChatOps에서 활성화하고 GitLab.com에서 유효성을 검사할 수 있습니다.

그러나 기능을 기본값으로 활성화하는 것은 반드시 안전한 것은 아닙니다. 피처 플래그가 제거되거나 기본값이 이전 릴리스의 API에 대해 실행될 때, 다운타임 없는 업데이트를 수행하는 고객은 이전 릴리스의 API에 새 프론트엔드 코드를 실행하게 됩니다.

모든 변경 사항을 동시에 활성화하는 것이 안전한지 확신이 없다면, 하나의 옵션은 API를 현재 릴리스에서 활성화하고 프론트엔드 변경 사항을 다음 릴리스에서 활성화하는 것입니다. 이것은 축소 및 확대 패턴의 예시입니다.

또한 프론트엔드를 수정하여 이전 릴리스의 API에 점진적으로 저하될 수도 있습니다.

우아한 저하

예를 들어, 프론트엔드 및 API 변경과 함께 새로운 기능을 추가할 때, 기존 API 응답에 대한 새로운 기능이 우아하게 저하되도록 프론트엔드를 작성할 수 있습니다. 이렇게 함으로써 하나의 변경 사항을 3회 릴리스에 걸쳐 전파할 필요가 없어질 수 있습니다.

확장 및 수축 패턴

온프레미스 인스턴스에 대한 제로 다운타임 업데이트를 보장하는 한 가지 방법은 확장 및 수축 패턴을 따르는 것입니다.

이는 모든 중단 변경 사항을 세 단계로 분해하는 것을 의미합니다: 확장, 이관, 계약.

  1. 확장: 소프트웨어를 역호환성 유지하면서 중단 변경 사항을 도입합니다.
  2. 이관: 모든 사용자가 새로운 구현을 사용하도록 업데이트됩니다.
  3. 계약: 역호환성이 제거됩니다.

이 세 단계는 제로 다운타임 업데이트를 허용하기 위해 다른 마일스톤의 일부여야만 합니다.

기능에 대한 지원 수준에 따라 계약 단계를 다음 주요 릴리스까지 연기할 수도 있습니다.

확장 및 수축 예시

라우트 변경, Sidekiq 워커 매개변수 변경 및 데이터베이스 마이그레이션은 모두 중단 변경 사항의 완벽한 예입니다. 이러한 변경 사항을 안전하게 처리하는 방법을 살펴봅시다.

라우트 변경

라우팅을 변경할 때, 새 버전에서 생성된 라우트가 이전 버전에 의해 제공될 수 있도록 주의해야 합니다. 여기에서 볼 수 있는 것처럼, 이를 하지 않으면 장애가 발생할 수 있습니다. 이 유형의 변경 사항은 두 구현체 간의 즉시 전환이 아닐 수 있습니다. 특히 캐너리(stage) 단계에서는 프로덕션에서 두 코드 버전이 공존하는 확장된 기간이 있습니다.

  1. 확장: 기존 컨트롤러를 가리키는 새로운 경로가 추가됩니다. 그러나 응용 프로그램에서는 새로운 경로의 링크를 생성하지 않습니다.
  2. 이관: 모든 장치에서 새 경로를 이해할 수 있는 상태이므로, 새로운 라우팅을 사용하여 링크를 생성할 수 있습니다.
  3. 계약: 이전 라우트를 안전하게 제거할 수 있습니다. (리포지터리 파일에 대한 링크와 같이 이전 경로가 널리 공유될 경우, 리다이렉트를 추가하고 이전 경로를 더 오랜 기간 유지할 수 있습니다.)

Sidekiq 워커 매개변수 변경

이 주제는 업데이트 간의 Sidekiq 호환성에서 자세히 설명되어 있습니다.

사이드키큐 워커 클래스에 새 매개변수를 추가해야 할 때, 다음 단계로 분할할 수 있습니다:

  1. 확장: 워커 클래스에 기본 값을 갖는 새 매개변수가 추가됩니다.
  2. 이관: 모든 워커의 호출에 새 매개변수를 추가합니다.
  3. 계약: 기본 값을 제거합니다.

첫눈에는 확장과 이관을 단일 마일스톤으로 묶어서 실행해도 안전해 보일 수 있지만, 이는 Puma가 Sidekiq 이전에 재시작하면 장애를 발생시킵니다. Puma는 이전 Sidekiq에서 처리할 수 없는 추가 매개변수로 작업을 큐에 등록합니다.

데이터베이스 마이그레이션

다음 그래프는 배포의 단순화된 시각적 표현이며, 이를 통해 마이그레이션 전략에서 확장과 수축이 어떻게 구현되는지 이해할 수 있습니다.

여기 특별한 고려 사항이 있습니다. 포스트-디플로이먼트 마이그레이션 프레임워크를 사용하면 세 단계를 하나의 마일스톤으로 묶을 수 있습니다.

gantt title Deployment dateFormat HH:mm section Deploy box Run migrations :done, migr, after schemaA, 2m Run post-deployment migrations :postmigr, after mcvn , 2m section Database Schema A :done, schemaA, 00:00 , 1h Schema B :crit, schemaB, after migr, 58m Schema C. : schemaC, after postmigr, 1h section Machine A Version N :done, mavn, 00:00 , 75m Version N+1 : after mavn, 105m section Machine B Version N :done, mbvn, 00:00 , 105m Version N+1 : mbdone, after mbvn, 75m section Machine C Version N :done, mcvn, 00:00 , 2h Version N+1 : mbcdone, after mcvn, 1h

이 스키마를 데이터베이스 관점에서 바라보면, 두 개의 배포가 단일 GitLab 배포로 이어진다는 것을 알 수 있습니다:

  1. Schema A에서 Schema B
  2. Schema B에서 Schema C

이러한 배포는 완벽하게 응용 프로그램 변경과 일치합니다.

  1. 처음에는 Schema A에서 Version N을 가지고 있습니다.
  2. 그런 다음 Version NVersion N+1이 모두 Schema B에 오랜 기간이 걸려 공존합니다.
  3. Schema B에서 Version N+1만 있는 경우 스키마가 다시 변경됩니다.
  4. 마지막으로 Schema C에서 Version N+1을 가지고 있습니다.

모든 이러한 세부 정보를 염두에 두면, 쿼리를 바꾸어야 하는 상황에서 고려해야 할 내용을 상상해보십시오. 이 쿼리에는 지원하는 인덱스가 있습니다.

  1. 확장: Schema A에서 Schema B로의 배포입니다. 새 인덱스를 추가하지만 현재는 응용 프로그램에서 무시합니다.
  2. 이관: Version N에서 Version N+1로의 응용 프로그램 배포입니다. 새 코드가 배포되었을 때, 지금까지의 시점에서는 새 쿼리만 실행됩니다.
  3. 계약: Schema B에서 Schema C로 (포스트-디플로이먼트 마이그레이션) 아무것도 이전 인덱스를 사용하지 않으므로 안전하게 제거할 수 있습니다.

이는 단순히 예시에 불과합니다. 특히 백그라운드 마이그레이션이 필요한 경우보다 복잡한 마이그레이션에는 하나 이상의 마일스톤이 필요할 수 있습니다. 자세한 내용은 마이그레이션 스타일 가이드를 참조하십시오.

이전 사건의 예시

일부 링크가 이슈 및 MR에서 깨졌습니다

MR 라우트를 이동했을 때, 새 서버의 사용자는 새 URL로 리디렉트되었습니다. 이 사용자들이 이러한 새 URL을 마크다운(또는 다른 곳)에 공유하면, 이는 이전 서버의 사용자에게는 깨진 링크가 됩니다.

자세한 내용은 관련 이슈를 참조하십시오.

이슈 또는 MR 설명과 코멘트에 오래된 캐시

마크다운 캐시 버전을 증가시키고 다른 마크다운 캐시 버전에서 생성된 설명이나 코멘트를 사용자가 편집할 때 버그가 발견되었습니다. 캐시된 HTML이 올바르게 생성되지 않았습니다. 대부분의 경우, 사용자는 편집을 선택하기 전에 마크다운을 볼 것이기 때문에 마크다운 캐시가 새로고침되는 경우가 많습니다. 그러나 우리는 혼합 버전을 실행하므로, 이것이 더 자주 발생할 수 있습니다. 다른 버전의 다른 사용자가 동일한 페이지를보고 캐시를 내부적으로 다른 버전으로 새로 고칠 수 있습니다.

자세한 내용은 관련 이슈를 참조하십시오.

프로젝트 서비스 템플릿이 잘못 복사되었습니다

서비스의 템플릿인지 여부를 나타내는 열을 변경했습니다. 서비스를 생성할 때 템플릿에서 속성을 복사하고 이 열을 false로 설정합니다. 이전 서버는 여전히 이전 열을 업데이트하지만, 우리는 이전 열에서 새 열로 업데이트하는 데이터베이스 트리거를 갖고 있기 때문에 이는 문제가 없습니다. 그러나 새 서버의 경우, 새 열만 업데이트하며 동일한 트리거가 이전 버전으로 작동하고 우리에게 역효과를 일으킵니다.

자세한 내용은 관련 이슈를 참조하십시오.

일부 사용자에게 사이드바가 로드되지 않았습니다.

우리는 하나의 GraphQL 필드의 데이터 유형을 변경했습니다. 새 서버에서 이슈 페이지를 여는 사용자가 GraphQL AJAX 요청이 이전 서버로 전달되면 유형 불일치가 발생하여 사이드바를 로드하지 못하게되는 자바스크립트 오류가 발생하였습니다.

자세한 내용은 관련 이슈를 참조하십시오.

CI artifact uploads were failing

NOT NULL 제약 조건을 추가하고 기존 행에는 적용되지 않도록 NOT VALID 제약 조건을 지정했습니다. 하지만, 이로 인해 문제가 해결되지 않았습니다. 이전 서버는 여전히 새로운 행을 null 값으로 삽입하고 있었기 때문입니다.

추가 정보는 관련 이슈를 참조하세요.

캐너리 배포와 프로덕션 배포 사이의 릴리스 기능 다운타임

문제를 해결하기 위해 기존 테이블에 NOT NULL 제약 조건이 없이 새로운 열을 추가했습니다. 다시 말해, 애플리케이션에서 해당 열에 값을 설정해야 합니다.

이전 버전의 애플리케이션은 엔티티/개념이 존재하지 않았기 때문에 NOT NULL 제약 조건을 설정하지 않았습니다.

문제는 캐너리 배포가 완료되자마자 시작됩니다. 해당 시점에서 데이터베이스 마이그레이션(열 추가)이 성공적으로 실행되었고 캐너리 인스턴스는 새로운 애플리케이션 코드를 사용하여 QA에 성공했습니다. 불행히도 프로덕션 인스턴스는 여전히 이전 코드를 사용하고 있어 새 릴리스 항목을 삽입하지 못하고 실패했습니다.

추가 정보는 릴리스 API와 관련된 이 이슈를 참조하세요.

노드 유형별 배포 시간이 다른 경우 빌드 실패

하나의 프로덕션 이슈에서 parallel 키워드를 사용하고 CI_NODE_TOTAL 변수에 의존하는 CI 빌드가 실패했습니다. 사용자가 커밋을 푸시한 후 발생한 문제입니다.

  1. 새로운 코드: Sidekiq이 새로운 파이프라인 및 새로운 빌드를 생성했습니다. build.options[:parallel]Hash입니다.
  2. 이전 코드: 러너는 이전 버전을 실행 중인 API 노드에서 작업을 요청했습니다.
  3. 그 결과, 새 코드 는 API 서버에서 실행되지 않았습니다. 러너의 요청이 실패한 이유는 이전 API 서버가 CI_NODE_TOTAL CI/CD 변수를 반환하려 했지만 정수 값 (예: 9) 대신 직렬화된 Hash 값 ({:number=>9, :total=>9})을 반환했기 때문입니다.

배포 파이프라인을 확인하면 모든 노드가 동시에 업데이트된 것을 볼 수 있습니다:

GitLab.com 배포 파이프라인

그러나, 업데이트가 거의 동시에 시작되었음에도 완료 시간이 크게 다를 수 있었습니다:

노드 유형 소요 시간(분)
API 54
Sidekiq 21
K8S 8

parallel 키워드 및 CI_NODE_TOTAL, CI_NODE_INDEX에 의존하는 빌드는 Sidekiq이 업데이트된 후 시간 동안 실패할 수 있었습니다. Kubernetes(K8S)도 Sidekiq pod을 실행하기 때문에 이 기간은 46분에서 33분까지 길 수 있었습니다. 어쨌든, 배포가 완료된 후 피처 플래그를 켜는 방법을 사용하면 이러한 문제를 방지할 수 있을 것입니다.