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 Cluster를 지원하며, 이는 에픽 878에서 소개되었습니다.

이는 이름에 추가적인 제약을 부과합니다: 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 Cluster의 미래적인 채택을 용이하게 하기 위해 적절한 곳에서 해시태그를 사용하도록 권장합니다. 예를 들어, Namespace 모델은 구성 캐시 키에 해시태그를 사용합니다.

멀티 키 명령을 수행하려면, 개발자들은 각 노드에 명령을 분할하고 응답을 집계하는 .pipelined 메서드를 사용할 수 있으며, 이는 트랜잭션에는 사용할 수 없는데, Redis Cluster가 크로스 슬롯 트랜잭션을 지원하지 않기 때문입니다.

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를 내림차순으로 정렬하기만 하면 됩니다.

느린 로그

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

GitLab.com에서는 Redis 느린 로그 항목이 pubsub-redis-inf-gprd* 색인에 redis.slowlog 태그에 사용 가능합니다. 이는 오랜 시간이 걸리는 명령어를 보여주고 성능에 영향을 줄 수 있는 문제일 수 있습니다.

fluent-plugin-redis-slowlog 프로젝트는 Redis의 slowlog 항목을 가져와 Fluentd(및 궁극적으로 Elasticsearch)에 전달하는 역할을 합니다.

전체 키스페이스 분석

Redis 키스페이스 분석기 프로젝트에는 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 문제를 식별하는 데 도움을 줄 뿐 아니라 그에 대한 수정이 의도한 대로 작동하는지 확인합니다.

참고

유틸리티 클래스

특정 용도로 도움이 되는 몇 가지 추가 클래스가 있습니다. 이들은 대부분 Redis 사용을 세부적으로 제어하기 위한 것으로, Rails 캐시 래퍼와 함께 사용되지 않습니다: Rails 캐시 또는 이러한 클래스 및 리터럴 Redis 명령어 중 하나만 사용합니다.

우리는 미래의 Rails에 대한 최적화 혜택을 누리기 위해 Rails.cache를 선호합니다. Ruby 객체는 Redis에 기록될 때 마샬링됩니다, 따라서 거대한 객체나 믿을 수 없는 사용자 입력을 저장하지 않도록 주의해야 합니다.

일반적으로 우리는 다음 중 적어도 하나일 때에만 이러한 클래스를 사용할 것입니다:

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

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

이러한 클래스들은 Redis 인스턴스를 래핑하기 위해 Gitlab::Redis::Wrapper를 사용하여 직접 작업할 수 있도록 편리하게 만듭니다. 일반적으로 클래스에 대한 .with 호출 후 Redis 연결을 제공하는 블록을 취합니다. 예를 들면:

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

# `value`가 `key` 집합의 멤버인지 확인합니다
Gitlab::Redis::Cache.with { |redis| redis.sismember(key, value) }

Gitlab::Redis::Boolean

Redis에서 모든 값은 문자열입니다. Gitlab::Redis::Boolean 는 부욜린을 일관되게 인코딩하고 디코딩합니다.

Gitlab::Redis::HLL

Redis의 PFCOUNT, PFADDPFMERGE 명령은 하이퍼로그로그(HyperLogLogs)를 다루는데, 이는 적은 메모리 사용량으로 고유 요소의 개수를 추정하는 데이터 구조입니다. 더 많은 정보는 Redis에서의 HyperLogLogs에서 확인할 수 있습니다.

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

Gitlab::SetCache

특정 항목이 항목 그룹에 있는지 효율적으로 확인해야 하는 경우, Redis 집합을 사용할 수 있습니다. 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