새 Redis 인스턴스 추가

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

가끔씩 우리는 새로운 Redis 인스턴스를 추가하고 싶을 수 있습니다. 일반적으로, 이는 캐시 또는 공유 상태와 같은 기존 인스턴스에서 기능적으로 분리된 것일 것입니다. 이 문서는 기존 데이터를 다루는 새로운 Redis 인스턴스를 추가하는 방법에 대한 접근 방식을 설명합니다. 이는 이전 예제를 기반으로 합니다.

이 문서는 새로운 Redis 인스턴스의 운영 측면을 자세하게 다루지는 않지만, 그 예제에 새로운 Redis 인스턴스를 구성하는 방법에 대한 정보가 포함되어 있습니다.

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

새로운 인스턴스를 사용하도록 기능을 전환하기 전에 코드베이스에서 이를 지원하고 참조할 수 있도록 지원해야 합니다. 다음과 같은 주요 설치 유형을 지원해야 합니다.

Fallback 인스턴스

애플리케이션 코드에서, 새로운 인스턴스가 구성되지 않은 경우를 대비해 fallback 인스턴스를 정의해야 합니다. 예를 들어, 만약 GitLab 인스턴스가 이미 별도의 공유 상태 Redis를 구성했고 우리가 공유 상태 Redis에서 데이터를 분할하고 있다면, 새로운 인스턴스의 구성은 그대로 공유 상태 Redis의 것으로 기본 설정되어야 합니다. 그렇지 않으면 새로운 Redis 인스턴스를 구성하지 않은 인스턴스를 즉시 망가뜨릴 수도 있습니다.

Gitlab::Redis::Wrapper (모든 Redis 인스턴스의 기본 클래스)에 .config_fallback 메소드를 정의하여 이를 수행할 수 있습니다. 만약 Foo 인스턴스를 추가하고자 한다면, 다음과 같이 할 수 있습니다.

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

이와 유사한 지정들이 trace_chunks_spec.rb에 있기 때문에 이 fallback이 올바르게 작동하는지 확인하기 위해 이러한 지정을 추가해야 합니다.

단계 2: 새 인스턴스에 쓰기 및 읽기 지원

새 인스턴스로 마이그레이션할 때, 데이터가 기존 또는 새로운 인스턴스에 있는 경우를 고려해야 합니다. 결과적으로, 어떤 조건에 따라 두 인스턴스 모두에 대해 읽기와 쓰기를 지원해야 할 수도 있습니다.

사용할 정확한 조건은 마이그레이션할 데이터에 따라 다릅니다. 예를 들어, 앞서 언급한 Trace Chunk의 경우 이미 데이터가 저장된 위치를 나타내는 데이터베이스 열이 있었습니다 (Redis 이외의 다른 저장 옵션이 있기 때문에).

데이터가 수명이 매우 짧고 중요하지 않을 경우, 응용프로그램을 다시 시작할 필요없이 구성만으로 전환하는 것이 적합할 수 있습니다.

데이터가 저장된 위치를 표시하는 더 자연스러운 방법이 없는 경우, 기능 플래그 사용이 편리할 수 있습니다.

  • 효력을 행사하려면 애플리케이션을 다시 시작할 필요가 없습니다.
  • 모든 애플리케이션 인스턴스(사이드킥, API, 웹 등)에 동시에 적용됩니다.
  • 증분 롤아웃을 지원하며 가급적으로 액터(프로젝트, 그룹, 사용자 등)별로 수행하여 오류를 모니터링하고 쉽게 롤백할 수 있습니다.

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

그 후, 새로운 인스턴스를 GitLab.com의 프로덕션 및 스테이징 환경에 구성해야 합니다. 이 변경 사항을 스테이징에서 효과적으로 테스트할 수 있는지 확인하는 것이 좋습니다.

이 작업이 완료된 후, 우리는 이 변경사항을 프로덕션에 롤아웃할 수 있습니다. 이상적으로는 기능 플래그에 대한 표준적인 증분 롤아웃 문서를 따라야 합니다.

새 인스턴스를 프로덕션에서 100%의 활동 시간 동안 사용하고 모든 문제가 없는 경우, 우리는 계속 진행할 수 있습니다.

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

UX 관점에서 편의를 느끼지 못하게 하면서 사용자를 새로운 Redis 스토어로 마이그레이션시키기 위한 방법이 필요합니다. 또한, 새로운 인스턴스가 예상치 못한 문제가 발생한 경우 “이전” Redis 인스턴스로 회귀할 수 있는 기능도 필요합니다.

마이그레이션 요구 사항:

  • 다운타임이 없어야 합니다.
  • 데이터 저장을 위한 TTL이 만료될 때까지 저장된 데이터의 손실이 있어서는 안 됩니다.
  • 기능 플래그 또는 ENV 변수 또는 이 둘의 조합을 사용한 부분 롤아웃이 가능해야 합니다.
  • 마이그레이션 모니터링이 필요합니다.
  • Prometheus 지표가 마련되어야 합니다.
  • 새로운 인스턴스나 논리가 예상대로 작동하지 않는 경우, 쉽게 다운타임 없이 회귀할 수 있는 방법이 필요합니다.

이는 다운타임 없는 DB 테이블 이름 변경과 다소 유사합니다. 우리는 새로운 Redis 인스턴스와 기존(예비) 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 저장소에서만 읽고 쓰도록 설정되어 있습니다. 기본 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::FooMultiStore를 사용하여 새로운 Redis 인스턴스와 기존 (fallback-instance) 모두에 쓰기를 수행합니다. 모든 읽기 명령은 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 명령어

참고: 이러한 명령에 전달된 루비 블록은 각 스토어당 한 번씩 실행됩니다. 따라서 Redis 연산을 제외하고 블록은 멱등성을 가져야 합니다.

  • pipelined
  • multi

지원 목록을 벗어난 명령을 사용할 때, method_missing은 예기치 않은 동작을 보장하기 위해 오래된 Redis 인스턴스로 전달하고 추적합니다. 이렇게 함으로써 예상치 못한 동작을 예전처럼 동작하도록 보장합니다. 개발 또는 테스트 환경에서는 조기 감지를 위해 오류가 발생할 것입니다.

참고: gitlab_redis_multi_store_method_missing_total 카운터 및 Gitlab::Redis::MultiStore::MethodMissingError를 추적함으로써, 개발자는 마이그레이션을 계속하기 전에 누락된 Redis 명령어에 대한 구현을 추가해야 합니다.

참고: 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 유형 기능 플래그임을 확인해야 합니다. 이러한 플래그는 장기적인 플래그입니다.

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