Redis 개발 지침

Redis 인스턴스

GitLab은 다음과 같은 목적으로 Redis를 사용합니다.

  • 캐싱(대부분은 Rails.cache를 통해).
  • Sidekiq를 사용한 작업 처리 대기열로.
  • 공유 애플리케이션 상태 관리.
  • CI 추적 청크 저장.
  • ActionCable을 위한 Pub/Sub 대기열 백엔드로.
  • 요청 제한 상태 저장.
  • 세션.

대부분의 환경(포함하여 GDK)에서 이러한 모든 목적은 동일한 Redis 인스턴스를 가리킵니다.

GitLab.com에서는 별도의 Redis 인스턴스를 사용합니다. 더 많은 설정에 대한 자세한 내용은 Redis SRE 가이드를 참조하세요.

모든 애플리케이션 프로세스가 동일한 Redis 서버를 사용하도록 구성되어 있으므로, PostgreSQL이 적합하지 않은 경우에는 프로세스 간 통신에 사용할 수 있습니다. 예를 들어, 일시적인 상태 또는 읽히는 것보다 훨씬 더 자주 쓰이는 데이터와 같은 경우입니다.

Geo가 활성화된 경우, 각 Geo 노드에는 독립적인 Redis 데이터베이스가 제공됩니다.

새로운 Redis 인스턴스를 추가하는 데 대한 개발 문서를 참조하십시오.

키 명명

Redis는 계층 구조가 없는 평면 네임스페이스이기 때문에, 충돌을 피하기 위해 키 이름에 주의를 기울여야 합니다. 일반적으로 우리는 콜론(:)으로 구분된 요소들을 사용하여 응용 프로그램 수준에서 구조를 제공합니다. 예를 들어 projects:1:somekey와 같은 예시가 있을 수 있습니다.

우리는 Redis 사용을 목적에 따라 다른 카테고리로 나누지만, 고가용성 구성인 GitLab.com과 같은 경우 각각의 카테고리가 별도의 Redis 서버에 매핑될 수 있습니다. 이는 키가 모든 카테고리 전반에 걸쳐 항상 전역적으로 고유해야 함을 의미합니다.

Redis 키 이름에는 대부분 불변 식별자(예: 프로젝트 ID)를 사용하는 것이 일반적으로 더 좋습니다. 예를 들어 전체 경로가 아닌 프로젝트 ID를 사용하는 것이 좋습니다. 경로 전체를 사용한 경우 프로젝트 이름이 변경되면 키가 더 이상 사용되지 않게 됩니다. 키의 내용이 이름 변경으로 인해 무효화된다면, 키가 변경되지 않도록 하는 대신 엔트리가 만료되도록 하는 후크를 포함시키는 것이 좋습니다.

다중 키 명령

GitLab은 중요한 작업량 유형에 대한 Redis 클러스터를 지원합니다.

이는 또 다른 명명 제약 조건을 부과합니다: GitLab이 Redis 서버에서 여러 키를 보유해야 하는 작업을 수행할 때(예: Redis에 보관되는 두 세트의 차이 비교), 키는 변할 수 있는 부분들을 중괄호로 묶어서 같은 Redis 서버에 저장되도록 해야 합니다. 예를 들어:

project:{1}:set_a
project:{1}:set_b
project:{2}:set_c

set_aset_b는 동일한 Redis 서버에 보관되며, set_c는 그렇지 않습니다.

현재 이를 개발 및 테스트 환경에서 RedisClusterValidator를 사용하여 유효성을 검사하고 있습니다. 이는 cacheshared_state와 같은 Redis 인스턴스에 대해 활성화되어 있습니다.

개발자들은 미래에 Redis 클러스터가 더 많은 Redis 유형에서 채택될 수 있도록 지원하기 위해, 적절한 곳에 해시태그를 사용하는 것이 권장됩니다. 예를 들어, Namespace 모델은 config cache keys에 대해 해시태그를 사용합니다.

다중 키 명령을 수행하기 위해, 개발자는 .pipelined 메서드를 사용하여 각 노드로 명령을 분리하여 보내고 응답을 집계할 수 있습니다. 하지만, 이는 트랜잭션을 위한 것이 아니며, Redis 클러스터는 교차 슬롯 트랜잭션을 지원하지 않습니다.

Rails.cache의 경우, .pipelined 메서드를 사용하여 read_multi_get에서 발견된 MGET 명령을 처리하고, 최소 파이프라인 크기는 1000 명령이며 GITLAB_REDIS_CLUSTER_PIPELINE_BATCH_LIMIT 환경 변수를 사용하여 조정할 수 있습니다.

구조화된 로깅에서의 Redis

GitLab 팀원을 위한: GitLab.com에서 Redis 구조화된 로깅 필드를 사용하는 방법을 보여주는 기본 동영상과 고급 동영상이 있습니다.

우리의 웹 요청 및 Sidekiq 작업에 대한 구조화된 로깅은 Redis 인스턴스당 지속 시간, 호출 횟수, 기록된 바이트, 읽은 바이트 및 전체 Redis 인스턴스를 위한 필드를 포함합니다. 특정 요청의 경우 다음과 같을 수 있습니다.

필드
json.queue_duration_s 0.01
json.redis_cache_calls 1
json.redis_cache_duration_s 0
json.redis_cache_read_bytes 109
json.redis_cache_write_bytes 49
json.redis_calls 2
json.redis_duration_s 0.001
json.redis_read_bytes 111
json.redis_shared_state_calls 1
json.redis_shared_state_duration_s 0
json.redis_shared_state_read_bytes 2
json.redis_shared_state_write_bytes 206
json.redis_write_bytes 255

이러한 모든 필드가 색인화되므로, 실제 운영 환경에서 Redis 사용을 조사하는 것이 간단합니다. 예를 들어, 캐시에서 가장 많은 데이터를 읽는 요청을 찾으려면 단순히 redis_cache_read_bytes를 내림차순으로 정렬하면 됩니다.

느린 로그

참고: GitLab.com에서 느린 로그를 확인하는 방법(GitLab 내부) 을 보여주는 비디오가 있습니다.

GitLab.com에서는 Redis 느린 로그 항목이 pubsub-redis-inf-gprd* 색인에 있으며, redis.slowlog 태그 에 있습니다. 이는 오랜 시간이 소요된 명령 및 성능에 영향을 줄 수 있는 것들을 보여줍니다.

fluent-plugin-redis-slowlog 프로젝트는 Redis의 slowlog 항목을 가져와서 Fluentd(마지막으로는 Elasticsearch)로 전달하는 역할을 맡고 있습니다.

전체 키스페이스 분석

Redis Keyspace Analyzer 프로젝트에는 Redis 인스턴스의 전체 키 목록 및 메모리 사용량을 덤프하고, 결과에서 잠재적으로 민감한 데이터를 제거하면서 해당 목록을 분석하는 도구가 포함되어 있습니다. 가장 빈번한 키 패턴 또는 가장 많은 메모리를 사용하는 키를 찾는 데 사용할 수 있습니다.

현재 이 작업은 GitLab.com Redis 인스턴스에서 자동으로 실행되지 않지만 필요에 따라 수동으로 실행됩니다.

N+1 호출 문제

RedisCommands::Recorder는 테스트에서 Redis N+1 호출 문제를 감지하는 도구입니다.

Redis는 주로 캐싱 용도로 사용됩니다. 일반적으로 캐시 호출은 가볍고 Redis 인스턴스에 영향을 미칠만한 부하를 생성하지 못합니다. 그러나 이를 모르고 부하를 생성하는 비용이 많이 드는 캐시 다시 계산을 트리거할 수도 있습니다. Redis 호출을 분석하고 그에 대한 기대되는 한도를 정의하세요.

테스트 작성

이는 ActiveSupport::Notifications instrumenter로 구현되었습니다.

단일 Redis 호출을 하는지 확인하는 테스트를 만들 수 있습니다.

it 'N+1 Redis 호출을 피합니다' do
  control = RedisCommands::Recorder.new { visit_page }

  expect(control.count).to eq(1)
end

또는 특정 Redis 호출의 수를 확인하는 테스트를 만들 수 있습니다.

it 'N+1 sadd Redis 호출을 피합니다' do
  control = RedisCommands::Recorder.new { visit_page }

  expect(control.by_command(:sadd).count).to eq(1)
end

특정 Redis 호출만 캡처하도록 하려면 패턴을 제공할 수도 있습니다.

it 'forks_count 키로의 N+1 Redis 호출을 피합니다' do
  control = RedisCommands::Recorder.new(pattern: 'forks_count') { visit_page }

  expect(control.count).to eq(1)
end

또한 특정 Redis 호출의 수에 상한선을 정의하기 위해 exceed_redis_calls_limitexceed_redis_command_calls_limit 특별한 매처를 사용할 수 있습니다.

it 'N+1 Redis 호출을 피합니다' do
  control = RedisCommands::Recorder.new { visit_page }

  expect(control).not_to exceed_redis_calls_limit(1)
end
it 'N+1 sadd Redis 호출을 피합니다' do
  control = RedisCommands::Recorder.new { visit_page }

  expect(control).not_to exceed_redis_command_calls_limit(:sadd, 1)
end

이러한 테스트는 Redis 호출과 관련된 N+1 문제를 식별하는 데 도움이 되며, 그에 대한 수정이 예상대로 작동하는지 확인할 수 있습니다.

참고 자료

유틸리티 클래스

우리는 특정한 사용 사례를 돕기 위한 몇 가지 추가 클래스를 보유하고 있습니다. 이들은 대부분 레디스 사용의 미세한 제어를 위한 것이므로, Rails.cache 래퍼와 결합해서 사용되지 않습니다. 대신에 Rails.cache 또는 이러한 클래스와 글자 그대로의 레디스 명령을 사용할 것입니다.

우리는 향후 Rails에 대한 최적화의 이점을 누리기 위해 Rails.cache를 사용하는 것을 선호합니다. 루비 객체는 Redis에 쓰여질 때 marshalled되므로, 우리는 대형 객체나 믿을 수 없는 사용자 입력을 저장하지 않도록 주의해야 합니다.

일반적으로 다음 중 하나 이상의 경우에만 이러한 클래스를 사용할 것입니다:

  1. 캐시 아닌 레디스 인스턴스에서 데이터를 조작하고 싶을 때.
  2. Rails.cache가 수행하고자 하는 작업을 지원하지 않을 때.

Gitlab::Redis::{Cache,SharedState,Queues}

이러한 클래스는 레디스 인스턴스를 감싸서(사용 중인 Gitlab::Redis::Wrapper를 사용하여) 직접적으로 작업할 수 있도록 편리하게 만듭니다. 일반적으로 클래스에서 .with를 호출하여, 레디스 연결을 산출하는 블록을 인자로 취합니다. 예를 들면:

# 공유 상태 (지속적) 레디스에서 `key`의 값 가져오기
Gitlab::Redis::SharedState.with { |redis| redis.get(key) }

# `value`가 `key`의 집합의 구성원인지 확인하기
Gitlab::Redis::Cache.with { |redis| redis.sismember(key, value) }

Gitlab::Redis::Boolean

레디스에서 각 값은 문자열입니다. Gitlab::Redis::Boolean은 부울값이 일관되게 인코딩되고 디코딩되도록 보장합니다.

Gitlab::Redis::HLL

레디스 PFCOUNT, PFADD, 및 PFMERGE 명령어는 하이퍼로그로그, 적은 메모리 사용으로 고유한 요소의 수를 추정할 수 있는 데이터 구조상에서 작동합니다. 자세한 내용은 Redis의 HyperLogLogs를 참조하세요.

Gitlab::Redis::HLL은 하이퍼로그로그에 값 추가 및 개수를 세는 것에 대한 편리한 인터페이스를 제공합니다.

Gitlab::SetCache

항목이 특정 그룹 안에 있는지 효율적으로 확인해야 하는 경우, 레디스 집합을 사용할 수 있습니다. Gitlab::SetCacheSISMEMBER 명령어를 사용하는 #include? 메소드와 함께, 집합에 있는 모든 항목을 가져오는 #read를 제공합니다.

이것은 브랜치 이름과 같은 깃 저장소 데이터를 캐시하는 데 집합을 사용하기 위해 RepositorySetCache에서 사용됩니다.

백그라운드 마이그레이션

레디스 기반 마이그레이션은 특정 키 패턴의 전체 레디스 인스턴스를 스캔하기 위해 SCAN 명령어를 사용합니다. 대용량 레디스 인스턴스의 경우, 마이그레이션은 보통이나 배포 후 마이그레이션의 시간 제한을 초과할 수 있습니다. RedisMigrationWorker는 백그라운드 마이그레이션으로 긴 기간 실행되는 레디스 마이그레이션을 수행합니다.

클래스를 생성하여 배포 후 마이그레이션을 사용하여 백그라운드 마이그레이션을 실행하려면:

module Gitlab
  module BackgroundMigration
    module Redis
      class BackfillCertainKey
        def perform(keys)
        # 키를 정리하거나 채우는 논리를 구현합니다
        end

        def scan_match_pattern
        # `SCAN` 명령어의 매치 패턴을 정의합니다.
        end

        def redis
        # 정확한 레디스 인스턴스를 정의합니다.
        end
      end
    end
  end
end

배포 후 마이그레이션을 통해 워커를 트리거하려면:

class ExampleBackfill < Gitlab::Database::Migration[2.1]
  disable_ddl_transaction!

  MIGRATION='BackfillCertainKey'

  def up
    queue_redis_migration_job(MIGRATION)
  end
end