새로운 Redis 인스턴스 추가

GitLab은 여러 Redis 인스턴스를 사용할 수 있습니다. 이러한 인스턴스는 기능적으로 분할되어 있어 예를 들어 하나의 Redis 인스턴스에서 CI 추적 청크를 저장하고 다른 인스턴스에서 세션을 저장할 수 있습니다.

가끔 새로운 Redis 인스턴스를 추가하고 싶을 때가 있습니다. 일반적으로 기존 캐시나 공유 상태 등과 같은 기존 인스턴스에서 기능적으로 분할된 새로운 Redis 인스턴스를 추가하게 됩니다. 이 문서에서는 기존 데이터를 처리하는 새로운 Redis 인스턴스를 추가하기 위한 접근 방식을 설명합니다. 다음은 기존의 예제를 기반으로 합니다.

본 문서에서는 새로운 Redis 인스턴스의 운영 관련 내용에 대해서는 상세히 다루지 않지만, 예제의 이픽에는 해당 정보가 포함되어 있습니다.

단계 1: 새로운 인스턴스 구성 지원

새로운 인스턴스를 사용하도록 어떠한 특성을 변경하기 전에, 해당 인스턴스를 구성하고 코드베이스에서 참조할 수 있도록 지원해야 합니다. 다음과 같은 주요 설치 유형을 지원해야 합니다:

대체 인스턴스

응용 프로그램 코드에서, 새로운 인스턴스가 구성되지 않은 경우를 위해 대체 인스턴스를 정의해야 합니다. 예를 들어, 만약 GitLab 인스턴스가 이미 별도의 공유 상태 Redis를 구성했고, 우리가 공유 상태 Redis의 데이터를 분할하는 경우, 새로운 인스턴스의 구성은 해당 Redis가 없을 때 공유 상태 Redis로 기본 설정되어야 합니다. 그렇지 않으면 새로 구성되는 Redis 인스턴스가 사용 가능해지자마자 구성하지 않은 인스턴스를 망가뜨릴 수 있습니다.

Gitlab::Redis::Wrapper에서 이러한 것을 정의할 수 있는 .config_fallback 메서드를 정의해야 합니다. 만약 Foo 인스턴스를 추가하고, 해당 인스턴스가 SharedState로 기본적으로 돌아갈 필요가 있다면, 다음과 같이 할 수 있습니다:

module Gitlab
  module Redis
    class Foo < ::Gitlab::Redis::Wrapper
      # Foo에서 사용하는 데이터는 이전에 SharedState에 저장되었습니다.
      def self.config_fallback
        SharedState
      end
    end
  end
end

이와 유사하게, 이러한 대체가 정상적으로 작동하는지 확인하기 위해 trace_chunks_spec.rb와 같은 스펙도 추가해야 합니다.

단계 2: 새 인스턴스에 데이터 작성 및 읽기 지원

새 인스턴스로 마이그레이션할 때, 데이터가 이미 기존 ‘구’ 인스턴스에 있거나 방금 추가한 새 인스턴스에 있는 경우에 대비해 양쪽에 모두 데이터를 읽고 쓸 수 있어야 합니다.

데이터를 마이그레이션해야 하는 조건에 따라 어디서 읽고 쓸지가 달라질 수 있습니다. 위의 추적 청크 사례의 경우, 데이터가 저장된 위치를 나타내는 데이터베이스 열이 이미 있었기 때문에(레디스 이외의 다른 저장 옵션이 있기 때문에), 우리는 두 인스턴스 모두에서 읽고 쓸 수단이 필요할 수 있습니다.

이 단계는 데이터 수명이 매우 짧고(최대 몇 분) 중요하지 않을 경우에는 적용되지 않을 수 있습니다. 이 경우 데이터 손실이 발생해도 괜찮다고 판단할 수 있고, 구성만으로 전환하는 것이 가능할 수 있습니다.

데이터가 저장된 위치를 표시하는 더 좋은 방법이 없는 경우에는 피처 플래그를 사용하는 것이 편리할 수 있습니다.

  • 적용에 지연이 없습니다.
  • 모든 응용 프로그램 인스턴스(Sidekiq, API, 웹 등)에 동시에 적용됩니다.
  • 증분 배포를 지원하며, 이상적으로는 사용자(프로젝트, 그룹, 사용자 등)별로 모니터링하고 오류를 쉽게 롤백할 수 있습니다.

단계 3: 데이터 마이그레이션

그런 다음, GitLab.com의 프로덕션 및 스테이징 환경에 대해 새 인스턴스를 구성해야 합니다. 이 변경 사항을 스테이징에서 효과적으로 테스트할 수 있기를 희망합니다.

그것이 완료된 후, 우리는 프로덕션으로 변경 사항을 롤아웃할 수 있게 됩니다. 이상적인 상황에서는 피처 플래그의 표준적인 증분 롤아웃 문서를 따라할 수 있어야 합니다.

새로운 인스턴스를 프로덕션에서 100%의 시간을 사용하면서 문제가 없는 경우에는 진행할 수 있을 것입니다.

제안된 해결책: MultiStore를 사용한 데이터 마이그레이션

우리는 UX 관점에서 어떠한 불편함도 없이 사용자를 새로운 Redis 스토어로 마이그레이션할 수 있는 방법이 필요합니다. 또한 새 인스턴스에 문제가 발생했을 경우 “구” Redis 인스턴스로 되돌아갈 수 있는 기능을 원합니다.

마이그레이션 요구 사항은 다음과 같습니다:

  • 다운타임 없음
  • 데이터 저장을 위한 TTL이 만료될 때까지 데이터 손실이 없음
  • 피처 플래그나 ENV 변수 또는 둘의 조합을 사용한 증분 배포
  • 전환 모니터링
  • Prometheus 지표가 완성됨
  • 예상치 못한 인스턴스나 로직이 예상과 다르게 작동할 경우 다운타임 없이 쉽게 롤백

이것은 다운타임이 없는 DB 테이블 이름 변경과 어느 정도 유사합니다. 우리는 새로운 Redis 인스턴스와 “구” 인스턴스 양쪽에 데이터를 작성해야 합니다. 우리는 새 인스턴스에서 읽지만, 새로운 전용 Redis 인스턴스에서 빠져 나올 때 “구” 인스턴스로 되돌아가야 합니다.

우리는 새로운 인스턴스로 발생한 어떠한 이슈나 예외 상황을 기록해야 하지만, 여전히 “구” 인스턴스로 돌아가야 할 필요가 있습니다.

제안된 마이그레이션 전략은 MultiStore를 구현하고 사용하는 것입니다. 이러한 접근 방식은 세션 키에 새로운 전용 Redis 인스턴스 추가에서 사용했던 것과 매우 흡사합니다. 또한, MultiStore에 해당하는 스펙이 있습니다.

MultiStore는 redis-rb ::Redis 인스턴스와 비슷합니다.

단계 1에서 추가한 새로운 Redis 인스턴스 클래스에서 ::Gitlab::Redis::MultiStoreWrapper에서 상속받아 multistore 클래스 메서드를 재정의하여 MultiStore를 정의해야 합니다.

module Gitlab
  module Redis
    class Foo < ::Gitlab::Redis::MultiStoreWrapper
      ...
      def self.multistore
        MultiStore.create_using_pool(self.pool, config_fallback.pool, store_name)
      end
    end
  end
end

MultiStore는 새 Redis 연결 풀을 주요 풀로, 구 (대체 인스턴스) 연결 풀을 보조 풀로 제공받아 초기화됩니다. 세 번째 인수는 로그, 메트릭 및 피처 플래그 이름에 사용되는 store_name입니다. 여러 개의 Redis 스토어에 대해 MultiStore 구현을 사용하는 경우에는 필요합니다.

기본적으로 MultiStore는 기본 Redis 스토어만에서 읽고 쓰기를 수행합니다. 기본 Redis 스토어는 secondary_store(구 대체 인스턴스)입니다. 이를 통해 우리는 기본 동작을 변경하지 않고 MultiStore를 소개할 수 있습니다.

MultiStore는 실제 마이그레이션을 제어하기 위해 두 개의 피처 플래그를 사용합니다.

  • use_primary_and_secondary_stores_for_[store_name]
  • use_primary_store_as_default_for_[store_name]

예를 들어, 새 Redis 인스턴스가 Gitlab::Redis::Foo라고 한다면 다음과 같이 두 개의 피처 플래그를 생성할 수 있습니다.

bin/feature-flag use_primary_and_secondary_stores_for_foo
bin/feature-flag use_primary_store_as_default_for_foo

use_primary_and_secondary_stores_for_foo 피처 플래그를 활성화하면 Gitlab::Redis::Foo에서는 MultiStore를 사용하여 새 Redis 인스턴스와 구 (대체 인스턴스) 모두에 쓰기를 수행합니다. 모든 읽기 명령은 use_primary_store_as_default_for_foo 피처 플래그를 통해 제어되는 기본 스토어에서만 수행됩니다. use_primary_store_as_default_for_foo 피처 플래그를 활성화하면, MultiStore는 기본적으로 primary_store(새 인스턴스)로 설정된 Redis 스토어를 사용합니다.

pipelined 명령(pipelinedmulti)의 경우, 우리는 두 스토어 모두에서 전체 작업을 실행하고 결과를 비교합니다. 그러한 경우 결과가 다르다면, Gitlab::Redis::MultiStore:PipelinedDiffError 오류를 발생시키고, 해당 오류는 gitlab_redis_multi_store_pipelined_diff_error_total 프로메테우스 카운터로 추적됩니다.

새로운 스토어에 대한 기간이 잘 채워졌다고 가정하면, 우리는 두 개의 스토어 상태를 비교하는 외부 검증을 수행할 수 있습니다. 만족스러운 유효성 검사 결과 후에, 새 Redis 스토어로 트래픽을 전환할 수 있을 것으로 생각합니다. use_primary_and_secondary_stores_for_foo 피처 플래그를 비활성화합니다. 이를 통해 MultiStore는 새 Redis 스토어로부터의 읽기 및 쓰기만을 수행하므로, 모든 트래픽이 새 Redis 스토어로 이동됩니다.

모든 트래픽이 기본 스토어로 이동했다면, 데이터 마이그레이션이 완료됩니다. 우리는 안전하게 MultiStore 구현을 제거하고 새롭게 소개된 Redis 스토어 인스턴스를 계속해서 사용할 수 있습니다.

구현 세부 정보

MultiStore는 읽기 및 쓰기 Redis 명령을 별도로 구현합니다.

읽기 명령

읽기 명령은 Gitlab::Redis::MultiStore::READ_COMMANDS 상수에 정의되어 있습니다.

쓰기 명령

쓰기 명령은 Gitlab::Redis::MultiStore::WRITE_COMMANDS 상수에 정의되어 있습니다.

pipelined 명령

참고: 이러한 명령에 전달되는 Ruby 블록은 각 리포지터리마다 한 번씩 실행됩니다. 따라서 Redis 작업을 제외하고 블록은 멱등성이어야 합니다.

  • pipelined
  • multi

지원 디렉터리 외의 명령을 사용할 경우, method_missing이 이를 이전 Redis 인스턴스로 전달하고 추적합니다. 이를 통해 예상치 못한 경우에도 이전과 같이 동작하도록 보장합니다. 개발 또는 테스트 환경에서는 이를 조기에 감지하기 위해 오류가 발생합니다.

note
gitlab_redis_multi_store_method_missing_total 카운터 및 Gitlab::Redis::MultiStore::MethodMissingError를 추적함으로써, 개발자는 마이그레이션을 진행하기 전에 누락된 Redis 명령에 대한 구현을 추가해야 합니다.
note
pipelinedmulti 블록 내에서의 변수 할당은 블록이 멱등성이어야 하므로 권장되지 않습니다. 멱등성이 아닌 블록을 제거한 수정 MR를 참조하십시오. 이전에는 마이그레이션 중에 잘못된 애플리케이션 동작을 초래했습니다.
오류
오류 메시지
Gitlab::Redis::MultiStore::PipelinedDiffError pipelined 명령이 두 리포지터리 모두에서 성공적으로 실행되었지만 결과가 다른 경우입니다.
Gitlab::Redis::MultiStore::MethodMissingError 해당 메서드가 없습니다. Redis 보조 리포지터리에서 메서드를 실행하도록 되감아줍니다.
메트릭
메트릭 이름 유형 레이블 설명
gitlab_redis_multi_store_pipelined_diff_error_total Prometheus 카운터 command, instance_name Redis MultiStore pipelined 명령 결과의 차이
gitlab_redis_multi_store_method_missing_total Prometheus 카운터 command, instance_name 클라이언트 측 Redis MultiStore 누락된 메서드 총계

단계 4: 마이그레이션 후 정리

Self-Managed형 인스턴스가 이 마이그레이션을 수행할 것으로 예상하는지에 따라 마이그레이션 경로를 유지하거나 제거할 수 있습니다. gitlab-com/gl-infra/scalability#1131 트레이스 청크 피처 플래그에 대한 이 주제에 대한 토론을 담고 있습니다. 이 경우처럼 유지하거나 제거할지를 결정할 수 있습니다. 지원 명령 코드의 유지 보수 비용이 기능적 분할 없이도 Self-Managed형 인스턴스가 처리할 수 있는 것에 비해 높은 경우를 준비하십시오.

마이그레이션 코드를 유지하기로 결정하는 경우:

  • 마이그레이션 단계를 문서화해야 합니다.
  • 피처 플래그를 사용했다면 ops 유형 플래그인지 확인해야 합니다. 이들은 장기적인 플래그입니다.

그렇지 않으면 플래그를 제거하고 프로젝트를 완료할 수 있습니다.