Redis 개발 가이드라인
GitLab v19.1GitLab은 Redis를 다음과 같은 여러 목적으로 사용합니다: 캐싱 (주로 Rails.cache를 통해). Sidekiq을 사용한 job 처리 큐. ActionCable의 Pub/Sub 큐 백엔드. 대부분의 환경(GDK 포함)에서는 이 모든 목적이 동일한 Redis 인스턴스를 가리킵니다.
Redis 인스턴스#
GitLab은 Redis를 다음과 같은 여러 목적으로 사용합니다:
-
캐싱 (주로
Rails.cache를 통해). -
Sidekiq을 사용한 job 처리 큐.
-
공유 애플리케이션 상태 관리.
-
CI 트레이스 청크 저장.
-
ActionCable의 Pub/Sub 큐 백엔드.
-
레이트 리밋 상태 저장.
-
세션.
대부분의 환경(GDK 포함)에서는 이 모든 목적이 동일한 Redis 인스턴스를 가리킵니다.
GitLab.com에서는 별도의 Redis 인스턴스를 사용합니다. 설정에 대한 자세한 내용은 Redis SRE 가이드를 참조하세요.
모든 애플리케이션 프로세스는 동일한 Redis 서버를 사용하도록 구성되어 있으므로, PostgreSQL이 적합하지 않은 경우 프로세스 간 통신에 Redis를 사용할 수 있습니다. 예를 들어, 읽기보다 쓰기 횟수가 훨씬 많은 일시적인 상태나 데이터가 이에 해당합니다.
Geo가 활성화된 경우, 각 Geo 사이트는 독립적인 자체 Redis 데이터베이스를 갖습니다.
새 Redis 인스턴스 추가에 관한 개발 문서가 있습니다.
키 명명#
Redis는 계층 구조 없이 평면 네임스페이스를 사용하므로, 충돌을 방지하기 위해 키 이름에 주의를 기울여야 합니다. 일반적으로 애플리케이션 수준에서 구조처럼 보이도록 콜론으로 구분된 요소를 사용합니다. 예를 들어 projects:1:somekey와 같은 형태입니다.
Redis 사용 목적을 별도의 카테고리로 분리하고, 이를 GitLab.com과 같은 고가용성 구성에서 별도의 Redis 서버에 매핑할 수 있지만, 기본 Omnibus 및 GDK 설정은 단일 Redis 서버를 공유합니다. 즉, 키는 모든 카테고리에 걸쳐 항상 전역적으로 고유해야 합니다.
Redis 키 이름에는 전체 경로보다 변경 불가능한 식별자(예: 전체 경로 대신 프로젝트 ID)를 사용하는 것이 일반적으로 더 좋습니다. 전체 경로를 사용하면 프로젝트 이름이 변경될 때 해당 키가 참조되지 않게 됩니다. 키의 내용이 이름 변경으로 인해 무효화되는 경우에는, 키 변경에 의존하는 것보다 항목을 만료시키는 훅을 포함하는 것이 더 좋습니다.
멀티 키 명령#
GitLab은 에픽 878에서 도입된 캐시 관련 워크로드 유형에 Redis Cluster를 지원합니다.
이로 인해 명명에 추가적인 제약이 생깁니다: GitLab이 여러 키를 동일한 Redis 서버에 보유해야 하는 작업(예: Redis에 보관된 두 세트를 비교하는 경우)을 수행할 때, 키의 변경 가능한 부분을 중괄호로 묶어 이를 보장해야 합니다. 예를 들어:
project:{1}:set_a
project:{1}:set_b
project:{2}:set_c
set_a와 set_b는 동일한 Redis 서버에 보관되도록 보장되지만, set_c는 그렇지 않습니다.
현재 개발 및 테스트 환경에서는 RedisClusterValidator로 이를 검증하며, 이는 cache와 shared_state Redis 인스턴스에 대해 활성화되어 있습니다.
더 많은 Redis 유형에서 Redis Cluster를 향후 채택하기 쉽도록, 개발자는 적절한 경우 해시 태그를 사용하도록 강력히 권장됩니다. 예를 들어, Namespace 모델은 config 캐시 키에 해시 태그를 사용합니다.
멀티 키 명령을 수행하려면, 개발자는 각 노드에 명령을 분할하여 전송하고 응답을 집계하는 .pipelined 메서드를 사용할 수 있습니다.
그러나 Redis Cluster는 크로스 슬롯 트랜잭션을 지원하지 않기 때문에 트랜잭션에는 사용할 수 없습니다.
Rails.cache의 경우, read_multi_get에서 발견되는 MGET 명령을 .pipelined 메서드를 사용하도록 패치하여 처리합니다.
파이프라인의 최소 크기는 1000개 명령으로 설정되어 있으며, GITLAB_REDIS_CLUSTER_PIPELINE_BATCH_LIMIT 환경 변수를 사용하여 조정할 수 있습니다.
구조화된 로깅에서의 Redis#
GitLab 팀원을 위해: GitLab.com에서 Redis 구조화 로깅 필드를 다루는 방법을 보여주는 기본 및 고급 영상이 있습니다.
웹 요청 및 Sidekiq job에 대한 구조화된 로깅에는 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 내부)이 GitLab.com에 있습니다.
GitLab.com에서 Redis 슬로우 로그의 항목은 redis.slowlog 태그와 함께 pubsub-redis-inf-gprd* 인덱스에서 사용할 수 있습니다.
이를 통해 오래 걸린 명령을 확인할 수 있으며, 성능 문제의 원인이 될 수 있습니다.
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 'avoids N+1 Redis calls' do
control = RedisCommands::Recorder.new { visit_page }
expect(control.count).to eq(1)
end
또는 특정 Redis 호출 횟수를 검증하는 테스트:
it 'avoids N+1 sadd Redis calls' do
control = RedisCommands::Recorder.new { visit_page }
expect(control.by_command(:sadd).count).to eq(1)
end
특정 Redis 호출만 캡처하도록 패턴을 제공할 수도 있습니다:
it 'avoids N+1 Redis calls to forks_count key' do
control = RedisCommands::Recorder.new(pattern: 'forks_count') { visit_page }
expect(control.count).to eq(1)
end
또한 특수 매처 exceed_redis_calls_limit와 exceed_redis_command_calls_limit를 사용하여 Redis 호출 횟수의 상한선을 정의할 수 있습니다:
it 'avoids N+1 Redis calls' do
control = RedisCommands::Recorder.new { visit_page }
expect(control).not_to exceed_redis_calls_limit(1)
end
it 'avoids N+1 sadd Redis calls' do
control = RedisCommands::Recorder.new { visit_page }
expect(control).not_to exceed_redis_command_calls_limit(:sadd, 1)
end
이러한 테스트는 Redis 호출과 관련된 N+1 문제를 식별하고, 수정이 예상대로 작동하는지 확인하는 데 도움이 됩니다.
참고#
캐싱#
Rails.cache에서 사용하는 Redis 인스턴스는 일반적으로 LRU 방식으로 키 제거 정책을 구성할 수 있으며, 메모리 한도에 도달하면 "가장 최근에 사용되지 않은" 캐시 항목이 제거(삭제)됩니다.
GitLab.com의 Rails.cache에서 사용하는 Redis 인스턴스 redis-cluster-cache에 대한 키 제거 구성을 참조하세요. 이 Redis 인스턴스는 최대 메모리 한도에 도달하지 않아야 합니다. maxmemory에서의 키 제거는 제거가 진행되는 동안 지연 시간이 증가하는 비용이 발생하기 때문입니다. 자세한 내용은 이 이슈를 참조하세요. redis-cluster-cache의 현재 메모리 사용량을 확인하세요.
이 캐시의 데이터는 설정된 만료 시간보다 일찍 사라질 수 있으므로, 진정으로 캐시처럼 사용하고 일시적인 데이터에만 Rails.cache를 사용하세요.
Redis에 캐시가 아닌 방식으로 안정적으로 지속되어야 하는 데이터에는 Gitlab::Redis::SharedState를 사용할 수 있습니다.
유틸리티 클래스#
특정 사용 사례를 돕기 위한 추가 클래스들이 있습니다. 이는 주로 Redis 사용을 세밀하게 제어하기 위한 것이므로, Rails.cache 래퍼와 함께 사용하지 않습니다: Rails.cache 또는 이러한 클래스와 리터럴 Redis 명령 중 하나를 사용합니다.
Rails에 향후 적용될 최적화의 이점을 누리기 위해 Rails.cache를 사용하는 것을 선호합니다. Ruby 객체는 Redis에 쓸 때 마샬링되므로, 너무 큰 객체나 신뢰할 수 없는 사용자 입력을 저장하지 않도록 주의해야 합니다.
일반적으로 다음 중 하나 이상이 해당될 때만 이러한 클래스를 사용합니다:
-
캐시가 아닌 Redis 인스턴스의 데이터를 조작하려는 경우.
-
Rails.cache가 수행하려는 작업을 지원하지 않는 경우.
Gitlab::Redis::{Cache,SharedState,Queues}#
이러한 클래스는 (Gitlab::Redis::Wrapper를 사용하여) Redis 인스턴스를 래핑하여 직접 작업하기 편리하게 합니다. 일반적인 사용 방법은 클래스에서 .with를 호출하는 것이며, 이는 Redis 연결을 yield하는 블록을 받습니다. 예를 들어:
# Get the value of `key` from the shared state (persistent) Redis
Gitlab::Redis::SharedState.with { |redis| redis.get(key) }
# Check if `value` is a member of the set `key`
Gitlab::Redis::Cache.with { |redis| redis.sismember(key, value) }
Gitlab::Redis::Cache는 Rails.cache와 동일한 Redis 인스턴스를 공유하므로, 구성된 경우 키 제거 정책을 가집니다. 이 클래스는 진정으로 캐시처럼 사용하는 데이터, 즉 없을 경우 재생성할 수 있는 데이터에 사용하세요.
이 클래스를 사용할 때는 키에 항상 TTL을 설정하세요. Rails.cache의 기본 TTL이 8시간인 것과 달리, 이 클래스는 기본 TTL을 설정하지 않습니다. 일반적인 캐싱에는 8시간 TTL을 사용하는 것을 고려하세요. 이는 하루 근무 시간과 일치하며, 사용자가 동일한 콘텐츠에 대해 하루에 한 번만 캐시 미스를 겪게 됩니다.
캐시에 큰 워크로드를 추가할 것으로 예상되거나 프로덕션 영향이 확실하지 않은 경우, #g_durability에 문의하세요.
Gitlab::Redis::SharedState는 키 제거 정책이 구성되지 않습니다.
재생성할 수 없고 설정된 만료 시간까지 지속될 것으로 예상되는 데이터에 이 클래스를 사용하세요.
또한 키에 기본 TTL을 설정하지 않으므로, 이 클래스를 사용할 때는 거의 항상 키에 TTL을 설정해야 합니다.
Gitlab::Redis::Boolean#
Redis에서 모든 값은 문자열입니다.
Gitlab::Redis::Boolean은 불리언 값이 일관되게 인코딩 및 디코딩되도록 합니다.
Gitlab::Redis::HLL#
Redis PFCOUNT, PFADD, PFMERGE 명령은 적은 메모리 사용으로 고유 요소의 수를 추정할 수 있는 데이터 구조인 HyperLogLog에서 작동합니다. 자세한 내용은 Redis의 HyperLogLog를 참조하세요.
Gitlab::Redis::HLL은 HyperLogLog에서 값을 추가하고 계산하는 편리한 인터페이스를 제공합니다.
Gitlab::SetCache#
항목이 그룹에 속하는지 효율적으로 확인해야 하는 경우 Redis 세트를 사용할 수 있습니다.
Gitlab::SetCache는 SISMEMBER 명령을 사용하는 #include? 메서드와 세트의 모든 항목을 가져오는 #read를 제공합니다.
이는 브랜치 이름과 같은 리포지터리 데이터를 캐싱하는 데 세트를 편리하게 사용할 수 있도록 RepositorySetCache에서 사용됩니다.
백그라운드 마이그레이션#
Redis 기반 마이그레이션은 SCAN 명령을 사용하여 특정 키 패턴을 찾기 위해 전체 Redis 인스턴스를 스캔합니다.
대규모 Redis 인스턴스의 경우, 마이그레이션이 일반 또는 배포 후 마이그레이션의 시간 제한을 초과할 수 있습니다. RedisMigrationWorker는 장기 실행 Redis 마이그레이션을 백그라운드 마이그레이션으로 수행합니다.
클래스를 만들어 백그라운드 마이그레이션을 수행하려면:
module Gitlab
module BackgroundMigration
module Redis
class BackfillCertainKey
def perform(keys)
# implement logic to clean up or backfill keys
end
def scan_match_pattern
# define the match pattern for the `SCAN` command
end
def redis
# define the exact Redis instance
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