새로운 Redis 인스턴스 추가

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

가끔씩 새로운 Redis 인스턴스를 추가하고 싶을 수 있습니다. 일반적으로 이는 기존 인스턴스 중 하나인 캐시나 공유 상태에서 기능적으로 분할된 것일 것입니다. 이 문서는 기존 데이터를 처리하는 새로운 Redis 인스턴스를 추가하기 위한 접근 방법을 설명합니다. 이는 이전 예시에 기반을 두고 있습니다:

이 문서는 새로운 Redis 인스턴스의 운영 측면을 심도 있게 다루지는 않지만, 예시 Epic에는 이에 대한 정보가 포함되어 있습니다.

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

새 인스턴스를 사용하도록 기능을 전환하기 전에, 코드 기반에서 이를 구성하고 참조할 수 있도록 지원해야 합니다. 이를 위해 주요 설치 유형을 지원해야 합니다:

Fallback 인스턴스

응용 프로그램 코드에서, 새 인스턴스가 구성되지 않은 경우를 대비하여 대처할 수 있는 후퇴 인스턴스를 정의해야 합니다. 예를 들어, GitLab 인스턴스가 이미 별도로 공유 상태 Redis를 구성했고, 우리가 공유 상태 Redis에서 데이터를 파티션하고 있다면, 새로운 인스턴스의 구성은 존재하지 않을 때 공유 상태 Redis의 구성을 기본값으로 설정해야 합니다. 그렇지 않으면 새로운 Redis 인스턴스를 구성하지 않은 인스턴스를 바로 망가뜨릴 수 있습니다.

Gitlab::Redis::Wrapper.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와 같은 스펙을 추가하여 이 후퇴 동작이 올바르게 작동하는지 확인해야 합니다.

단계 2: 새로운 인스턴스에 쓰고 읽는 것 지원

새 인스턴스로 마이그레이션할 때, 데이터가 다음 중 어느 곳에 있는지에 따라 처리해야 합니다.

  • ‘이전’ (원본) 인스턴스
  • 방금 추가한 새 인스턴스

결과적으로 어떤 조건에 따라 두 인스턴스에 모두 읽고 쓰도록 지원해야 할 수 있습니다.

사용할 정확한 조건은 마이그레이션할 데이터에 따라 다르며, 위에서 언급한 trace chunks 경우 이미 데이터가 어디에 저장되어 있는지를 나타내는 데이터베이스 열이 있었습니다 (Redis 외에도 다른 저장 옵션이 있기 때문입니다).

이 단계는 데이터의 수명이 매우 짧고 중요하지 않은 경우에는 적용되지 않을 수 있습니다. 이 경우에는 작은 데이터 손실을 감수하고 구성을 통해 전환하는 것이 괜찮다고 결정할 수 있습니다.

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

  • 이를 적용하려면 응용프로그램 다시 시작이 필요하지 않습니다.
  • 모든 응용프로그램 인스턴스 (Sidekiq, API, 웹 등)에 동시에 적용됩니다.
  • 프로젝트, 그룹, 사용자 등의 작 (actor)별로 일부분 구현을 지원하기 때문에 에러를 모니터링하고 쉽게 롤백할 수 있습니다.

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

그런 다음, 새 인스턴스를 GitLab.com의 프로덕션 및 스테이징 환경에 구성해야 합니다. 이 변경 사항을 스테이징에서 효과적으로 테스트할 수 있는 것이 좋을 것입니다. 그 후, 우리는 이 변경 사항을 프로덕션으로 롤아웃할 수 있게 될 것입니다. 이상적으로는 표준적인 점진적 롤아웃 문서에 따라서 이를 점진적으로 실행할 수 있을 것입니다.

새 인스턴스를 프로덕션에서 사용하더라도 문제가 없는 상태로 일정 기간 동안 사용한 후에는 계속 진행할 수 있을 것입니다.

제안된 해결책: MultiStore와 후퇴 전략을 사용하여 데이터 마이그레이션

우리는 사용자를 새로운 Redis 리포지터리로 마이그레이션하는 방법이 필요합니다. 또한 새 인스턴스에 문제가 발생할 경우 “이전” Redis 인스턴스로 돌아갈 수 있는 기능도 원합니다.

마이그레이션 요구 사항:

  • 다운타임 없음.
  • 데이터 저장이 만료될 때까지 저장 데이터 유실 없음.
  • 피처 플래그 또는 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.new(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 인스턴스와 이전 (후퇴) 인스턴스에 모두 기록합니다. 모든 읽기 명령은 기본 리포지터리에서만 수행되며, 이는 use_primary_store_as_default_for_foo 피처 플래그를 사용하여 제어합니다. use_primary_store_as_default_for_foo 피처 플래그를 활성화하면 MultiStore는 (새 인스턴스) 기본적으로 primary_store를 사용하여 작동합니다.

파이프라인 명령(pipelined 명령 및 multi 명령)에 대해서는 두 리포지터리에서 모두 작업을 수행한 후 결과를 비교합니다. 결과가 다른 경우, Gitlab::Redis::MultiStore:PipelinedDiffError 오류를 발생시키고 gitlab_redis_multi_store_pipelined_diff_error_total Prometheus 카운터에 기록합니다.

새 리포지터리가 채워진 후에 일정 기간이 지나면 외부 검증을 수행하여 두 리포지터리의 상태를 비교할 수 있습니다. 검증 결과가 만족스러우면, 새로운 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 작업을 제외하고 블록은 멱등(idempotent)해야 합니다.

  • 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 리포지터리 MultiStore pipelined 명령의 리포지터리 간 차이
gitlab_redis_multi_store_method_missing_total Prometheus 카운터 command, instance_name 클라이언트 측 Redis MultiStore 메소드 누락 총계

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

우리는 이주 경로를 유지하거나 제거할지 선택할 수 있습니다. 이는 Self-managed 인스턴스가 이 마이그레이션을 처리할 것으로 예상하는지 여부에 따라 다릅니다. gitlab-com/gl-infra/scalability#1131 에는 이러한 주제에 대한 토론이 포함되어 있습니다. trace chunks 피처 플래그의 경우와 같이 유지할때와 유지하지 않을지를 결정할 수 있습니다. 우리가 유지 코드 지원의 유지 비용이 Self-managed 인스턴스가 이 기능적 분할 없이도 처리할 수 있는 이점보다 높다고 결정한 경우가 그러합니다.

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

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

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

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