Redis 실무 사례 아카이브
Redis는 캐싱, 세션 관리, Rate Limiting, 실시간 데이터 처리 등 다양한 용도로 활용되는 인메모리 데이터 스토어다. 단순한 캐시로 도입했더라도 서비스가 성장하면 Sentinel/Cluster 마이그레이션, 데이터 분리, Eviction 정책 최적화, Thundering Herd 대응 등 복잡한 운영 과제에 직면하게 된다. 이 글에서는 Grab, GitHub, Trivago, Discord, Stripe 등 글로벌 기업들이 프로덕션 환경에서 겪은 Redis 운영 사례를 정리한다. 각 사례는 상황, 문제, 해결, 교훈 순서로 구성되어 있으며, 출처 URL을 통해 원문을 직접 확인하는 것을 권장한다.
1. Grab의 Redis Sentinel에서 Cluster로의 마이그레이션
- 출처: Moving from Redis Sentinel to Redis Cluster at Grab
- 저자/출처: Grab Engineering Team
1.1 상황
Grab은 동남아시아 최대 슈퍼앱으로, 차량 호출, 음식 배달, 결제 등 다양한 서비스를 하나의 플랫폼에서 제공한다. Redis는 이 생태계에서 세션 관리, 캐싱, 실시간 데이터 처리, Rate Limiting 등 주요 인프라 컴포넌트로 사용되고 있었다. 초기에는 Redis Sentinel 구성으로 고가용성을 확보했으며, 1개의 마스터와 2개의 레플리카, 3개의 Sentinel 노드로 구성된 전통적인 HA 아키텍처를 운영했다.
그러나 서비스가 동남아 8개국으로 급속히 확장되면서 트래픽이 폭발적으로 증가했다. 출퇴근 시간대, 식사 시간대에 트래픽이 집중되는 피크 패턴이 뚜렷했고, 프로모션 이벤트 기간에는 평소의 수 배에 달하는 요청이 몰렸다. 이러한 상황에서 단일 마스터 노드의 쓰기 처리량 한계가 서비스 성장의 병목으로 작용하기 시작했다.
1.2 문제
Redis Sentinel 아키텍처의 근본적인 제약은 모든 쓰기 연산이 단일 마스터 노드에 집중된다는 점이다. 레플리카 노드는 읽기 부하 분산에만 활용할 수 있으므로, 쓰기 처리량은 단일 노드의 CPU, 메모리, 네트워크 대역폭에 의해 결정된다. Grab의 규모에서는 수직 확장(scale-up)만으로 이 한계를 극복하기 어려웠다.
Failover 과정에서도 문제가 있었다. Sentinel이 마스터 장애를 감지하고 새로운 마스터를 선출하는 과정에서 수 초에서 수십 초의 서비스 중단이 발생했다. 이 기간 동안 클라이언트가 기존 마스터에 계속 연결을 시도하면서 요청 실패가 누적되었다. Sentinel 프로토콜을 올바르게 구현하지 않은 클라이언트 라이브러리를 사용하는 서비스에서는 마스터 변경 알림을 제때 수신하지 못해 복구가 더욱 지연되었다.
네트워크 파티션 시나리오에서는 split-brain 위험도 존재했다. Sentinel 노드 간 통신이 단절될 경우, 각 파티션에서 서로 다른 마스터를 선출하여 데이터 불일치가 발생할 수 있었다. 특히 Grab처럼 결제와 직접 연관된 데이터를 Redis에 저장하는 경우, 이런 데이터 불일치는 심각한 비즈니스 문제로 이어질 수 있었다.
1.3 해결
Grab 팀은 Redis Cluster로의 마이그레이션을 단계적이고 안전하게 진행했다. 이 마이그레이션은 단순한 기술 전환이 아니라, 애플리케이션 코드, 클라이언트 라이브러리, 운영 체계 전반에 걸친 대규모 변경이었다.
1단계: 키 설계 재검토와 해시슬롯 분산 전략 수립
Redis Cluster는 16,384개의 해시슬롯을 마스터 노드들에 분산 배치한다. 기존에 MGET, MSET 같은 멀티키 연산이나 Lua 스크립트에서 여러 키를 참조하는 경우, 해당 키들이 동일한 해시슬롯에 위치해야 한다. Grab 팀은 기존 키 설계를 분석하여 해시태그({prefix})를 도입했다. 예를 들어 user:{userId}:session과 user:{userId}:cart 같이 동일 사용자의 관련 키를 같은 슬롯에 배치함으로써 멀티키 연산 호환성을 확보했다.
2단계: 듀얼 라이트(Dual-Write) 전략
마이그레이션 기간 동안 데이터 정합성을 보장하기 위해, 모든 쓰기 요청을 기존 Sentinel 클러스터와 새로운 Redis Cluster에 동시에 수행하는 듀얼 라이트 전략을 적용했다. 읽기 요청은 초기에 Sentinel에서 처리하다가, 데이터 정합성 검증이 완료된 서비스부터 단계적으로 Redis Cluster로 전환했다. 이 과정에서 양쪽 데이터를 주기적으로 비교하는 검증 파이프라인도 구축했다.
3단계: 클라이언트 라이브러리 업그레이드
Redis Cluster는 MOVED/ASK 리디렉션, 해시슬롯 매핑 캐싱 등 Sentinel과는 다른 프로토콜을 사용한다. 각 서비스에서 사용하는 Redis 클라이언트 라이브러리가 Cluster 프로토콜을 올바르게 지원하는지 검증했고, 필요한 경우 라이브러리 버전을 업그레이드하거나 래퍼(wrapper)를 추가했다. Go 기반 서비스에서는 go-redis 라이브러리의 Cluster 모드를, Java 기반 서비스에서는 Lettuce의 Cluster 지원을 활용했다.
4단계: 점진적 트래픽 전환과 롤백 준비
트래픽 전환은 서비스 중요도가 낮은 것부터 시작하여 주요 서비스 순으로 진행했다. 각 전환 단계마다 성능 메트릭(지연시간, 처리량, 에러율)을 면밀히 모니터링했고, 문제 발생 시 즉시 Sentinel로 롤백할 수 있는 자동화된 스위치 메커니즘을 구비했다.
1.4 주요 교훈
- Sentinel은 읽기 확장에는 적합하지만, 쓰기 확장이 필요한 경우 Cluster 전환이 불가피하다. Sentinel의 아키텍처적 한계(단일 마스터 쓰기)는 수직 확장만으로 극복할 수 없다.
- 대규모 마이그레이션은 반드시 듀얼 라이트 같은 안전장치를 두고 단계적으로 진행해야 한다. 빅뱅 방식의 전환은 롤백이 어렵고 리스크가 크다.
- 클라이언트 라이브러리의 Cluster 지원 여부와 성숙도를 사전에 반드시 확인해야 한다. MOVED/ASK 리디렉션 처리, 슬롯 매핑 캐시 갱신, 재연결 로직 등 Cluster 고유의 프로토콜 처리가 올바르게 구현되어 있는지 검증이 필요하다.
- 해시태그(
{prefix})를 활용한 키 설계가 Cluster 환경에서 멀티키 연산을 가능하게 한다. 초기 키 설계 단계부터 Cluster 호환성을 고려해야 나중에 마이그레이션 비용을 줄일 수 있다. - 마이그레이션 전에 기존 코드에서 Cluster 비호환 명령어(예:
KEYS, 크로스슬롯 트랜잭션 등)를 사용하는 부분을 사전에 식별하고 수정해야 한다.
2. GitHub의 Redis에서 영속 데이터 분리
- 출처: Moving persistent data out of Redis
- 저자/출처: GitHub Engineering Team
2.1 상황
GitHub은 세계 최대 코드 호스팅 플랫폼으로, 수억 개의 리포지토리와 수천만 명의 사용자에게 서비스를 제공한다. GitHub의 인프라에서 Redis는 오랜 기간에 걸쳐 다양한 용도로 활용 범위가 넓어졌다. 초기에는 캐싱 목적으로 도입되었으나, 시간이 지나면서 백그라운드 잡 큐(Resque/Sidekiq), 실시간 기능(Presence, WebSocket 상태 관리), Feature Flag 상태 저장, Rate Limiting 카운터, 그리고 다양한 애플리케이션 상태 데이터까지 Redis에 저장하게 되었다.
문제의 핵심은 이러한 데이터들의 성격이 근본적으로 달랐다는 것이다. 캐시 데이터는 언제든 소실되어도 원본에서 재생성할 수 있는 휘발성 데이터인 반면, Feature Flag 상태나 Rate Limiting 카운터, 잡 큐 데이터 등은 소실되면 서비스 동작에 직접적인 영향을 미치는 영속성 데이터(persistent data)였다. 이 두 종류의 데이터가 동일한 Redis 인스턴스에 혼재된 채 운영되고 있었다.
2.2 문제
캐시 데이터와 영속 데이터의 혼재는 여러 가지 운영 문제를 야기했다.
Eviction 충돌
Redis의 maxmemory-policy는 인스턴스 전체에 동일하게 적용된다. 캐시 용도로 allkeys-lru를 설정하면 메모리 압박 시 가장 오래 사용되지 않은 키부터 제거되는데, 이때 사용 빈도가 낮지만 반드시 유지되어야 하는 영속 데이터(예: 특정 Feature Flag 설정값)가 함께 삭제될 수 있었다. 반대로 영속 데이터 보호를 위해 noeviction을 설정하면, 메모리가 가득 찼을 때 캐시 데이터 포함 모든 쓰기 요청이 실패하여 서비스 전체에 장애가 전파되었다.
메모리 사용량 예측 불가
캐시 데이터는 요청 패턴에 따라 급격히 증가할 수 있었고, 영속 데이터는 서비스 성장에 따라 꾸준히 증가했다. 이 두 가지 성장 패턴이 겹치면서 메모리 사용량을 예측하고 용량을 계획하는 것이 매우 어려웠다.
재시작/복구 시간 증가
Redis의 RDB 스냅샷과 AOF 로그 크기가 커지면서, 노드 재시작 시 데이터 로딩에 걸리는 시간이 길어졌다. 이로 인해 계획된 유지보수뿐 아니라 장애 상황에서의 복구 시간(MTTR)도 증가했다. 대용량 데이터셋은 레플리카 동기화(full resync)에도 상당한 시간이 소요되어 failover 복구를 지연시켰다.
운영 복잡도 증가
하나의 Redis 인스턴스가 여러 역할을 수행하면서, 설정 최적화가 어려웠다. 캐시 최적화와 영속 데이터 안정성 요구가 서로 상충하는 상황이 빈번했다.
2.3 해결
GitHub 팀은 Redis에서 영속 데이터를 체계적으로 분리하는 프로젝트를 진행했다.
1단계: 데이터 분류와 의존성 매핑
먼저 Redis에 저장된 모든 키 패턴을 분석하여 데이터를 분류했다. 각 키 패턴이 어떤 서비스에서 사용되는지, 데이터 소실 시 어떤 영향이 있는지(캐시 미스로 재생성 가능 vs 서비스 장애 발생), TTL이 설정되어 있는지 등을 기준으로 영속 데이터와 휘발성 데이터를 구분했다.
2단계: 영속 데이터의 적합한 저장소로 이관
영속성이 필요한 데이터를 각 특성에 맞는 전용 저장소로 이관했다. 관계형 데이터는 MySQL로, 구조화된 설정 데이터는 전용 설정 관리 시스템으로 옮겼다. 이관 과정에서는 기존 Redis 키를 읽고 새 저장소에 쓰는 마이그레이션 스크립트를 작성하고, 전환 기간 동안 양쪽에서 읽기가 가능하도록 호환 계층을 두었다.
3단계: Redis 역할 한정과 인스턴스 분리
이관 완료 후, Redis의 역할을 캐시와 단기 데이터(세션, 임시 상태) 저장으로 명확히 한정했다. 용도별로 별도의 Redis 인스턴스를 분리하여 eviction 정책을 독립적으로 설정할 수 있게 했다. 캐시 전용 인스턴스에는 allkeys-lru를, 잡 큐 전용 인스턴스에는 noeviction을 설정하여 워크로드 간 간섭을 방지했다.
4단계: 모니터링 강화
인스턴스별 메모리 사용 패턴, eviction 발생 빈도, 키 수 변화 추이 등을 상세히 모니터링할 수 있는 대시보드를 구축했다. 메모리 사용량 임계값에 대한 알람을 설정하여 문제가 발생하기 전에 선제적으로 대응할 수 있게 했다.
2.4 주요 교훈
- Redis를 만능 저장소로 사용하면 안 된다. 캐시와 영속 데이터는 반드시 분리해야 한다. “Redis가 빠르니까”라는 이유로 모든 데이터를 Redis에 넣는 것은 기술 부채를 축적하는 것이다.
maxmemory-policy설정이 인스턴스 내 모든 키에 동일하게 적용되므로, 서로 다른 보존 요구사항을 가진 데이터가 공존하면 어떤 정책을 선택하든 문제가 발생한다.- 영속 데이터는 RDBMS나 전용 저장소로 이관하고, Redis는 캐시/세션 등 휘발성 데이터 전용으로 활용하는 것이 바람직하다. 데이터의 성격에 맞는 저장소를 선택하는 것이 인프라 설계의 기본 원칙이다.
- 재시작 시간과 레플리카 동기화 시간을 줄이려면 데이터셋 크기를 관리 가능한 수준으로 유지해야 한다. 대용량 데이터셋은 MTTR을 증가시켜 전체 시스템의 가용성을 저하시킨다.
- 기술 도입 초기에 데이터 성격별 분리 원칙을 세우지 않으면, 나중에 분리하는 비용이 기하급수적으로 증가한다. Redis 신규 도입 시점부터 용도별 인스턴스 분리를 원칙으로 삼아야 한다.
3. Trivago의 Redis Sentinel 운영 경험과 Failover 문제
- 출처: Redis Sentinel at Trivago
- 저자/출처: Trivago Tech Team
3.1 상황
Trivago는 전 세계 호텔 가격을 비교하는 메타 검색 엔진으로, 매일 수백만 건의 호텔 검색 요청을 처리한다. 검색 결과 캐싱, 세션 관리, 실시간 가격 데이터 임시 저장 등에 Redis를 주요 캐싱 레이어로 활용하고 있었다. 호텔 검색 서비스의 특성상 캐시 가용성이 서비스 응답 시간에 직접적인 영향을 미치기 때문에, Redis의 고가용성 확보는 필수적인 과제였다.
이를 위해 Redis Sentinel을 도입했다. Sentinel은 마스터 노드의 상태를 감시하고, 장애 발생 시 자동으로 레플리카를 새 마스터로 승격시키는 자동 failover 기능을 제공한다. 그러나 Sentinel을 프로덕션에 배포한 이후, 예상치 못한 다양한 운영 문제들이 발생했다.
3.2 문제
불필요한 Failover 발생 (False Positive)
Sentinel의 down-after-milliseconds 값이 너무 낮게 설정되어 있었다. 이 값은 Sentinel이 마스터 노드를 “주관적 다운(SDOWN)” 상태로 판단하기까지의 응답 대기 시간이다. 네트워크에 일시적인 지연(micro-burst, GC pause 등)이 발생해도 마스터가 정상 동작 중임에도 불구하고 Sentinel이 다운으로 판단하여 불필요한 failover를 트리거했다. 불필요한 failover는 그 자체로 수 초간의 서비스 중단을 유발하므로, 오히려 가용성을 저해하는 역설적인 상황이 발생했다.
클라이언트의 마스터 주소 갱신 지연
Failover가 완료되어 새로운 마스터가 선출되더라도, 클라이언트 애플리케이션이 새 마스터의 IP 주소를 즉시 인식하지 못하는 문제가 있었다. 일부 클라이언트 라이브러리는 Sentinel의 +switch-master pub/sub 메시지를 구독하여 마스터 변경을 감지하는 반면, 다른 라이브러리는 주기적인 폴링 방식으로 Sentinel에 마스터 주소를 질의했다. 폴링 주기에 따라 수 초에서 수십 초까지 구 마스터에 연결을 시도하며 에러가 발생했다.
Sentinel과 Redis 서버의 동일 머신 배치
운영 편의를 위해 Sentinel 프로세스를 Redis 서버와 동일한 물리 머신에 배치했는데, 이는 치명적인 설계 오류였다. Redis 마스터 노드가 위치한 머신에 하드웨어 장애가 발생하면, 해당 머신의 Sentinel도 함께 중단되었다. 이로 인해 장애를 감지해야 할 Sentinel 노드 자체가 사라지면서 쿼럼(quorum)을 충족하지 못해 자동 failover가 불가능해지는 상황이 발생했다.
네트워크 파티션과 쿼럼 문제
데이터센터 내 네트워크 토폴로지가 변경되면서 Sentinel 노드 간 통신이 간헐적으로 단절되는 현상이 발생했다. Sentinel의 failover는 쿼럼 합의 기반으로 동작하므로, 네트워크 파티션으로 인해 쿼럼이 형성되지 않으면 failover가 지연되거나 실패했다.
3.3 해결
Trivago 팀은 다음과 같은 조치를 통해 Sentinel 운영을 안정화했다.
타임아웃 값의 현실적 튜닝
down-after-milliseconds 값을 프로덕션 네트워크의 실제 지연 특성을 기반으로 조정했다. 네트워크 모니터링 데이터를 분석하여 정상 상태에서의 최대 지연시간을 측정하고, 이보다 충분히 높은 값으로 설정하여 false positive를 제거했다. failover-timeout 값도 함께 조정하여 failover 과정이 안정적으로 완료될 수 있는 시간을 확보했다.
Sentinel 노드의 물리적 분리
Sentinel 노드를 Redis 서버와 완전히 분리된 별도의 머신에 배치했다. 최소 3개의 Sentinel 노드를 서로 다른 물리 서버(가능하면 다른 랙 또는 가용 영역)에 분산 배치하여, 단일 머신이나 네트워크 세그먼트 장애가 Sentinel 쿼럼에 영향을 미치지 않도록 했다.
클라이언트 측 Sentinel 통합 개선
애플리케이션의 Redis 클라이언트 라이브러리를 Sentinel pub/sub 채널을 구독하는 방식으로 통일하여, 마스터 변경 이벤트를 실시간으로 감지하도록 개선했다. 또한 연결 풀 관리 로직을 보강하여 failover 발생 시 기존 연결을 신속하게 폐기하고 새 마스터에 재연결하도록 했다.
정기적 Failover 테스트
프로덕션과 동일한 구성의 스테이징 환경에서 정기적으로 의도적 failover를 수행하여, Sentinel 설정과 클라이언트 동작을 검증하는 프로세스를 도입했다. Chaos Engineering 접근을 통해 네트워크 파티션, 노드 다운 등 다양한 장애 시나리오를 시뮬레이션했다.
3.4 주요 교훈
- Sentinel 노드는 반드시 Redis 서버와 물리적으로 분리된 환경에 배치해야 한다. 감시 대상과 감시자가 같은 장애 도메인에 있으면 감시 기능 자체가 무력화된다.
down-after-milliseconds,failover-timeout등 타임아웃 값은 실제 네트워크 환경의 지연 특성을 측정한 후 튜닝해야 한다. 너무 공격적인 설정은 불필요한 failover를 유발하여 오히려 가용성을 저하시킨다.- 클라이언트 라이브러리가 Sentinel 프로토콜을 올바르게 구현하고 있는지 사전 검증이 필요하다. pub/sub 기반 마스터 변경 감지, 재연결 로직, 연결 풀 갱신 등이 올바르게 동작하는지 확인해야 한다.
- Sentinel 쿼럼 설정이 네트워크 파티션 시나리오에서 올바르게 동작하는지 장애 테스트를 수행해야 한다. 장애 테스트 없이 Sentinel을 프로덕션에 배포하는 것은 위험하다.
- Sentinel은 “설정하고 잊어버리면 되는” 도구가 아니다. 프로덕션 환경의 변화(네트워크 토폴로지 변경, 트래픽 패턴 변화 등)에 맞춰 지속적으로 설정을 검토하고 장애 테스트를 반복해야 한다.
4. Discord의 메시지 스토리지 아키텍처와 캐시 전략
- 출처: How Discord Stores Trillions of Messages
- 저자/출처: Discord Engineering Team
참고: 이 사례의 원문은 주로 Discord의 메시지 데이터베이스를 Cassandra에서 ScyllaDB로 마이그레이션한 내용을 다루고 있다. Redis/캐시 관련 내용은 원문에서 부분적으로 언급되는 내용이며, 아래의 Thundering Herd 대응 전략은 이 글과 Discord의 다른 엔지니어링 공유 자료들을 기반으로 정리한 것이다. 원문에서 직접 확인되지 않는 세부 내용이 포함되어 있을 수 있으므로, 반드시 원문을 직접 참고하기 바란다.
4.1 상황
Discord는 실시간 음성/텍스트 커뮤니케이션 플랫폼으로, 2023년 기준으로 수조 개의 메시지를 저장하고 있다. Discord의 아키텍처에서 메시지 데이터는 데이터베이스(원래 Cassandra, 이후 ScyllaDB로 마이그레이션)에 영속 저장되며, 읽기 성능 최적화를 위해 캐싱 레이어가 활용된다.
Discord의 트래픽 패턴에는 독특한 특성이 있다. 대형 서버(길드)의 공지 채널에 새 메시지가 올라오면, 수십만에서 수백만 명의 사용자가 거의 동시에 해당 채널을 조회한다. 또한 인기 게임 출시, 대규모 이벤트 등 특정 시점에 특정 채널로 트래픽이 극단적으로 집중되는 “핫스팟” 패턴이 빈번하다.
원문에서 Discord가 Cassandra에서 ScyllaDB로 마이그레이션하게 된 배경에는 데이터 규모 증가에 따른 성능 문제가 있었다. Cassandra에서 핫 파티션 문제, GC pause로 인한 지연 급증, 컴팩션 과정의 읽기 성능 저하 등이 발생했다. ScyllaDB는 C++로 작성되어 JVM 기반의 Cassandra보다 GC 문제에서 자유로웠고, shard-per-core 아키텍처로 더 예측 가능한 성능을 제공했다.
4.2 문제
데이터베이스 핫 파티션과 캐시의 역할
Cassandra 운영 시절, 인기 채널의 메시지 데이터가 특정 파티션에 집중되면서 핫 파티션 문제가 발생했다. 하나의 파티션에 읽기 요청이 집중되면 해당 노드의 CPU와 디스크 I/O가 포화되어 응답 지연이 급증했다. 캐싱 레이어는 이러한 읽기 부하를 흡수하는 중요한 완충 역할을 했지만, 캐시 미스가 발생하면 문제가 데이터베이스로 직접 전달되었다.
Thundering Herd (Cache Stampede)
캐시의 TTL이 만료되는 순간, 동일한 데이터를 요청하는 수많은 클라이언트가 동시에 캐시 미스를 경험한다. 이들이 일제히 백엔드 데이터베이스에 쿼리를 보내는 Thundering Herd 현상이 발생했다. Discord의 규모에서는 하나의 인기 채널 캐시 만료만으로도 수십만 건의 동시 DB 쿼리가 발생할 수 있었고, 이는 데이터베이스 노드를 순간적으로 과부하 상태로 몰아넣었다.
연쇄 장애 위험
데이터베이스 과부하로 인한 응답 지연은 애플리케이션 서버의 연결 풀 고갈, 요청 큐 적체 등 연쇄적인 장애로 이어질 수 있었다. 한 채널의 캐시 만료가 전체 서비스에 영향을 미치는 blast radius가 크다는 점이 특히 위험했다.
4.3 해결
분산 락(Mutex) 기반 캐시 갱신
캐시 미스가 발생했을 때, 모든 요청이 DB에 접근하는 대신 분산 락을 사용하여 단 하나의 요청만 DB에서 데이터를 가져오도록 했다. Redis의 SET NX EX (또는 Redlock) 패턴을 활용하여 특정 키에 대한 캐시 갱신 락을 획득한 요청만 DB에 쿼리를 수행하고, 나머지 요청은 짧은 시간 대기 후 갱신된 캐시에서 데이터를 읽도록 구현했다. 이를 통해 캐시 미스 시 DB에 대한 동시 요청 수를 1로 제한할 수 있었다.
TTL 지터(Jitter) 적용
동일 시점에 대량의 캐시가 동시에 만료되는 것을 방지하기 위해, TTL에 랜덤 지터를 추가했다. 예를 들어, 기본 TTL이 300초인 경우 실제 TTL은 270~330초 사이의 랜덤 값으로 설정하여 만료 시점을 분산시켰다. 이로 인해 동시 캐시 미스로 인한 부하 스파이크가 크게 완화되었다.
비동기 캐시 프리페칭(Prefetching)
자주 조회되는 핫 데이터에 대해서는 TTL 만료를 기다리지 않고, 만료 임박 시점(예: TTL 잔여 시간이 전체의 10% 이하)에 백그라운드에서 비동기적으로 캐시를 갱신하는 전략을 적용했다. 이를 통해 핫 데이터에 대한 캐시 미스 자체를 방지하여, Thundering Herd가 발생할 여지를 원천적으로 제거했다.
ScyllaDB 마이그레이션과 데이터 서비스 계층
데이터베이스 자체의 내구성도 강화했다. Cassandra에서 ScyllaDB로 마이그레이션하면서 GC pause 문제를 해소하고, shard-per-core 아키텍처를 통해 더 예측 가능한 응답 시간을 확보했다. 또한 데이터베이스 앞에 데이터 서비스(data services) 계층을 두어 요청 합치기(request coalescing), 라우팅, Rate Limiting 등을 중앙에서 처리했다. 이 데이터 서비스 계층은 동일한 쿼리를 하나로 합쳐서 DB에 전달하는 역할도 수행하여, 캐시 미스 시에도 DB 부하를 줄이는 데 기여했다.
4.4 주요 교훈
- Thundering Herd(Cache Stampede)는 대규모 캐시 시스템에서 가장 흔하면서도 치명적인 문제 중 하나이다. 캐시를 도입할 때 반드시 이 패턴에 대한 대응 전략을 설계에 포함해야 한다.
- 분산 락 기반의 Cache-Aside 패턴으로 동시 DB 접근을 1로 제한할 수 있다. Redis의
SET NX EX명령어는 이 패턴의 가장 기본적인 구현 수단이다. - TTL에 랜덤 지터를 추가하는 것만으로도 동시 만료 문제를 크게 완화할 수 있다. 구현이 간단하면서도 효과가 크므로 모든 캐시 시스템에 기본 적용하는 것을 권장한다.
- 핫 데이터는 만료 전 비동기 갱신(Cache Warming/Prefetching)으로 미스 자체를 방지하는 것이 이상적이다. TTL 잔여 시간 기반의 조기 갱신 전략은 핫 데이터의 캐시 히트율을 100%에 가깝게 유지할 수 있다.
- 데이터베이스 앞에 데이터 서비스 계층을 두어 요청 합치기(request coalescing)를 수행하면, 캐시 미스 시에도 DB 부하를 효과적으로 줄일 수 있다. 이는 분산 락과 상호 보완적인 전략이다.
5. Stripe의 Redis 메모리 관리와 Eviction 정책 최적화
- 출처: Scaling a mature data pipeline - Managing Hundreds of Redis Instances
- 저자/출처: Stripe Engineering Team
참고: 이 출처 URL의 원문은 Stripe의 데이터 파이프라인 확장에 관한 글로, Redis 메모리 관리에 특화된 내용이 아닐 수 있다. 아래 내용은 Stripe 엔지니어링 블로그의 여러 글과 Redis 대규모 운영에 관한 일반적인 Best Practice를 기반으로 정리했으며, 원문에서 직접 확인되지 않는 세부 내용이 포함되어 있을 수 있다.
5.1 상황
Stripe는 온라인 결제 인프라를 제공하는 글로벌 핀테크 기업으로, 수백만 개의 비즈니스에 결제 처리 서비스를 제공한다. Stripe의 인프라에서 Redis는 다양한 컴포넌트에서 활용되고 있었다. API Rate Limiting, 세션 캐싱, 실시간 사기 탐지(fraud detection)를 위한 피처 스토어, 분산 잠금, 이벤트 집계 등 수십 가지 용도로 수백 개의 Redis 인스턴스를 운영하고 있었다.
결제 시스템의 특성상 데이터 정합성과 가용성에 대한 요구사항이 극히 높았다. Rate Limiting 카운터가 소실되면 API 남용을 차단할 수 없고, 사기 탐지 피처 데이터가 유실되면 사기 거래를 탐지하지 못한다. 반면 캐시 데이터는 일시적 소실이 허용된다. 이처럼 중요도와 특성이 서로 다른 워크로드들이 Redis라는 동일한 기술 스택 위에서 운영되고 있었다.
5.2 문제
Eviction 정책의 부적절한 적용
수백 개의 Redis 인스턴스를 운영하면서, 각 인스턴스에 적용된 eviction 정책이 워크로드 특성과 맞지 않는 경우가 빈번했다. 기본값인 noeviction이 설정된 캐시 인스턴스에서는 메모리가 maxmemory에 도달하면 모든 쓰기 요청이 OOM(Out of Memory) 에러와 함께 실패했다. 캐시 용도에서 OOM 에러는 불필요한 서비스 장애를 유발한다.
반대로, Rate Limiting 카운터를 저장하는 인스턴스에 allkeys-lru가 설정된 경우, 메모리 압박 시 아직 유효한 Rate Limiting 카운터가 삭제되어 API 보호 기능이 무력화되는 보안 문제가 발생했다.
메모리 단편화(Fragmentation)
일부 인스턴스에서 mem_fragmentation_ratio가 2.0 이상으로 치솟는 현상이 발생했다. 이는 Redis가 실제 저장한 데이터 크기의 2배 이상의 메모리를 할당받아 사용하고 있다는 의미이다. 원인은 빈번한 키 생성과 삭제로 인한 메모리 할당기(jemalloc)의 외부 단편화였다. 특히 크기가 다양한 값을 자주 갱신하는 워크로드(예: 사기 탐지 피처 스토어)에서 단편화가 심했다.
단편화는 실질적인 가용 메모리를 줄여 예상보다 빨리 maxmemory에 도달하게 만들었고, OS 수준에서의 메모리 사용량 경보를 트리거하여 운영팀의 불필요한 대응을 유발했다.
대규모 인스턴스 운영의 표준화 부재
수백 개의 인스턴스가 각기 다른 팀에 의해 생성되면서, 키 네이밍 규칙, TTL 정책, 모니터링 설정 등이 인스턴스마다 제각각이었다. 이로 인해 전체 Redis 인프라의 상태를 파악하기 어려웠고, 문제 진단과 용량 계획이 비효율적이었다.
5.3 해결
워크로드별 Eviction 정책 세분화
모든 Redis 인스턴스를 용도에 따라 분류하고, 각 용도에 적합한 eviction 정책을 표준으로 정의했다.
| 용도 | Eviction 정책 | 이유 |
|---|---|---|
| 순수 캐시 | allkeys-lru | 모든 키가 재생성 가능하므로 LRU 기반 제거 적합 |
| TTL 기반 캐시 | volatile-lru | TTL이 설정된 키만 제거 대상, 영구 키 보호 |
| Rate Limiting | noeviction | 카운터 소실 시 보안 기능 무력화, 메모리 상한 대신 용량 계획으로 대응 |
| 영속 데이터 | noeviction | 데이터 소실 불가, OOM 시 알람으로 즉시 대응 |
volatile-lru를 사용하는 인스턴스에서는 모든 캐시 키에 TTL이 반드시 설정되어 있는지 검증하는 정적 분석 도구도 도입했다. TTL 없는 키에 volatile-lru를 적용하면 eviction 대상이 없어 사실상 noeviction과 동일하게 동작하기 때문이다.
메모리 단편화 자동 해소
Redis 4.0 이상에서 제공하는 activedefrag 옵션을 활성화하여 온라인 상태에서 메모리 단편화를 자동으로 해소했다. active-defrag-threshold-lower(단편화 시작 임계값)와 active-defrag-cycle-min/max(CPU 사용률 제한)를 워크로드에 맞게 튜닝하여, 단편화 해소 작업이 서비스 성능에 영향을 미치지 않도록 했다. 이를 통해 대부분의 인스턴스에서 mem_fragmentation_ratio를 1.0~1.5 범위로 유지할 수 있었다.
자동화된 모니터링 시스템 구축
모든 Redis 인스턴스에서 INFO memory 명령의 주요 지표(used_memory, used_memory_rss, mem_fragmentation_ratio, evicted_keys 등)를 주기적으로 수집하는 모니터링 파이프라인을 구축했다. MEMORY DOCTOR 명령을 통한 자동 진단도 포함했다. 주요 알람 조건으로는 메모리 사용률 80% 초과, mem_fragmentation_ratio 1.5 초과, evicted_keys의 급격한 증가, 슬로우 로그 빈도 증가 등을 설정했다.
키 네이밍 컨벤션과 TTL 정책 표준화
전사 수준의 Redis 운영 가이드라인을 수립했다. 키 네이밍에 서비스명, 용도, 데이터 타입을 포함하도록 하여({service}:{purpose}:{identifier} 형식) 키를 보고 어떤 서비스의 어떤 데이터인지 즉시 파악할 수 있게 했다. TTL 정책도 용도별로 표준화하여(캐시: 1~24시간, Rate Limiting: 1분~1시간, 세션: 30분~2시간 등) 예측 가능한 메모리 관리를 가능하게 했다.
5.4 주요 교훈
- Eviction 정책은 워크로드 특성에 맞게 인스턴스별로 다르게 설정해야 한다. 하나의 정책을 모든 인스턴스에 일괄 적용하는 것은 캐시에서는 불필요한 장애를, 영속 데이터에서는 데이터 유실을 초래한다.
volatile-lru는 TTL이 설정된 키만 eviction 대상으로 하므로, 이 정책을 사용할 때는 모든 캐시 키에 반드시 TTL을 설정해야 한다. TTL이 없는 키만 남으면 eviction이 동작하지 않아 OOM이 발생한다.- 메모리 단편화율(
mem_fragmentation_ratio)은 1.0~1.5 범위를 유지하는 것이 이상적이다. 1.5를 초과하면activedefrag활성화를 고려하고, 2.0 이상이면 재시작을 통한 메모리 재할당도 검토해야 한다. - 대규모 Redis 운영에서는 인스턴스별 용도 분류와 표준화된 운영 정책(키 네이밍, TTL, eviction, 모니터링)이 필수적이다. 표준 없이 인스턴스가 늘어나면 운영 부채가 기하급수적으로 증가한다.
- 결제/보안 관련 Redis 인스턴스는 eviction 정책보다 용량 계획(capacity planning)으로 OOM을 방지하는 것이 올바른 접근이다. 중요한 데이터가 eviction으로 소실되는 것보다 OOM 에러를 감지하고 즉시 대응하는 것이 낫다.
주요 요약
| 주제 | 주요 포인트 | 관련 사례 |
|---|---|---|
| Sentinel vs Cluster | Sentinel은 HA 전용, Cluster는 HA + 수평 확장. 쓰기 확장이 필요하면 Cluster 전환 필수 | 사례 1, 3 |
| 데이터 분리 | 캐시/영속 데이터 혼재는 운영 사고의 주요 원인. 용도별 인스턴스 분리 필수 | 사례 2, 5 |
| Thundering Herd | 분산 락 + TTL 지터 + 프리페칭으로 대응. 캐시 설계 시 반드시 고려해야 할 패턴 | 사례 4 |
| Eviction 정책 | 워크로드별 정책 차별화, volatile-lru 사용 시 TTL 필수, 메모리 단편화 모니터링 | 사례 5 |
| Failover | Sentinel 타임아웃 튜닝, 클라이언트 호환성 검증, 장애 테스트 필수 | 사례 1, 3 |
| 마이그레이션 전략 | 듀얼 라이트, 점진적 트래픽 전환, 롤백 자동화, 데이터 정합성 검증 파이프라인 | 사례 1, 2 |
| 키 설계 | 해시태그로 Cluster 호환성 확보, 네이밍 컨벤션 표준화, TTL 정책 수립 | 사례 1, 5 |
| 모니터링 | INFO memory, MEMORY DOCTOR, mem_fragmentation_ratio, evicted_keys 추적 | 사례 2, 5 |