- 일반적인 함정
- 업데이트의 안내
- 코드는 얼마나 오랫동안 역호환되어야 하는가?
- 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 코드는 🙂의 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에서 실행됩니다, 또한 “캐너리” 단계가 먼저 업데이트를 받는 곳이 있습니다.
그러나 문제는 단순히 많은 노드를 가지고 있는 것뿐만이 아닙니다. 더 큰 문제는 배포를 다른 컨텍스트로 나눌 수 있다는 것입니다. 그리고 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 변경이 있는 경우, 프론트엔드 및 API 변경 모두가 기본적으로 비활성화되어 있으면 안전합니다. 여러 MR을 어떤 순서로 Merge해도 괜찮습니다. 변경 사항이 모두 GitLab.com에 배포된 후, 기능을 ChatOps에서 활성화하고 GitLab.com에서 유효성을 검사할 수 있습니다.
그러나 변경 사항을 기본적으로 활성화하는 것은 항상 안전한 것은 아닙니다. 변경 사항이 기본값에서 제거되거나 기본값이 변경되면 동일한 릴리스에서 코드가 Merge된 곳에서 다운타임 없는 업데이트를 수행하는 고객이 새로운 프론트엔드 코드를 이전 릴리스의 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’ 둘 다 있을 때는 새로운 쿼리만 실행됩니다.
- 마지막으로 ‘버전 N+1’이 ‘스키마 C’에 있는 경우입니다.
모든 이러한 세부 사항을 기억하여, 쿼리를 교체해야 할 경우를 상상해 봅시다. 그리고 이 쿼리에는 이를 지원하는 인덱스가 있습니다.
- 확장: 이는 ‘스키마 A’에서 ‘스키마 B’로의 배포입니다. 새 인덱스가 추가되지만 현재 애플리케이션에서는 무시합니다.
- 이동: 이는 ‘버전 N’에서 ‘버전 N+1’로의 애플리케이션 배포입니다. 새 코드가 배포되었으며, 해당 시점에서 새 쿼리만 실행됩니다.
- 축소: ‘스키마 B’에서 ‘스키마 C’로 (후반 마이그레이션)입니다. 더 이상 예전 인덱스를 사용하지 않으므로 안전하게 제거할 수 있습니다.
이것은 단순한 예일 뿐입니다. 특히 백그라운드 마이그레이션이 필요할 때와 같이 더 복잡한 마이그레이션은 여러 마일스톤 이상이 필요할 수도 있습니다. 자세한 내용은 마이그레이션 스타일 가이드를 참조하십시오.
이전 사건의 예시
몇몇 이슈와 MR 링크가 깨졌습니다
우리가 MR 라우트를 이동했을 때, 새 서버의 사용자들이 새 URL로 리디렉션 되었습니다. 이 사용자들이 이 새 URL을 Markdown(또는 다른 곳)에서 공유했을 때, 이는 이전 서버의 사용자들에게는 깨진 링크가 되었습니다.
더 많은 정보가 필요하다면, 관련 이슈를 참조하세요.
이슈나 Merge Request 설명과 코멘트에서 구식 캐시
Markdown 캐시 버전을 올렸을 때, 사용자가 다른 Markdown 캐시 버전에서 만들어진 설명이나 코멘트를 편집할 때 버그를 발견했습니다. 캐시된 HTML이 제대로 생성되지 않았습니다. 대부분의 경우에는 사용자들이 편집을 선택하기 전에 Markdown을 본 후에 일어날 일이 아니었을 것입니다. 하지만 우리가 혼합된 버전을 실행했기 때문에, 이러한 경우가 발생할 가능성이 높아졌습니다. 다른 버전의 다른 사용자가 동일한 페이지를 보고 캐시를 뒤에서 다른 버전으로 새로 고칠 수 있게되는 것입니다.
더 많은 정보가 필요하다면, 관련 이슈를 참조하세요.
프로젝트 서비스 템플릿이 잘못 복사되었습니다
서비스가 템플릿인지를 나타내는 열을 변경했습니다. 서비스를 생성할 때, 우리는 템플릿에서 속성을 복사하고 이 열을 false
로 설정했습니다. 이전 서버들은 여전히 이전 열을 업데이트했지만, 우리가 이전 열로부터 새 열을 업데이트하는 DB 트리거가 있었기 때문에 이는 문제가 되지 않았습니다. 하지만 새 서버들은 새 열만을 업데이트했고, 동일한 트리거가 이제 우리에게 반대로 작동하여 잘못된 값으로 설정되었습니다.
더 많은 정보가 필요하다면, 관련 이슈를 참조하세요.
측면 표시줄이 일부 사용자에게 로드되지 않았습니다
우리는 한 개의 GraphQL 필드의 데이터 타입을 변경했습니다. 사용자가 새 서버에서 이슈 페이지를 열었을 때, GraphQL AJAX 요청이 이전 서버로 전송되었고, 데이터 타입 불일치가 발생했습니다. 이로써 측면 표시줄이 로드되지 않는 JavaScript 오류가 발생했습니다.
더 많은 정보가 필요하다면, 관련 이슈를 참조하세요.
CI 아티팩트 업로드가 실패했습니다
우리는 열에 NOT NULL
제약을 추가하고 기존 행에는 강제되지 않도록 NOT VALID
제약을 추가했습니다. 하지만 이에도 불구하고, 기존 서버들은 여전히 null 값을 가진 새 행을 삽입하고 있었기 때문에 이는 여전히 문제가 되었습니다.
더 많은 정보가 필요하다면, 관련 이슈를 참조하세요.
캐너리 배포와 프로덕션 배포 간의 기능 다운 타임
문제를 해결하기 위해, 기존 테이블에 NOT NULL
제약을 가진 새 열을 추가했습니다. 다시 말해서, 이는 애플리케이션에게 해당 열에 값을 설정하도록 요구합니다.
이전 버전의 애플리케이션은 엔티티/개념이 이전에 존재하지 않았기 때문에 NOT NULL
제약을 설정하지 않았습니다.
문제는 캐너리 배포가 완료된 직후에 시작되었습니다. 그 순간에, 열을 추가하는 데이터베이스 마이그레이션이 성공적으로 실행되었고 캐너리 인스턴스는 새 애플리케이션 코드를 사용하기 시작했기 때문에 QA(Quality Assurance)는 성공적이었습니다. 불행히도, 프로덕션 인스턴스는 여전히 이전 코드를 사용하여 새로운 릴리스 항목을 삽입하는 데 실패했습니다.
더 많은 정보가 필요하다면, 릴리스 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분까지 길 수 있었습니다. 어쨌든, 배포가 완료된 후에 피처 플래그를 켜는 것은 이러한 문제를 방지할 수 있을 것입니다.