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 서버에 매핑될 수 있습니다. 이는 키가 항상 모든 범주에서 전역적으로 고유해야 함을 의미합니다.

대체로 변경 가능한 식별자(전체 경로가 아닌 프로젝트 ID와 같은)를 사용하는 것이 좋습니다. 전체 경로를 사용하는 경우, 키가 프로젝트 이름이 변경되면 사용되지 않게 됩니다. 키의 내용이 이름 변경으로 만료되는 경우, 키가 변경되는 것이 의존하기보다는 항목을 만료시키는 후크를 포함하는 것이 좋습니다.

다중 키 명령어

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

이는 다중 키 명령어를 수행하는 경우의 추가적인 제한 사항을 부과하는데, 예를 들어 Redis에서 유지되는 두 집합을 비교하는 등의 작업을 수행하는 경우 키는 변경 가능한 부분을 중괄호로 둘러싸는 방식으로 보장해야 합니다. 예를 들면:

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

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

현재 개발 및 테스트 환경에서는 RedisClusterValidator를 통해 이를 검증하고 있으며, 이는 cacheshared_state Redis 인스턴스.에 대해서 활성화되어 있습니다.

개발자들은 Redis Cluster의 더 많은 유형에서의 사용을 위한 향후 채택을 용이하게 하기 위해 적절한 곳에서 해시 태그를 사용하도록 권장받습니다. 예를 들어 Namespace 모델은 구성 캐시 키에 대해 해시 태그를 사용합니다.

다중 키 명령을 수행하기 위해 개발자들은 .pipelined 메서드를 사용하여 각 노드로 명령을 분할하여 보내고 응답을 집계할 수 있습니다. 그러나 이것은 트랜잭션에 대해서는 작동하지 않습니다. 왜냐하면 Redis Cluster는 크로스 슬롯 트랜잭션을 지원하지 않기 때문입니다.

Rails.cache에서는 read_multi_get에서 찾을 수 있는 MGET 명령어를 패치해서 .pipelined 메서드를 사용하여 처리합니다. 파이프라인의 최소 크기는 1000개의 명령어이며, GITLAB_REDIS_CLUSTER_PIPELINE_BATCH_LIMIT 환경 변수를 사용하여 조정할 수 있습니다.

구조화된 로깅에서의 Redis

GitLab 팀원을 위한 참고사항: GitLab.com에서 Redis 구조화된 로깅 필드를 다루는 방법을 보여주는 기본고급 비디오가 있습니다.

우리의 구조화된 로깅은 웹 요청 및 Sidekiq 작업에 대한 호출 횟수, 지속기간, 쓰여진 바이트, 여러 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 Keyspace Analyzer 프로젝트에는 Redis 인스턴스의 전체 키 디렉터리 및 메모리 사용량을 덤프하고 이 디렉터리을 분석하여 결과에서 잠재적으로 민감한 데이터를 제거하는 도구가 포함되어 있습니다. 이를 사용하여 가장 빈번한 키 패턴이나 가장 많은 메모리를 사용하는 키를 찾을 수 있습니다.

현재 이 도구는 GitLab.com Redis 인스턴스에 대해 자동으로 실행되지는 않지만 필요할 때 매뉴얼으로 실행됩니다.

N+1 호출 문제

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

Redis는 주로 캐싱 목적으로 사용됩니다. 보통 캐시 호출은 가벼우며 Redis 인스턴스에 영향을 미치는데 충분한 부하를 생성할 수 없습니다. 그러나 이를 모르고 고가의 캐시 다시 계산을 트리거할 수도 있습니다. 이 도구를 사용하여 Redis 호출을 분석하고 이에 대한 기대하는 한계를 정의하세요.

테스트 작성

이것은 ActiveSupport::Notifications 인스트루먼터로 구현되었습니다.

단일 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 사용을 위한 몇 가지 추가 클래스가 있습니다. 이들은 주로 Redis 사용을 세밀하게 제어하기 위한 것이며, Rails.cache 래퍼와 결합하여 사용되지는 않습니다. ‘Rails.cache’ 또는 이러한 클래스 및 리터럴 Redis 명령 중 하나를 사용할 것입니다.

우리는 미래의 최적화 이점을 누릴 수 있도록 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, PFADD, 및 PFMERGE 명령어는 하이퍼로그로그(HyperLogLogs)를 사용하여 낮은 메모리 사용량으로 고유 요소의 수를 추정하는 데이터 구조에 작용합니다. 더 자세한 정보는 Redis의 HyperLogLogs를 참조하세요.

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

Gitlab::SetCache

항목이 항목 그룹에 효율적으로 있는지 확인해야 하는 경우, Redis 집합을 사용할 수 있습니다. Gitlab::SetCacheSISMEMBER 명령어를 사용하는 #include? 메서드를 제공하며, 집합 내의 모든 항목을 가져오기 위한 #read도 제공합니다.

이것은 브랜치 이름과 같은 리포지터리 데이터를 캐시하기 위해 세트를 사용하기 위한 편리한 방법으로, RepositorySetCache 에서 사용됩니다.

백그라운드 마이그레이션

Redis 기반의 마이그레이션은 SCAN 명령어를 사용하여 특정 키 패턴을 위해 전체 Redis 인스턴스를 스캔하기를 포함합니다. 큰 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