업데이트 간의 역호환성

GitLab 배포는 여러 구성 요소로 분해될 수 있습니다. GitLab을 업데이트하는 것은 원자적이지 않습니다. 따라서 많은 구성 요소가 역방향으로 호환되어야 합니다.

흔한 함정

어떤 면에서 이러한 시나리오들은 모두 일시적인 상태입니다. 그러나 종종 실사 운영 환경에서 여러 시간 동안 계속될 수 있습니다. 따라서 우리는 그것들을 영구적인 상태와 같은 주의를 기울여 다뤄야 합니다.

Sidekiq 워커를 수정할 때

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

  • 이전 서명으로 작업이 대기열에 추가되지만 새 월간 릴리스에서 실행될 때 괜찮은가요?
  • 새 서명으로 작업이 대기열에 추가되지만 이전 월간 릴리스에서 실행될 때 괜찮은가요?

새로운 Sidekiq 워커를 추가할 때

아직 Sidekiq 노드가 업데이트되지 않았기 때문에 이러한 작업이 여러 시간 동안 실행되지 않는 것이 괜찮은가요(새로운 워커 추가)?

JavaScript를 수정할 때

브라우저가 새 JavaScript 코드를 가지고 있지만 Rails 코드가 이전 월간 릴리스에서 실행 중인 경우에 괜찮은가요:

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

배포 전 마이그레이션을 추가할 때

배포 전 마이그레이션이 실행되었지만 웹, Sidekiq 및 API 노드가 이전 릴리스에서 실행 중인 경우에 괜찮은가요?

배포 후 마이그레이션을 추가할 때

모든 GitLab 노드가 업데이트되었지만, 몇 일 후에야 배포 후 마이그레이션이 실행되는 것이 괜찮은가요?

백그라운드 마이그레이션을 추가할 때

모든 노드가 업데이트되었지만, 배포 후 마이그레이션이 몇 일 후에 실행되고, 백그라운드 마이그레이션이 1주일이 걸리는 것이 괜찮은가요?

Rails와 같은 의존성을 업그레이드할 때

일부 노드가 새로운 Rails 버전을 사용해도 되는 것이 괜찮은가요, 하지만 일부 노드는 이전 Rails 버전을 사용하는 것이 괜찮은가요?

업데이트 안내

업데이트하는 동안의 역학적 호환성 문제는 종종 매우 미묘합니다. 이것이 유용한 이유입니다 자신을 익히는 것입니다:

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

  • 🚢 새 버전
  • 🙂 이전 버전

이 예에서 한 달에 한 번의 릴리스로 업데이트하고 있다고 상상할 수 있습니다. 하지만 코드가 얼마나 오랫동안 역방향으로 호환될 지을 참조하세요.

업데이트 단계 PostgreSQL DB 웹 노드 API 노드 Sidekiq 노드 호환성 관련 고려 사항
초기 상태 🙂 🙂 🙂 🙂  
배포 전 마이그레이션 실행 🚢 배포 후 마이그레이션이 제외됨 🙂 🙂 🙂 🙂의 Rails 코드가 🚢에 대한 DB 호출을 실행
웹 노드 업데이트 🚢 배포 후 마이그레이션이 제외됨 🚢 🙂 🙂 🚢의 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은 어떤 구성 요소로 분해될 수 있을까요?

50,000 참조 아키텍처에서는 48개 이상의 노드에서 GitLab을 실행합니다. GitLab.com은 그보다 더 큽니다, 그리고 인프라의 일부는 Kubernetes에서 실행됩니다, 또한 먼저 업데이트를 받는 “canary” 단계가 있습니다.

하지만 문제는 단순히 많은 노드가 있다는 것만이 아닙니다. 더 큰 문제는 배포를 서로 다른 문맥으로 나눌 수 있다는 것입니다. 그리고 GitLab.com뿐만 아니라 많은 다른 서비스들도 이를 수행합니다. 일부 가능한 분할은 다음과 같습니다:

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

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

업데이트 단계의 순서는 중요하지 않나요?

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

잠재적인 하위 호환성 문제를 확인했을 때 해결할 수 있는 방법은 무엇인가요?

협력

Rails나 Puma의 주요 또는 부정 버전 업데이트의 경우:

  • Quality 팀을 참여시켜 MR을 철저히 테스트합니다.
  • 병합하기 전에 MR에서 @gitlab-org/release/managers에 알림을 보냅니다.

기능 플래그

기능 플래그는 하위 호환성 문제를 처리하는 도구이며, 전략이 아닙니다.

예를 들어, 새로운 기능을 추가하고 frontend 및 API 변경이 모두 기본 설정에서 비활성화되어 있는 경우 안전합니다. 여러 MR을 순서와 관계없이 병합할 수 있습니다. 모든 변경 사항이 GitLab.com에 배포된 후에 챗옵스에서 기능을 활성화하고 GitLab.com에서 유효성을 검사할 수 있습니다.

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

모든 변경 사항을 동시에 활성화해도 안전한지 확실하지 않다면, 현재 릴리스에서 API를 활성화하고 다음 릴리스에서 frontend 변경을 활성화할 수 있습니다. 이것은 확장 및 축소 패턴의 예입니다.

또는 frontend를 수정하여 이전 릴리스의 API에 대해 우아하게 퇴보할 수 있는지 여부를 고려해 지연할 수도 있습니다.

우아한 퇴보

예를 들어, frontend 및 API 변경으로 새로운 기능을 추가할 때, frontend를 작성하여 이전 API 응답에 대해 새로운 기능이 우아하게 퇴보될 수 있습니다. 이렇게 함으로써 변경 사항을 3개의 릴리스로 분산하는 필요성을 피할 수 있습니다.

확장 및 축소 패턴

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

이것은 모든 파괴적인 변경이 확장, 이주, 그리고 축소 세 단계로 분할되어야 함을 의미합니다.

  1. 확장: 소프트웨어의 하위 호환성을 유지하면서 파괴적인 변경을 도입합니다.
  2. 이주: 모든 이용자가 새 구현을 사용하도록 업데이트됩니다.
  3. 축소: 하위 호환성이 제거됩니다.

이러한 세 단계는 서로 다른 마일스톤의 일부여야만 합니다. 이로써 다운타임 없는 업데이트가 가능해집니다. 해당 기능의 지원 수준에 따라, 축소 단계는 다음 주요 릴리스까지 지연될 수 있습니다.

확장 및 축소 예시

경로 변경, Sidekiq worker 매개변수 변경, 그리고 데이터베이스 마이그레이션은 모두 파괴적인 변경의 완벽한 예입니다. 어떻게 안전하게 처리할 수 있는지 살펴봅시다.

경로 변경

경로를 변경할 때, 새 버전에서 생성된 경로가 이전 버전에서 제공될 수 있도록 주의해야 합니다. 이를 하지 않으면 여기서 볼 수 있듯이, 장애가 발생할 수 있습니다. 특히 canary 스테이지와 함께, 프로덕션에서 코드의 두 가지 버전이 동시에 존재하는 확장된 기간이 있습니다.

  1. 확장: 이전 라우트를 가리키며, 새 라우트가 추가됩니다. 그러나 응용 프로그램에서 새 경로에 대한 링크를 생성하는 것은 없습니다.
  2. 이주: 이제 새 라우트를 이해하는 모든 머신이 있으므로, 새 라우트로 링크를 생성할 수 있습니다.
  3. 축소: 이전 라우트를 안전하게 제거할 수 있습니다. (예를 들어, 이전 라우트가 저장소 파일에 대한 링크와 같이 널리 공유될 것으로 예상된다면, 리디렉션을 추가하고 오래 동안 이전 라우트를 유지할 수 있습니다.)

Sidekiq worker의 매개변수 수정

이 주제에 대한 자세한 내용은 업데이트별 Sidekiq 호환성에서 설명합니다.

Sidekiq worker 클래스에 새 매개변수를 추가해야 할 때 다음 단계로 나눌 수 있습니다.

  1. 확장(Expand): worker 클래스가 기본값을 갖는 새 매개변수를 추가합니다.
  2. 이주(Migrate): 우리는 worker의 모든 호출에 새 매개변수를 추가합니다.
  3. 축약(Contract): 우리는 기본값을 제거합니다.

첫 번째로 보았을 때, 확장과 이주를 하나의 이정만에 묶어도 안전해 보일 수 있지만, 이는 Puma가 Sidekiq보다 먼저 재시작될 경우 중단을 유발합니다. Puma는 이전의 Sidekiq가 처리할 수 없는 추가적인 매개변수로 작업을 큐에 넣습니다.

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

다음 그래프는 배포의 간단한 시각적 표현입니다. 이 그래프는 확장과 축약이 우리의 마이그레이션 전략에서 어떻게 구현되는지 이해하는 데 도움이 됩니다.

특별한 고려 사항이 있습니다. 배포 후 마이그레이션 프레임워크를 사용하면 이 세 단계를 하나의 이정만에 묶을 수 있습니다.

gantt title 배포 dateFormat HH:mm section 배포 단계 box 마이그레이션 실행 :done, migr, schemaA 이후, 2분 배포 후 마이그레이션 실행 :postmigr, mcvn 이후, 2분 section 데이터베이스 스키마 A :done, schemaA, 00:00 , 1시간 스키마 B :crit, schemaB, migr 이후, 58분 스키마 C. : schemaC, postmigr 이후, 1시간 section 서버 A 버전 N :done, mavn, 00:00 , 75분 버전 N+1 : mavn 이후, 105분 section 서버 B 버전 N :done, mbvn, 00:00 , 105분 버전 N+1 : mbdone, mbvn 이후, 75분 section 서버 C 버전 N :done, mcvn, 00:00 , 2시간 버전 N+1 : mbcdone, mcvn 이후, 1시간

이 스키마를 데이터베이스 관점에서 살펴보면 두 개의 배포가 하나의 GitLab 배포로 흐르는 것을 볼 수 있습니다.

  1. 스키마 A에서 스키마 B
  2. 스키마 B에서 스키마 C

이런 배포는 애플리케이션 변경과 완벽하게 일치합니다.

  1. 처음에는 스키마 A에서 버전 N이 있습니다.
  2. 그런 다음, 스키마 B에는 버전 N버전 N+1이 함께 오래동안 존재합니다.
  3. 스키마 B에는 버전 N+1만 존재할 때 스키마가 다시 변경됩니다.
  4. 마지막으로 스키마 C에는 버전 N+1이 있습니다.

모든 이러한 세부 정보를 염두에 두고, 우리가 쿼리를 교체해야 한다고 상상해 봅시다. 이 쿼리에는 지원하는 인덱스가 있습니다.

  1. 확장: 이것은 스키마 A에서 스키마 B로의 배포입니다. 우리는 새 인덱스를 추가하지만 애플리케이션은 지금은 이를 무시합니다.
  2. 이주: 이것은 버전 N에서 버전 N+1로의 애플리케이션 배포입니다. 새 코드가 배포되었고, 이 시점에서는 새 쿼리만 실행됩니다.
  3. 축약: 스키마 B에서 스키마 C로 (배포 후 마이그레이션) 아무것도 이전 인덱스를 사용하지 않습니다. 우리는 안전하게 이를 제거할 수 있습니다.

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

이전 사건의 예시

문제 및 MR 링크가 깨짐

MR 경로를 변경하면 새 서버에서 사용자는 새 URL로 리디렉션됩니다. 이 사용자들이 새 URL을 마크다운이나 다른 곳에 공유하면 이는 이전 서버의 사용자에게는 깨진 링크가 됩니다.

자세한 정보는 해당 이슈를 참조하십시오.

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

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

자세한 정보는 해당 이슈를 참조하십시오.

프로젝트 서비스 템플릿이 잘못 복사됨

서비스가 템플릿인지 여부를 나타내는 열을 변경했습니다. 서비스를 생성할 때, 템플릿에서 속성을 복사하고이 열을 false로 설정했습니다. 이전 서버는 여전히 이전 열을 업데이트하지만, DB 트리거가 있기 때문에 괜찮았습니다. 그러나 새 서버들은 새 열만 업데이트했고, 동일한 트리거가 우리에게 반대로 작용하여 잘못된 값을 설정했습니다.

자세한 정보는 해당 이슈를 참조하십시오.

사용자 중 일부의 경우 사이드바가 로드되지 않았습니다

하나의 GraphQL 필드의 데이터 유형을 변경했습니다. 새 서버에서 이슈 페이지를 열고 GraphQL AJAX 요청이 이전 서버로 전송되면 유형 불일치가 발생하여 사이드바가 로드되지 않는 JavaScript 오류가 발생했습니다.

자세한 정보는 관련 이슈를 참조하세요.

CI 아티팩트 업로드가 실패했습니다

열에 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_TOTALCI_NODE_INDEX에 의존하는 빌드는 Sidekiq 업데이트 후에 실패했습니다. Kubernetes (K8S)도 Sidekiq 팟을 실행하기 때문에, 이 기간은 46분에서 33분까지 길 수 있었습니다. 어쨌든, 배포가 완료된 후에 켜지는 기능 플래그를 가지고 있다면 이를 방지할 수 있습니다.