- 일반적인 착수사례
- 업데이트 안내
- 코드가 얼마나 오래 양방향으로 호환되어야 하는가?
- 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은 어떤 구성 요소로 나눠질 수 있을까요?
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을 철저히 테스트합니다.
- 병합하기 전에 MR을
@gitlab-org/release/managers
에 알립니다.
기능 플래그
기능 플래그는 하위 호환성 문제를 처리하는 방법으로만, 전략이 아닙니다.
예를 들어, 새로운 기능을 추가하고 프론트엔드 및 API 변경이 있는 경우, 프론트엔드 및 API 변경이 기본적으로 비활성화된다면 안전합니다. 이는 여러 개의 MR을 어떤 순서로든 병합할 수 있습니다. 변경 사항이 모두 GitLab.com에 배포된 후, 기능을 ChatOps에서 활성화하고 GitLab.com에서 유효성을 검사할 수 있습니다.
그러나 변경 사항을 기본적으로 활성화하는 것은 반드시 안전한 것은 아닙니다. 변경 사항이 삭제되거나 기본값이 활성화된 경우, 동일한 릴리스에서 고객이 제로 다운타임 업데이트를 수행하면 새로운 프론트엔드 코드가 이전 릴리스의 API에 대해 실행될 것입니다.
모든 변경 사항을 동시에 활성화하는 것이 안전한지 확신이 들지 않는다면 현재 릴리스에서 API를 활성화하고 다음 릴리스에서 프론트엔드 변경을 활성화하는 것이 한 가지 옵션입니다. 이것은 확장 및 축소 패턴의 예입니다.
또는 프론트엔드를 수정하여 이전 릴리스의 API에 대해 원활하게 퇴화할 수 있는 경우, 릴리스를 지연하지 않고 처리할 수도 있습니다.
원활한 퇴화
예를 들어, 프론트엔드 및 API 변경이 있는 새로운 기능을 추가할 때, 새 기능이 이전 API 응답에 대해 원활하게 퇴화되도록 프론트엔드를 작성할 수 있습니다. 이렇게 함으로써 변경 사항을 3개의 릴리스에 걸쳐 전파할 필요가 없을 수 있습니다.
확장 및 축소 패턴
온프레미스 인스턴스의 제로 다운타임 업데이트를 보장하는 한 가지 방법은 확장 및 축소 패턴을 따르는 것입니다.
이는 모든 호환성 파괴 변경이 3단계로 분해되는 것을 의미합니다: 확장, 이주, 축소.
- 확장: 호환성 파괴 변경사항을 소프트웨어를 하위 호환성 유지하도록 도입합니다.
- 이주: 모든 사용자가 새 구현을 사용하도록 업데이트됩니다.
- 축소: 하위 호환성이 제거됩니다.
이 3단계는 제로 다운타임 업데이트를 허용하기 위해 서로 다른 마일스톤의 일부여야만 합니다.
기능의 지원 수준에 따라, 축소 단계는 다음 주 버전 릴리스까지 연기될 수 있습니다.
확장 및 축소 예시
루트 변경, 새로운 Sidekiq worker 매개변수 추가, 데이터베이스 마이그레이션은 모두 호환성 파괴 변경의 완벽한 예입니다. 이를 안전하게 처리하는 방법을 살펴봅시다.
루트 변경
경로를 변경할 때는 새 버전에서 생성된 경로를 예전 버전에서 제공할 수 있도록 주의해야 합니다. 그렇지 않으면 보시다시피 장애로 이어질 수 있습니다. 이런 유형의 변경은 두 구현물 간의 즉시적 인 전환처럼 보일 수 있습니다. 그러나 특히 카나리아 단계와 함께 하면 생산 환경에서 새 기능의 버전과 예전 버전의 코드가 공존하는 경우가 있습니다.
- 확장: 예전 라우트를 가리키는 새로운 라우트가 추가됩니다. 그러나 애플리케이션에서 이제까지 새 라우트에 대한 링크를 생성하지는 않습니다.
- 이주: 이제 모든 서버에서 새 라우트를 이해할 수 있기 때문에 새 라우트로 링크를 생성할 수 있습니다.
- 축소: 예전 라우트를 안전하게 제거할 수 있습니다. (저장소 파일의 링크와 같이 예전 라우트가 널리 공유될 가능성이 있는 경우, 리다이렉트를 추가하고 예전 라우트를 오래 유지하는 것이 좋을 수 있습니다.)
Sidekiq worker의 매개변수 변경
이 주제에 대한 자세한 내용은 업데이트 간 Sidekiq 호환성에서 설명되어 있습니다.
새로운 매개변수를 Sidekiq worker 클래스에 추가해야 할 때, 다음 단계로 나눌 수 있습니다:
- 확장: worker 클래스에 기본값이 있는 새 매개변수가 추가됩니다.
- 이주: 모든 worker 호출에 새 매개변수를 추가합니다.
- 축소: 기본값을 제거합니다.
첫 눈에는 확장 및 이주를 하나의 단계로 묶는 것이 안전할 것으로 보일 수 있지만, Puma가 Sidekiq보다 먼저 다시 시작되면 장애가 발생합니다. 기존의 Sidekiq가 처리할 수 없는 추가 매개변수로 Puma가 작업을 대기시킵니다.
데이터베이스 마이그레이션
다음 그래프는 배포의 단순화된 시각적 표현으로, 마이그레이션 전략에서 확장 및 축소가 어떻게 구현되는지 이해하는 데 도움이 됩니다.
여기에는 특별한 고려 사항이 있습니다. 사후 배포 마이그레이션 프레임워크를 사용하면 세 단계를 한 번의 마일스톤으로 묶을 수 있습니다.
이 스키마는 데이터베이스 관점에서 볼 때 두 개의 배포가 하나의 GitLab 배포로 향하고 있음을 보여줍니다:
-
스키마 A
에서스키마 B
로 -
스키마 B
에서스키마 C
로
이러한 배포는 완벽하게 애플리케이션 변경과 일치합니다.
- 처음에는
버전 N
이스키마 A
에 있습니다. - 그런 다음
버전 N
및버전 N+1
둘이스키마 B
에 동시에 오래 있고, 이제 새 쿼리만 실행됩니다. -
스키마 B
에는버전 N+1
만 있게 되면 스키마가 다시 변경됩니다. - 마지막으로
스키마 C
에는버전 N+1
만 있게 됩니다.
이러한 모든 내용을 염두에 두면, 쿼리를 교체해야 한다고 상상해 봅시다. 이 쿼리에는 지원을 위한 인덱스가 있습니다.
-
확장:
스키마 A
에서스키마 B
로. 새 인덱스가 추가되지만 애플리케이션은 지금은 무시합니다. -
이주:
버전 N
에서버전 N+1
로의 애플리케이션 배포. 새 코드가 배포되면 이 시점에만 새 쿼리가 실행됩니다. -
축소:
스키마 B
에서스키마 C
로. 이전 인덱스를 더 이상 사용하지 않습니다. 안전하게 제거할 수 있습니다.
이것은 단지 예시일 뿐입니다. 특히 백그라운드 마이그레이션이 필요한 더 복잡한 마이그레이션의 경우 하나의 마일스톤 이상이 필요할 수 있습니다. 자세한 내용은 마이그레이션 스타일 가이드를 참조하세요.
이전 사건 예시
일부 링크로 인한 이슈와 MR(Merge Request) 오류
MR 경로를 이동한 후, 새 서버의 사용자들은 새 URL로 리디렉션 되었습니다. 이러한 사용자들이 이 새 URL을 마크다운이나 다른 곳에 공유할 경우 이전 서버의 사용자들에게는 링크가 깨지게 되었습니다.
자세한 정보는 관련 이슈를 참조하세요.
이슈 또는 MR 설명과 댓글에서 오래된 캐시
Markdown 캐시 버전을 올렸을 때, 한 사용자가 다른 Markdown 캐시 버전에서 생성된 설명이나 댓글을 편집하면 버그가 발생하는 것을 발견했습니다. 저장 후에 캐시된 HTML이 제대로 생성되지 않았습니다. 대부분의 경우, 사용자들은 편집을 선택하기 전에 Markdown을 확인하고, 이로 인해 Markdown 캐시가 새로 고쳐집니다. 그러나 혼합된 버전을 실행하기 때문에 이것이 더 자주 발생할 수 있습니다. 다른 버전의 다른 사용자가 동일한 페이지를 보고 캐시를 암묵적으로 다른 버전으로 새로 고칠 수 있습니다.
자세한 정보는 관련 이슈를 참조하세요.
프로젝트 서비스 템플릿이 잘못 복사됨
서비스가 템플릿인지 나타내는 열을 변경했습니다. 우리가 서비스를 만들 때, 우리는 템플릿에서 속성을 복사하고 이 열을 false
로 설정했습니다. 이전 서버는 여전히 이전 열을 업데이트하고 있었지만, 이는 괜찮았습니다. 왜냐하면 우리가 이전 것에서 새로운 열을 업데이트하는 DB 트리거를 가지고 있었기 때문입니다. 그러나 새 서버에서는 그들은 새로운 열만을 업데이트하고 동일한 트리거가 잘못된 값으로 설정되어 우리에겐 문제가 되었습니다.
자세한 정보는 관련 이슈를 참조하세요.
일부 사용자에게 사이드바가 로드되지 않았습니다
하나의 GraphQL 필드의 데이터 타입을 변경했습니다. 새 서버에서 사용자가 이슈 페이지를 열고 GraphQL AJAX 요청이 이전 서버로 전송되면, 타입 불일치가 발생했고, 이로 인해 사이드바가 로드되지 않는 JavaScript 오류가 발생했습니다.
자세한 정보는 관련 이슈를 참조하세요.
CI 아티팩트 업로드가 실패했습니다
한 열에 NOT NULL
제약을 추가하고 이를 기존 행에 강제 적용되지 않도록 NOT VALID
제약으로 했습니다. 그러나 여전히 문제가 발생했습니다. 왜냐하면 이전 서버는 여전히 새로운 행을 null 값으로 삽입하고 있었기 때문입니다.
자세한 정보는 관련 이슈를 참조하세요.
캐너리(Canary) 배포와 프로덕션 배포 간 릴리즈 기능에 대한 다운타임
문제를 해결하기 위해 기존 테이블에 새 열에 NOT NULL
제약을 추가했습니다. 다시 말해, 이는 응용프로그램이 열에 값을 설정해야 한다는 것입니다.
이전 버전의 응용프로그램은 그 개체/개념이 이전에 존재하지 않았기 때문에 NOT NULL
제약을 설정하지 않았습니다.
문제는 캐너리 배포가 완료된 직후에 시작됩니다. 그 순간, 데이터베이스 마이그레이션(열을 추가하기 위한)이 성공적으로 실행되었으며 캐너리 인스턴스는 새 응용프로그램 코드를 사용하기 시작하여 QA가 성공했습니다. 불행히도 프로덕션 인스턴스는 여전히 이전 코드를 사용하고 있기 때문에 새 릴리즈 항목을 삽입하지 못하게 되었습니다.
자세한 정보는 릴리즈 API와 관련된 이슈를 참조하세요.
노드 유형별로 배포 시간이 달라 CI 빌드가 실패하는 경우
한 프로덕션 이슈에서, 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분까지 짧아질 수 있었습니다. 어쨌든, 배포가 완료된 후에 기능 플래그를 켜는 것이 이 문제를 예방할 수 있을 것입니다.