- 일반적인 주의 사항
- 업데이트 절차
- 코드가 얼마나 오래 하위 호환성을 유지해야 합니까?
- GitLab은 어떤 구성 요소로 나눌 수 있습니까?
- 업데이트 단계의 순서가 중요하지 않습니까?
- 잠재적인 하위 호환성 문제를 식별했습니다. 어떻게 할 수 있습니까?
- 확장 및 축소 사례
- 이전 사건의 예
업데이트 간의 하위 호환성
GitLab 배포는 많은 구성 요소로 분해될 수 있습니다. GitLab 업데이트는 원자적이지 않습니다. 따라서 많은 구성 요소가 하위 호환성을 가져야 합니다.
일반적인 주의 사항
어떤 의미에서는 이러한 시나리오는 모두 일시적인 상태입니다. 그러나 라이브 운영 환경에서는 종종 몇 시간 동안 지속될 수 있습니다. 따라서 우리는 이들을 영구 상태와 동일한 주의로 다루어야 합니다.
Sidekiq 워커 수정 시
예를 들어, 인수를 변경할 때:
- 이전 서명이 있는 작업이 대기열에 추가되지만 새로운 월간 릴리스에 의해 실행되는 것이 괜찮습니까?
- 새로운 서명이 있는 작업이 대기열에 추가되지만 이전 월간 릴리스에 의해 실행되는 것이 괜찮습니까?
새로운 Sidekiq 워커 추가 시
Sidekiq 노드가 아직 업데이트되지 않는 경우 이러한 작업이 몇 시간 동안 실행되지 않는 것이 괜찮습니까?
JavaScript 수정 시
브라우저에 새로운 JavaScript 코드가 있지만 Rails 코드가 이전 월간 릴리스를 실행하고 있는 경우:
- REST API에서 괜찮습니까?
- GraphQL API에서 괜찮습니까?
- 컨트롤러의 내부 API에서 괜찮습니까?
배포 전 마이그레이션 추가 시
배포 전 마이그레이션이 실행되었지만 웹, Sidekiq 및 API 노드가 이전 릴리스를 실행하고 있는 경우 괜찮습니까?
배포 후 마이그레이션 추가 시
모든 GitLab 노드가 업데이트되었지만 배포 후 마이그레이션이 며칠 후에 실행되는 것이 괜찮습니까?
백그라운드 마이그레이션 추가 시
모든 노드가 업데이트되었고, 이후에 배포 후 마이그레이션이 며칠 후에 실행되며 백그라운드 마이그레이션이 완료되는 데 일주일이 걸리는 것이 괜찮습니까?
Rails와 같은 종속성 업그레이드 시
일부 노드가 새로운 Rails 버전을 가지고 있지만 일부 노드는 이전 Rails 버전을 가지고 있는 것이 괜찮습니까?
업데이트 절차
업데이트 중 하위 호환성 문제가 종종 매우 미묘합니다. 따라서 다음 내용을 숙지할 가치가 있습니다:
이러한 문제가 어떻게 발생하는지 설명하기 위해 이 예제를 살펴보십시오:
- 🚢 새로운 버전
- 🙂 이전 버전
이 예에서 우리는 한 월간 릴리스를 업데이트하고 있다고 상상할 수 있습니다. 그러나 코드가 얼마나 오랫동안 하위 호환성을 가져야 하는가?를 참조하십시오.
업데이트 단계 | PostgreSQL DB | 웹 노드 | API 노드 | Sidekiq 노드 | 호환성 문제 |
---|---|---|---|---|---|
초기 상태 | 🙂 | 🙂 | 🙂 | 🙂 | |
배포 전 마이그레이션 실행 | 🚢 배포 후 마이그레이션 제외 | 🙂 | 🙂 | 🙂 | 🙂에서의 Rails 코드가 🚢에 대한 DB 호출을 하고 있음 |
웹 노드 업데이트 | 🚢 배포 후 마이그레이션 제외 | 🚢 | 🙂 | 🙂 | 🚢의 JavaScript가 🙂에 API 호출을 하고 있음. 🚢의 Rails 코드가 🙂에서 실행되는 작업을 대기열에 추가하고 있음 |
API 및 Sidekiq 노드 업데이트 | 🚢 배포 후 마이그레이션 제외 | 🚢 | 🚢 | 🚢 | 🚢의 Rails 코드가 배포 후 마이그레이션 또는 백그라운드 마이그레이션 없이 DB 호출을 하고 있음 |
배포 후 마이그레이션 실행 | 🚢 | 🚢 | 🚢 | 🚢 | 🚢의 Rails 코드가 백그라운드 마이그레이션 없이 DB 호출을 하고 있음 |
백그라운드 마이그레이션 완료 | 🚢 | 🚢 | 🚢 | 🚢 |
이 예는 포괄적이지 않습니다. GitLab은 매우 다양한 방식으로 배포될 수 있습니다. 각 업데이트 단계도 원자적이지 않습니다. 예를 들어, 롤링 배포의 경우 그룹 내의 노드는 일시적으로 서로 다른 버전을 사용할 수 있습니다. 업데이트 단계 간에 많은 시간이 경과한다고 가정해야 합니다. 이는 종종 GitLab.com에서도 일어납니다.
코드가 얼마나 오래 하위 호환성을 유지해야 합니까?
제로 다운타임 업데이트 지침을 따르는 사용자에게는 대답이 월간 릴리스 한 번입니다. 예를 들어:
- 13.11 => 13.12
- 13.12 => 14.0
- 14.0 => 14.1
GitLab.com의 경우, 하루에 여러 개의 작은 버전 업데이트가 있을 수 있으므로 GitLab.com은 변경 사항이 얼마나 멀리 하위 호환성을 유지해야 하는지에 대해 제약을 두지 않습니다.
많은 사용자가 일부 월간 릴리스를 건너뛰며, 예를 들어:
- 13.0 => 13.12
이러한 사용자는 업데이트 중 일부 다운타임을 수용합니다. 불행히도 우리는 이 경우를 완전히 무시할 수 없습니다. 예를 들어, 13.12는 13.0의 Sidekiq 작업을 실행할 수 있으며, 이는 주요 릴리스까지 작업에서 인수를 제거하는 것을 피하는 이유를 설명합니다. 주요 질문은 업데이트가 완료된 후 배포가 좋은 상태에 도달할 것인가입니다?
GitLab은 어떤 구성 요소로 나눌 수 있습니까?
1000 RPS 또는 50,000 사용자 참조 아키텍처는 GitLab을 48개 이상의 노드에서 실행합니다. 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을 철저히 테스트합니다.
- 병합 전에 MR의
@gitlab-org/release/managers
에게 알립니다.
기능 플래그
기능 플래그는 하위 호환성 문제를 처리하기 위한 도구이지 전략이 아닙니다.
예를 들어, 프론트엔드 및 API 변경이 모두 기본적으로 비활성화되어 있다면 프론트엔드 및 API 변경이 포함된 새로운 기능을 추가하는 것은 안전합니다. 이는 여러 개의 병합 요청으로 진행할 수 있으며, 순서는 상관 없습니다. 모든 변경 사항이 GitLab.com에 배포된 후, 기능은 ChatOps에서 활성화되고 GitLab.com에서 검증할 수 있습니다.
하지만 기본적으로 기능을 활성화하는 것은 반드시 안전하지 않습니다. 기능 플래그가 제거되거나 기본값이 활성화로 전환되는 경우, 코드가 병합된 동일한 릴리스에서 제로 다운타임 업데이트를 수행하는 고객은 이전 릴리스의 API와 새로운 프론트엔드 코드를 실행하게 됩니다.
모든 변경 사항을 한 번에 활성화하는 것이 안전한지 확신이 서지 않는다면, 한 가지 옵션은 현재 릴리스에서 API를 활성화하고 다음 릴리스에서 프론트엔드 변경을 활성화하는 것입니다. 이는 확장 및 축소 패턴의 예입니다.
또는 프론트엔드를 수정하여 이전 릴리스의 API에 대해 우아하게 저하되도록 하여 릴리스를 지연시키지 않을 수 있습니다.
우아한 쇠퇴
예를 들어, 프론트엔드 및 API 변경 사항이 포함된 새로운 기능을 추가할 때, 새로운 기능이 이전 API 응답에 대해 우아하게 쇠퇴하도록 프론트엔드를 작성하는 것이 가능할 수 있습니다. 이는 변경 사항을 3개의 릴리스에 걸쳐 퍼트릴 필요를 피하는 데 도움이 될 수 있습니다.
확장 및 축소 패턴
온프레미스 인스턴스에 대한 제로 다운타임 업데이트를 보장하는 한 가지 방법은 확장 및 축소 패턴을 따르는 것입니다.
이는 모든 파괴적인 변경 사항이 세 단계로 나뉘어 이루어짐을 의미합니다: 확장, 마이그레이션, 축소.
- 확장: 소프트웨어가 이전 호환성을 유지하면서 파괴적인 변경 사항이 도입됩니다.
- 마이그레이션: 모든 소비자가 새로운 구현을 사용하도록 업데이트됩니다.
- 축소: 이전 호환성이 제거됩니다.
이 세 단계는 서로 다른 마일스톤의 일부여야 하며, 제로 다운타임 업데이트를 허용합니다.
기능에 대한 지원 수준에 따라 축소 단계는 다음 주요 릴리스까지 연기될 수 있습니다.
확장 및 축소 사례
라우트 변경, Sidekiq 작업자 매개변수 변경, 데이터베이스 마이그레이션은 모두 파괴적인 변경 사항의 완벽한 예입니다. 안전하게 처리하는 방법을 살펴보겠습니다.
라우트 변경
라우트를 변경할 때, 새로운 버전에서 생성된 라우트가 이전 버전에서도 제공될 수 있도록 주의를 기울여야 합니다. 보시다시피, 이를 수행하지 않으면 중단이 발생할 수 있습니다. 이러한 유형의 변경은 두 구현 간의 즉각적인 전환처럼 보일 수 있습니다. 그러나 특히 카나리 단계에서는 코드의 두 버전이 프로덕션에서 공존하는 기간이 길어집니다.
- 확장: 새로운 라우트가 추가되며, 이전 라우트와 동일한 컨트롤러를 가리킵니다. 그러나 애플리케이션에서 새로운 라우트에 대한 링크는 생성되지 않습니다.
- 마이그레이션: 이제 모든 머신이 새로운 라우트를 이해할 수 있으므로 새로운 라우팅으로 링크를 생성할 수 있습니다.
- 축소: 이전 라우트를 안전하게 제거할 수 있습니다. (이전 라우트가 저장소 파일 링크와 같이 널리 공유될 가능성이 있다면, 리다이렉트를 추가하고 이전 라우트를 더 오랫동안 유지하는 것을 원할 수 있습니다.)
Sidekiq 작업자 매개변수 변경
이 주제는 Sidekiq 업데이트 간 호환성에서 자세히 설명됩니다.
Sidekiq 작업자 클래스에 새로운 매개변수를 추가해야 할 때, 이를 다음 단계로 나눌 수 있습니다:
- 확장: 작업자 클래스가 기본 값으로 새 매개변수를 추가합니다.
- 마이그레이션: 작업자의 모든 호출에 새 매개변수를 추가합니다.
- 축소: 기본 값을 제거합니다.
처음에는 확장과 마이그레이션을 단일 마일스톤으로 묶는 것이 안전해 보일 수 있지만, 이는 Puma가 Sidekiq보다 먼저 다시 시작되면 중단이 발생합니다. Puma는 이전 Sidekiq가 처리할 수 없는 추가 매개변수로 작업을 큐에 추가합니다.
데이터베이스 마이그레이션
다음 그래프는 배포의 단순화된 시각적 표현으로, 확장 및 축소가 마이그레이션 전략에서 어떻게 구현되는지를 이해하는 데 도움이 됩니다.
여기에서 특별한 고려 사항이 있습니다. 우리의 배포 후 마이그레이션 프레임워크를 사용하면 세 단계를 하나의 마일스톤으로 묶을 수 있습니다.
이 스키마를 데이터베이스 관점에서 살펴보면 두 개의 배포가 단일 GitLab 배포로 피드를 제공하는 것을 볼 수 있습니다:
-
스키마 A
에서스키마 B
로 -
스키마 B
에서스키마 C
로
그리고 이러한 배포는 애플리케이션 변경 사항과 완벽하게 일치합니다.
- 처음에
스키마 A
의버전 N
이 있습니다. - 그런 다음
스키마 B
에서버전 N
과버전 N+1
이 모두 있는 긴 전환 기간이 있습니다. -
스키마 B
에서버전 N+1
만 있을 때 스키마가 다시 변경됩니다. - 마지막으로
스키마 C
에서버전 N+1
이 있게 됩니다.
이 모든 세부 사항을 염두에 두고, 쿼리를 교체해야 하고 이 쿼리가 이를 지원하는 인덱스를 갖고 있다고 가정해 보겠습니다.
-
확장: 이것은
스키마 A
에서스키마 B
로의 배포입니다. 우리는 새로운 인덱스를 추가하지만, 애플리케이션은 지금 당장 이를 무시합니다. -
마이그레이션: 이것은
버전 N
에서버전 N+1
으로의 애플리케이션 배포입니다. 새로운 코드가 배포되며, 이 시점에서 새로운 쿼리만 실행됩니다. -
축소:
스키마 B
에서스키마 C
로의 (배포 후 마이그레이션). 더 이상 이전 인덱스를 사용하는 것이 없으므로 안전하게 제거할 수 있습니다.
이는 단지 예일 뿐입니다. 특히 백그라운드 마이그레이션이 필요한 경우 더 복잡한 마이그레이션이 하나 이상의 마일스톤을 요구할 수 있습니다. 자세한 내용은 마이그레이션 스타일 가이드를 참조하십시오.
이전 사건의 예
일부 문제 및 MR 링크가 깨졌습니다
MR 경로를 이동했을 때, 새로운 서버의 사용자들은 새로운 URL로 리디렉션되었습니다. 이 사용자가 새로운 URL을 Markdown(또는 다른 곳)에서 공유했을 때, 이전 서버의 사용자에게는 깨진 링크가 되었습니다.
자세한 정보는 관련 문제를 참조하세요.
문제 또는 병합 요청 설명 및 댓글에 오래된 캐시가 있습니다
Markdown 캐시 버전을 업데이트했으며, 사용자가 다른 Markdown 캐시 버전에서 생성된 설명이나 댓글을 수정할 때 버그를 발견했습니다. 저장한 후 캐시된 HTML이 제대로 생성되지 않았습니다. 대부분의 경우, 사용자가 Edit을 선택하기 전에 Markdown을 보았기 때문에 이러한 일이 발생하지 않았습니다. 이로 인해 Markdown 캐시가 새로 고쳐졌습니다. 하지만 혼합된 버전을 실행하기 때문에 이러한 일이 발생할 가능성이 더 높습니다. 다른 버전의 다른 사용자가 동일한 페이지를 보고 캐시를 백그라운드에서 다른 버전으로 새로 고칠 수 있었습니다.
자세한 정보는 관련 문제를 참조하세요.
프로젝트 서비스 템플릿이 잘못 복사되었습니다
서비스가 템플릿인지 여부를 나타내는 열을 변경했습니다. 서비스를 생성할 때 템플릿에서 속성을 복사하고 이 열을 false
로 설정합니다. 이전 서버는 여전히 이전 열을 업데이트하고 있었지만, DB 트리거가 새로운 열을 이전 열에서 업데이트하기 때문에 괜찮았습니다. 그러나 새로운 서버는 새로운 열만 업데이트하고 있었고, 같은 트리거가 이제 우리에게 불리하게 작용하여 잘못된 값으로 되돌렸습니다.
자세한 정보는 관련 문제를 참조하세요.
일부 사용자에게 사이드바가 로드되지 않았습니다
하나의 GraphQL 필드의 데이터 유형을 변경했습니다. 사용자가 새로운 서버에서 문제 페이지를 열고 GraphQL AJAX 요청이 이전 서버로 가면, 유형 불일치가 발생하여 JavaScript 오류를 일으켜 사이드바가 로드되지 않았습니다.
자세한 정보는 관련 문제를 참조하세요.
CI 아티팩트 업로드가 실패했습니다
열에 NOT NULL
제약 조건을 추가하고 이를 NOT VALID
제약 조건으로 표시하여 기존 행에 대해 적용되지 않도록 했습니다. 그러나 그럼에도 불구하고 이전 서버는 여전히 null 값으로 새 행을 삽입하고 있었습니다.
자세한 정보는 관련 문제를 참조하세요.
카나리 및 생산 배포 간의 릴리스 기능 다운타임
문제를 해결하기 위해, 기본값을 지정하지 않고 NOT NULL
제약 조건이 있는 기존 테이블에 새 열을 추가했습니다. 다시 말해, 이는 애플리케이션이 열에 값을 설정해야 한다는 것을 의미합니다.
애플리케이션의 이전 버전에서는 엔티티/개념이 이전에 존재하지 않았기 때문에 NOT NULL
제약 조건을 설정하지 않았습니다.
문제는 카나리 배포가 완료된 직후에 시작됩니다. 그 순간, 데이터베이스 마이그레이션(열 추가)이 성공적으로 실행되었고, 카나리 인스턴스가 새로운 애플리케이션 코드를 사용하기 시작했으므로 QA는 성공적이었습니다. 불행히도, 생산 인스턴스는 이전 코드를 여전히 사용하고 있었기 때문에 새 릴리스 항목을 삽입하는 데 실패하기 시작했습니다.
자세한 정보는 릴리스 API와 관련된 이 문제를 참조하세요.
노드 유형 간의 배포 시간 차이로 인한 빌드 실패
한 가지 프로덕션 문제에서,
parallel
키워드를 사용하고
정수인 CI_NODE_TOTAL
변수에 의존하는 CI 빌드가 실패했습니다. 이는 사용자가 커밋을 푸시한 후 발생했습니다:
- 새 코드: Sidekiq는 새로운 파이프라인과 새로운 빌드를 생성했습니다.
build.options[:parallel]
는Hash
입니다. - 이전 코드: 러너는 이전 버전을 실행 중인 API 노드에서 작업을 요청했습니다.
- 결과적으로 새 코드가 API 서버에서 실행되지 않았습니다. 러너의 요청은 실패했으며, 이전 API 서버는
CI_NODE_TOTAL
CI/CD 변수를 반환하려 했지만, 정수 값(예: 9) 대신 직렬화된Hash
값({:number=>9, :total=>9}
)을 전달했습니다.
배포 파이프라인을 살펴보면,
모든 노드가 병렬로 업데이트된 것을 확인할 수 있습니다:
그러나 업데이트가 같은 시점에 시작되었음에도 불구하고, 완료 시간은 상당히 차이가 있었습니다:
노드 유형 | 소요 시간 (분) |
---|---|
API | 54 |
Sidekiq | 21 |
K8S | 8 |
parallel
키워드를 사용하고 CI_NODE_TOTAL
및 CI_NODE_INDEX
에 의존하는 빌드는 Sidekiq가 업데이트된 후에 실패하게 됩니다. Kubernetes(K8S)도 Sidekiq 포드를 실행하기 때문에, 이 시간은 최대 46분 또는 최소 33분이 될 수 있습니다. 어쨌든, 배포가 완료된 후에 켤 수 있는 기능 플래그를 추가하는 것은 이러한 상황을 방지할 것입니다.