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 대신 전체 경로를 사용하는 것이 좋습니다. 전체 경로를 사용하면 프로젝트의 이름이 변경되면 키를 조회하지 않음으로써 무효화됩니다. 키의 내용이 이름 변경으로 인해 무효화된다면 키가 바뀌기를 의존하는 대신 항목을 만료시키는 후크를 포함시키는 것이 좋습니다.

다중 키 명령어

GitLab은 epic 878에서 도입된 캐시 관련 워크로드를 위해 Redis Cluster를 지원합니다.

이는 다중 키 명령을 수행하는 작업을 하는 경우 추가적인 제약을 가합니다. 예를 들어 Redis에 저장된 두 세트를 비교하는 경우, 변경 가능한 부분을 중괄호로 묶어 해당 Redis 서버에 보관되도록 해야 합니다. 예를 들면:

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

set_aset_b는 동일한 Redis 서버에 보관되는 것이 보장되지만 set_c는 그렇지 않습니다.

현재 개발 및 테스트 환경에서 RedisClusterValidator를 사용하여 적합성을 검증합니다. 이는 Redis 인스턴스에서 cacheshared_state를 위해 활성화되어 있습니다.

개발자는 Redis Cluster의 더 많은 유형에서 Redis Cluster의 미래 채택을 용이하게 하기 위해 필요한 경우 해시 태그를 사용하도록 권장받습니다. 예를 들어 Namespace 모델은 config cache keys에 해시 태그를 사용합니다.

다중 키 명령을 수행하기 위해 개발자는 .pipelined 메서드를 사용할 수 있으며, 이는 각 노드로 명령을 분할하고 응답을 집계합니다. 그러나 이는 Redis Cluster에서 교차 슬롯 트랜잭션을 지원하지 않기 때문에 transactions에 대해서는 작동하지 않습니다.

Rails.cache를 위해 read_multi_get에서 발견된 MGET 명령을 처리하기 위해 .pipelined을 사용하여 패치합니다. 파이프라인의 최소 크기는 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 호출 수의 상한선을 정의하는 데 특별한 matchers 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 값을 또는 신뢰할 수 없는 사용자 입력을 저장해야 하는 큰 객체를 저장하지 않도록 주의해야 합니다.

일반적으로 우리는 다음 중 하나 이상을 충족할 때 이러한 클래스만 사용합니다:

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

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

이러한 클래스는 Redis 인스턴스를 감싸 표준적으로 직접 작업할 수 있도록합니다. 일반적인 사용법은 클래스에서 .with를 호출하고 Redis 연결을 생성하는 블록을 사용하는 것입니다. 예를 들면:

# 공유 상태 (지속적) Redis에서 `key`의 값을 가져옵니다
Gitlab::Redis::SharedState.with { |redis| redis.get(key) }

# `key`의 값을 포함하는지 확인합니다
Gitlab::Redis::Cache.with { |redis| redis.sismember(key, value) }

Gitlab::Redis::Boolean

Redis에서는 모든 값이 문자열입니다. Gitlab::Redis::Boolean은 불리언 값을 일관되게 인코딩하고 디코딩하도록 보장합니다.

Gitlab::Redis::HLL

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

Gitlab::Redis::HLL은 HyperLogLogs에 값 추가 및 계산을 위한 편리한 인터페이스를 제공합니다.

Gitlab::SetCache

특정 항목이 항목 그룹에 있는지 효율적으로 확인해야 하는 경우, Redis set을 사용할 수 있습니다. Gitlab::SetCacheSISMEMBER 명령어를 사용하는 #include? 메서드와 집합 내 모든 항목을 검색하는 #read를 제공합니다.

이는 브랜치 이름과 같은 리포지토리 데이터를 캐싱하기 위해 RepositorySetCache에서 사용됩니다.

백그라운드 마이그레이션

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

클래스를 만들어 백그라운드 마이그레이션을 수행하려면:

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

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

        def redis
        # 정확한 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