1. Redis 개요
Redis(Remote Dictionary Server)는 ANSI C로 작성된 오픈 소스, 인메모리 데이터 구조 저장소입니다. BSD 라이선스를 따르며, 네트워크 기반의 키-값(Key-Value) NoSQL 데이터베이스로 동작합니다. 다양한 언어의 API를 지원하며, 메모리 기반의 빠른 속도와 선택적 영속성을 제공합니다. 주로 '데이터 구조 서버'라고 불리는데, 이는 값(value)으로 문자열(String), 해시(Hash), 리스트(List), 셋(Set), 정렬된 셋(Sorted Set) 등 여러 데이터 타입을 지원하기 때문입니다.
2. Redis 데이터 타입과 활용 사례
- String: 가장 기본적인 set/get 작업에 사용됩니다. 값은 문자열이나 숫자가 될 수 있으며, 주로 복잡한 카운팅 기능의 캐시로 활용됩니다.
- Hash: 구조화된 객체를 저장하기에 적합합니다. 특정 필드만 조작하기 쉬워 객체 캐싱에 유리합니다.
- List: 간단한 메시지 큐 기능을 구현하거나,
lrange명령어를 활용한 페이지네이션 기능을 제공합니다. 성능이 뛰어나고 사용자 경험이 좋습니다. - Set: 중복되지 않는 값의 집합입니다. 전역 중복 제거 기능에 사용됩니다. 분산 시스템 환경에서 JVM 자체의 Set을 사용하는 대신 Redis Set을 사용하면 클러스터 환경에서도 간편하게 중복을 제거할 수 있습니다.
- Sorted Set: 각 요소에 가중치(score)를 부여하여 score 기준으로 정렬됩니다. 리더보드, TOP N 조회, 지연 작업, 범위 검색 등에 활용됩니다.
3. Redis 키 만료 정책
Redis는 메모리 관리를 위해 여러 가지 키 만료 정책을 제공합니다.
- 타이머 기반 삭제 (定时删除): 타이머를 사용해 키가 만료되면 즉시 삭제합니다. 메모리는 즉시 확보되지만 CPU 자원을 많이 소모합니다. 대규모 동시 요청 환경에서는 CPU가 요청 처리에 집중해야 하므로 이 방식은 사용되지 않습니다.
- 주기적 삭제 (定期删除): Redis는 기본적으로 100ms마다 무작위로 키를 검사하여 만료된 키를 삭제합니다. 모든 키를 검사하지는 않으므로, 일부 키가 제때 삭제되지 않을 수 있습니다.
- 지연 삭제 (惰性删除): 키에 접근할 때 해당 키가 만료되었는지 확인하고, 만료되었다면 그때 삭제합니다. 주기적 삭제와 함께 사용됩니다.
주기적 삭제와 지연 삭제만으로는 모든 만료 키를 처리할 수 없습니다. 키가 주기적 삭제에서 누락되고, 사용자가 해당 키를 요청하지 않아 지연 삭제도 실행되지 않으면 Redis 메모리가 계속 증가합니다. 이때 메모리 제거 정책 (Eviction Policy)이 필요합니다.
메모리 제거 정책 (redis.conf 설정)
# maxmemory-policy volatile-lru
- noeviction: 메모리가 부족하면 새 데이터 쓰기를 거부하고 오류를 반환합니다. 제거를 수행하지 않습니다.
- allkeys-lru: 메모리가 부족하면 전체 키 공간에서 가장 오래 사용되지 않은 키(Least Recently Used)를 제거합니다. 가장 권장됩니다.
- allkeys-random: 전체 키 공간에서 무작위로 키를 제거합니다. 거의 사용되지 않습니다.
- volatile-lru: 만료 시간이 설정된 키 중에서만 LRU 방식으로 제거합니다. Redis를 캐시와 영구 저장소로 동시에 사용할 때 사용되며, 권장되지 않습니다.
- volatile-random: 만료 시간이 설정된 키 중에서 무작위로 제거합니다. 권장되지 않습니다.
- volatile-ttl: 만료 시간이 설정된 키 중에서 만료 시간이 가장 가까운 키를 우선 제거합니다. 권장되지 않습니다.
참고: 만료 시간(expire)이 설정되지 않은 키가 있는 경우, volatile-lru, volatile-random, volatile-ttl 정책은 noeviction과 동일하게 동작합니다.
4. Redis 일반적인 문제
(1) 캐시와 데이터베이스의 쓰기 일관성 문제
데이터베이스와 캐시를 함께 사용하면 데이터 불일치 문제가 발생할 수 있습니다. 강한 일관성(Strong Consistency)이 필요하다면 캐시를 사용하지 않는 것이 좋습니다. 모든 해결책은 최종 일관성(Eventual Consistency)만을 보장하며, 불일치 가능성을 낮출 뿐 완전히 피할 수는 없습니다.
해결 방법: 먼저 데이터베이스를 업데이트한 후 캐시를 삭제하는 전략을 사용합니다. 캐시 삭제 실패 가능성을 대비해 메시지 큐와 같은 보상 메커니즘을 마련합니다.
(2) 캐시 눈사태 (Cache Avalanche)
대량의 캐시가 동시에 만료되어 모든 요청이 데이터베이스로 몰리면서 데이터베이스가 과부하로 다운되는 현상입니다.
해결 방법:
- 캐시 만료 시간에 랜덤 값을 추가하여 대량 만료를 방지합니다.
- 동시 요청이 많지 않은 경우, 잠금 및 큐(lock & queue)를 사용합니다.
- 각 캐시 데이터에 캐시 유효성 태그를 추가하여 태그가 만료되면 데이터를 다시 로드합니다.
(3) 캐시 관통 (Cache Penetration)
요청한 데이터가 캐시에 없고 데이터베이스에도 없어서 매번 데이터베이스를 조회해야 하는 경우입니다. 악의적인 공격자가 존재하지 않는 데이터를 계속 요청하면 데이터베이스에 과부하가 발생할 수 있습니다.
해결 방법:
- API 계층에서 사용자 인증, ID 유효성 검사(ID <= 0 차단) 등 기본 검증을 수행합니다.
- 캐시와 데이터베이스 모두에 데이터가 없으면 키-널(key-null) 값을 캐시에 저장합니다. 유효 시간은 짧게(예: 30초) 설정하여 정상적인 데이터 사용에 지장을 주지 않도록 합니다.
- 블룸 필터(Bloom Filter)를 사용하여 존재하지 않는 데이터를 미리 걸러냅니다.
(4) 캐시 돌파 (Cache Breakdown / Hotspot Invalid)
특정 핫스팟 키가 만료되는 순간, 대규모 동시 요청이 캐시를 우회하여 데이터베이스로 직접 몰리는 현상입니다.
해결 방법:
- 뮤텍스 키 (Mutex Key) 사용: 한 스레드만 데이터베이스에 접근하여 캐시를 다시 쓰도록 하고, 다른 스레드는 대기합니다. 분산 환경에서는 분산 락이 필요합니다. 동시성이 매우 높은 경우 처리량이 감소할 수 있습니다.
- 핫스팟 데이터 영구화 (Never Expire): 물리적으로 만료 시간을 설정하지 않거나, 논리적으로 만료 시간을 키의 값에 포함시켜 관리합니다. 만료 직전에 백그라운드 스레드가 캐시를 갱신합니다. 이 방식은 엄격한 일관성이 필요 없는 시스템에 적합합니다.
(5) 캐시 동시성 경쟁 (Concurrency Race Condition)
여러 클라이언트가 동시에 같은 키를 업데이트하려고 할 때 데이터 순서가 꼬여 데이터베이스 값이 손상되는 문제입니다.
해결 방법:
- 분산 락 + 타임스탬프: 분산 락(예:
SETNX)을 사용하여 병렬 쓰기를 직렬화합니다. 각 업데이트에 타임스탬프를 추가하여 최신 데이터만 유지합니다. - 메시지 큐 사용: Redis
SET작업을 메시지 큐에 넣어 순차적으로 실행합니다. 높은 동시성 환경에서 일반적인 해결책입니다.
5. 기타 고려 사항
캐시 워밍업 (Cache Warming)
시스템 배포 시 관련 캐시 데이터를 미리 Redis에 로드하여 초기 사용자의 데이터베이스 부하를 줄입니다.
처리 방법:
- 데이터 양이 적으면 애플리케이션 시작 시 캐시를 로드합니다.
- 데이터 양이 많으면 정기적인 스크립트를 사용하여 캐시를 갱신합니다.
- 데이터 양이 매우 많으면 핫스팟 데이터를 우선 로드합니다.
캐시 서킷 브레이커 / 폴백 (Cache Degradation / Fallback)
캐시가 실패하거나 서버가 다운되었을 때 데이터베이스에 접근하지 않고 기본 데이터나 메모리 내 데이터를 반환하는 전략입니다. 일반적으로 핫스팟 데이터를 서비스 메모리에 캐싱하여 캐시 장애 시에도 서비스가 영향을 최소화하도록 합니다. 이는 유손실(有損) 작업이므로 비즈니스 영향도를 최소화해야 합니다.
6. Redis 성능 테스트 도구: memtier_benchmark
memtier_benchmark는 Redis Labs에서 제공하는 명령줄 도구로, 키-값 저장소에 데이터 부하를 생성하고 스트레스 테스트를 수행합니다.
설치 전 필수 조건
Linux 시스템에 다음 라이브러리 또는 도구가 설치되어 있어야 합니다.
- Git
- libevent 2.0.10 이상
- libpcre 8.x
- autoconf, automake, GNU make, GCC C++ 컴파일러
설치 방법 (CentOS 7.2 예시)
- Git 설치:
yum install git - 컴파일 도구 설치:
yum install -y autoconf automake make gcc-c++ git - 필요 라이브러리 설치:
yum install -y pcre-devel zlib-devel libmemcached-devel openssl-devel libevent-devel - (필요시) libevent 업데이트: 소스코드를 다운로드하여 컴파일 및 설치합니다.
- 환경 변수 설정:
export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:${PKG_CONFIG_PATH} - 소스 코드 클론:
git clone https://github.com/RedisLabs/memtier_benchmark.git - 디렉토리 이동:
cd memtier_benchmark - 컴파일 및 설치:
autoreconf -ivf; ./configure; make; make install
테스트 명령어 및 옵션
기본 명령어 형식:
./memtier_benchmark -s <서버 주소> -p <포트> -a <비밀번호> -c <클라이언트 수> -d <데이터 크기> --threads=<스레드 수> --ratio=<set:get 비율> --test-time=<테스트 시간>
주요 옵션 설명:
-s, --server: 서버 주소 (기본값: localhost)-p, --port: 포트 (기본값: 6379)-c, --clients: 스레드당 클라이언트 수 (기본값: 50)-t, --threads: 시뮬레이션할 스레드 수 (기본값: 4)--test-time: 테스트 지속 시간 (초)--ratio: set과 get 요청 비율 (기본값: 1:10)--data-size: 객체 데이터 크기 (기본값: 32 바이트)--data-size-list: 키 크기 비율을 설정합니다. 예)--data-size-list=4000:50,16000:50(4KB 50%, 16KB 50%)--data-size-pattern:R(랜덤) 또는S(순차) 중 선택--client-stats: 각 클라이언트의 통계 파일 생성--out-file: 최종 결과 파일 생성
전체 명령어 예시:
memtier_benchmark -s 127.0.0.1 -p 6379 --threads=4 --clients=100 --data-size-list=4000:50,512000:50 --test-time=1800 --ratio=1:10 --client-stats=/dir/client --out-file=/dir/result.txt