Redis란?
Redis(Remote Dictionary Server)는 오픈 소스 기반의 인메모리(in-memory) 데이터 구조 저장소로, 매우 빠른 읽기/쓰기 성능을 제공하는 Key-Value 저장소다. 단순한 Key-Value 저장소를 넘어 다양한 데이터 구조를 지원하여 캐시(Cache), 메시지 브로커, 세션 관리 등 다양한 분야에서 활용된다.
Redis의 주요 특징
✅ 빠른 성능: 데이터를 메모리에 저장하므로 디스크 기반 데이터베이스보다 빠른 응답 속도를 제공함 (평균 응답 시간 < 1ms).
✅ 다양한 데이터 구조 지원: String, Hash, List, Set, Sorted Set 등 여러 데이터 타입을 제공.
✅ 지속성 옵션 제공: 데이터를 디스크에 저장할 수 있어 장애 발생 시 데이터 복구 가능.
✅ Pub/Sub 지원: 메시지 브로커 역할을 수행할 수 있어 실시간 메시징 시스템 구축 가능.
✅ 트랜잭션 지원: MULTI와 EXEC 명령어를 통해 여러 명령을 원자적으로 실행 가능.
✅ 스케일링 지원: 클러스터 기능을 통해 확장성 및 가용성 증가.
Redis 캐시로 사용하기
📌 캐시란?
캐시(Cache)는 자주 사용하는 데이터를 미리 저장해 빠르게 제공하는 저장소다. 하드 디스크보다 빠른 RAM을 사용하여 데이터 조회 속도를 향상시킨다. 예를 들어, 웹 사이트의 이미지, 스타일시트, 자바스크립트 파일 등은 사용자의 브라우저 캐시에 저장될 수 있어, 동일한 페이지나 사이트에 다시 접근할 대 더 빠르게 로딩될 수 있다.
캐시 관련 용어
- 캐시 히트(Cache Hit): 요청한 데이터가 캐시에 있어 빠르게 제공되는 경우
- 캐시 미스(Cache Miss): 요청한 데이터가 캐시에 없어 DB에서 데이터를 가져와야 하는 경우
- 캐시 교체 정책(Cache Eviction Policy): 캐시 공간이 가득 찼을 때 어떤 데이터를 제거할지 결정하는 정책
- 대표적으로 LRU(Least Recently Used), LFU(Least Frequently Used) 등이 존재
Redis를 캐시로 사용하는 이유
✅ 빠른 성능: 평균 응답 속도 1ms 미만, 초당 수백만 건의 요청 처리 가능.
✅ 자동 만료 정책: TTL(Time-To-Live) 설정을 통해 캐시 데이터를 자동으로 삭제 가능.
✅ 영속성 옵션: 필요 시 데이터를 디스크에 저장하여 장애 발생 시 데이터 복구 가능.
✅ 확장성: 클러스터 모드를 활용해 확장 가능하며, 부하 분산으로 안정성 증가.
- 캐시 히트(Cache Hit): 요청된 데이터가 캐시에 있을 때, 즉 데이터를 캐시에서 빠르게 가져올 수 있는 경우를 의미한다.
- 캐시 미스(Cache Miss): 요청된 데이터가 캐시에 없어서 느린 저장소에서 직접 데이터를 가져와야 할 때를 의미한다.
- 캐시 교체 정책(Cache Eviction Policy): 캐시가 꽉 찼을 때 어떤 데이터를 캐시에서 제거할지 결정하는 규칙이다. 가장 오래된 데이터를 제거하는 LRU(Least Recently Used), 가장 적게 사용된 데이터를 제거하는 LFU(Least Frequently Used) 등 다양한 정책이 있다.
즉, 같은 데이터에 대해 반복적인 액세스가 일어나는 경우, 원본 보다 빠른 접근 속도, 자주 변하지 않는 데이터인 필요한 경우 캐시를 효율적으로 사용할 수 있다.
읽기 전략
Look-Aside(Lazy Loading)
Look-Aside 패턴(또는 Cache-Aside 패턴)은 캐시에 데이터가 없을 경우 DB에서 가져와 캐시에 저장하는 방식이다.
동작 방식
- 애플리케이션이 Redis에서 데이터 조회.
- 캐시 히트(Cache Hit) 시 데이터 반환.
- 캐시 미스(Cache Miss) 시 데이터베이스에서 데이터 조회 후 캐시에 저장.
- TTL을 설정하여 일정 시간 후 데이터가 자동 삭제되도록 관리.
💡 장점: 필요한 데이터만 캐시에 저장해 메모리 사용 효율적. 💡 단점: 처음 요청 시 캐시 미스로 인해 응답 시간이 길어질 수 있음.
Cache-Aside 패턴의 주요 장점 중 하나는 캐시와 데이터베이스 간의 일관성 유지를 애플리케이션 로직으로 처리할 수 있다는 점이다. 애플리케이션은 데이터 업데이트 시 캐시 데이터를 명시적으로 만료시키거나 업데이트할 책임이 있다. 또한, 캐시 시스템의 부하를 줄이고, 데이터베이스 요청을 최적화하는데 유용하다. 하지만, 캐시 미스 시 어플리케이션 성능에 부담을 줄 수 있으므로, 적절한 TTL 설정과 캐시 정책 관리가 필요하다.
이러한 구조는 Redis가 다운되더라도 바로 장애로 이어지지 않고 DB에서 바로 데이터를 조회할 수 있다.
대신 캐시로 붙어있던 커넥션(connection)이 많았다면 해당 커넥션이 모두 DB로 이동하므로 갑자기 DB에 많은 부하가 몰릴 수 있다.
그래서 캐시를 새로 투입하거나 혹은 DB에만 새로운 데이터를 저장한 경우, 처음에 캐시 미스가 엄청 발생해 성능 저하가 발생한다.
이럴 때는 미리 DB의 데이터를 캐시로 밀어 넣어주는 작업을 진행할 수 있는데 이러한 작업을 캐시 워밍(cache warming)이라 한다.
캐시 워밍(cache warming)🌤️
캐시 워밍(cache warming)은 캐시 시스템을 초기화하거나 최적화하기 위해 미리 데이터를 캐시에 로딩하는 과정을 말한다.
시스템이 사용자 요청을 받기 전에 이미 자주 접근하는 데이터를 캐시에 저장함으로써, 사용자 요청 시 데이터를 더 빠르게 제공할 수 있게 해 준다. (실제 티켓링크에서는 상품 오픈 전 상품의 정보를 미리 DB에서 cache로 올려주는 작업을 매번 하고 있다)
서비스나 어플리케이션이 시작될 때 또는 특정 이벤트 후에 캐시를 사전에 준비하는 것이 주요 목적이다.
캐시 워밍은 특히 대규모 시스템에서 성능 향상을 도모할 수 있다.
1. 데이터 선택: 어떤 데이터를 캐시에 미리 로딩할지 결정하는 것이 중요하다. 대개 시스템에서 자주 요청되는 데이터나 조회 수가 많은 데이터가 대상이 된다.
2. 캐시 업데이트: 캐시 워밍이 수행될 시간을 결정해야 한다. 이는 서비스가 시작될 때, 저사용 시간대에, 또는 주기적으로 수행될 수 있다.
3. 자동화: 캐시 워밍 과정을 자동화하여 시스템 재시작이나 특정 트리거 발생 시 자동으로 데이터를 캐시에 로딩할 수 있다.
💡장점: 초기 캐시 미스를 줄여 성능 저하 방지.🚨 주의점: 불필요한 데이터를 캐시에 저장하면 메모리 낭비 발생 가능.
쓰기 전략
Write-Around
- 데이터를 DB에만 저장하고, 캐시에는 저장하지 않는 방식.
- 💡 장점: 불필요한 캐시 쓰기를 줄여 메모리 사용 효율적.
- 💡 단점: 바로 조회해야 할 경우 캐시 미스 발생 가능.
Write-Through
- 데이터를 캐시와 DB에 동시에 저장하는 방식.
- 💡 장점: 데이터 일관성을 유지하기 쉬움.
- 💡 단점: 모든 쓰기 연산이 캐시를 거쳐야 하므로 지연 시간 증가.
Write-Behind (Write-Back)
- 데이터를 캐시에만 저장하고 일정 주기마다 배치로 DB에 반영하는 방식.
- 장점: DB 부하 감소 및 빠른 쓰기 성능 제공.
- 단점: 캐시에 저장된 데이터가 손실될 위험 존재.
Redis 데이터 타입 활용하기
String | 기본 Key-Value 저장 (ex: SET key value) |
List | 순서가 있는 리스트 (ex: LPUSH mylist A) |
Set | 중복 없는 집합 (ex: SADD myset A) |
Sorted Set | 점수 기반 정렬된 집합 (ex: ZADD leaderboard 100 user1) |
Hash | 객체 저장 (ex: HSET user:1 name 혜리 age 25) |
Bitmaps | 비트 연산 지원 (ex: SETBIT key 0 1) |
HyperLogLogs | 유니크한 데이터 개수 추정 (ex: PFADD unique_visitors user1) |
Streams | 로그 및 메시지 저장 (ex: XADD mystream * sensor-id 1234 temperature 19.8) |
- 문자열(Strings)
- 텍스트 또는 이진 데이터를 저장할 수 있다.
- ex) "I am a default datatype."
- 목록(Lists)
- 문자열의 순서가 있는 목록이며, 주로 목록의 양끝에 데이터를 삽입하거나 삭제할 때 사용된다.
- ex) [A -> B -> C -> A -> D]
- 집합(Sets)
- 중복을 허용하지 않는 문자열 집합으로, 데이터의 유니크한 값을 저장할 때 유용하다.
- ex) {A, B, C, D, E, F}
- 정렬된 집합(Sorted Sets)
- 집합의 각 멤버에 스코어를 매겨 순서를 지정할 수 있으며, 랭킹이나 리더보드 같은 기능에서 유용하다.
- ex) {"LG":1, "KT":2,"SSG":3,"NC":4,"KIA":5}
- 해시(Hashes)
- 키와 값 쌍의 컬렉션으로, 객체를 저장하고자 할 때 적합하다.
- ex) {"Name":"JJUN","Age":"11","Birthday":"1111"}
- 비트맵(Bitmaps)
- 비트 필드에 데이터를 저장하며, 이를 사용해 대규모 데이터 집합에서의 간단한 값(예: 참/거짓)을 효율적으로 표현한다.
- ex) 1010100010101011010101010
- 하이퍼로그로그(HyperLogLogs)
- 중복을 허용하지 않는 큰 집합의 멤버 수를 추정하는 데 사용되며, 대량의 데이터를 처리하고 메모리를 적게 사용해야 할 때 유용하다.
- ex) 00010101 00010101 01101010
- 스트림(Streams)
- Streams는 메시지를 시간 순서대로 저장한다.
- 각 메시지는 유니크한 ID를 부여받아 순서를 관리한다.
- ex) {"ID":"124214584395-0", {"f1":"v1","f2":"v2"}
Best Practice - Counting
Strings
- 단순 증감 연산
- INCR / INCRBY / INCRBYFLOAT / HINCRBY / HINCRBYFLOAT / ZINCRBY
예시
SET score:a 10
"OK"
INCR score:a
(integer) 11
INCRBY score:a 4
(integer) 15
Bits
- 데이터 저장공간 절약
- 정수로 된 데이터만 카운팅 가능
SETBIT visitors:20210817 3 1
(integer) 0
SETBIT visitors:20210817 6 1
(integer) 0
BITCOUNT visitors:20210817
(integer) 2
HyperLogLogs
- 대용량 데이터를 카운팅 할 때 적절(오차 0.81%)
- set과 비슷하지만 저장되는 용량은 매우 작음(12KB 고정)
- 저장된 데이터는 다시 확인할 수 없음 (데이터 보호용으로도 적절히 사용 가능)
- ex) 웹사이트에 방문한 유니크한 IP 개수, 크롤링한 URL의 개수, 검색 엔진에서 검색된 유니크한 단어 개수
PFADD crawled:20211024 "http://www.google.com/"
(integer) 1
PFADD crawled:20171124 "http://www.google.com/"
(integer) 1
PFCOUNT crawled:20211024
(integer) 8278423
PFMERGE crawled:all crawled:20211024 crawled:20211023 . .
(integer) 23905917
Best Practice - Messaging
Lists
- Blocking 기능을 이용해 Event Queue로 사용
- 키가 있을 때만 데이터 저장 가능 - LPUSHX / RPUSHX
- 이미 캐싱되어 있는 피드에만 신규 트윗을 저장
- 키가 있을 경우에만 해당 리스트에 데이터를 추가한다. 키가 존재한다는 건 이전에 이미 사용하던 큐라는 의미, 사용했던 큐에만 메시지를 넣어줄 수 있기 때문에 비효율적인 데이터의 이동을 막을 수 있다.
- 애플리케이션을 자주 이용하던 유저의 타임라인에만 새로운 데이터를 미리 캐시해 놓을 수 있으며, 자주 사용하지 않는 유저는 caching key 자체가 존재하지 않기 때문에 그런 유저들을 위해 데이터를 미리 쌓아놓는 것과 같은 비효율적인 작업을 방지할 수 있다.
Streams
- 로그를 저장하기 가장 적절한 자료구조
- append-only
- 시간 범위로 검색 / 신규 추가 데이터 수신 / 소비자별 다른 데이터 수신(소비자 그룹)
XADD mystream * sensor-id 1234 temperature 19.8
"163482341307-0"
XADD mystream * sensor-id 1234 temperature 20.4
"1634823263756-0"
Redis에서 데이터를 영구 저장하려면? (RDB vs AOF)
Redis Persistence
Redis는 In-memory 데이터 스토어!!
- 서버 재시작 시 모든 데이터 유실
- 복제 기능을 사용해도 사람의 실수 발생 시 데이터 복원 불가
- Redis를 캐시 이외의 용도로 사용한다면 적절한 데이터 백업 필요
Redis Persistence Option (RDB, AOF)
Redis에서는 데이터를 영구저장하는 두 가지 방법을 제공한다(만약 Redis를 캐시 용도로 사용한다면 이 기능은 사용할 필요 없다).
RDB (Redis Database)
RDB는 특정 시점의 데이터 스냅샷을 생성하여 디스크에 저장한다. 주기적으로 또는 명시적인 명령어를 통해 실행될 수 있다. RDB는 복구 시 매우 빠르다는 장점이 있다. Redis가 RDB 파일을 로드하기만 하면 되기 때문에, 큰 데이터 집합을 빠르게 복구할 수 있다. 다만, Redis가 마지막 스냅샷 이후에 수신한 모든 데이터를 잃을 수 있는 단점이 있다.
AOF (Append Only File)
AOF는 처리된 모든 쓰기 연산 명령어를 기록한다. Redis 시작 시, 이 파일을 읽어 모든 데이터를 재구성한다.
AOF는 데이터의 신뢰성 측면에서 장점을 가지며, 손실될 가능성이 매우 적다. 하지만, 파일 크기가 커질 수 있고, RDB에 비해 데이터 복구 시간이 길어질 수 있다. 최신 버전의 Redis에서는 AOF 파일의 크기가 너무 커지는 것을 방지하기 위해 'AOF rewrite'라는 과정을 제공한다. 이 과정은 AOF 파일을 재작성하여 불필요한 내용을 제거하고 파일 크기를 줄인다.
자동 / 수동 파일 저장 방법
RDB
- 자동: redis.conf 파일에서 SAVE 옵션(시간 기준)
- 수동: BGSAVE 커맨드를 이용해 CLI 창에서 수동으로 RDB 파일 저장
- SAVE 커맨드는 절대 사용 X
AOF
- 자동: redis.conf 파일에서 auto-aof-rewrite-percentage 옵션(크기 기준)
- 수동: BGREWRITEAOF 커맨드를 이용해 CLI 창에서 수동으로 AOF 파일 재작성
RDB vs AOF 선택 기준
- 백업은 필요하지만 어느 정도의 데이터 손실이 발생해도 괜찮은 경우
- RDB 단독 사용
- redis.conf 파일에서 SAVE 옵션을 적절히 사용
- ex) SAVE 900 1
- 장애 상황 직전까지의 모든 데이터가 보장되어야 할 경우
- AOF 사용(appendonly yes)
- APPENDFSYNC 옵션이 everysec인 경우 최대 1초 사이의 데이터 유실 가능(기본 설정)
- 제일 강력한 내구성이 필요한 경우
- RDB & AOF 동시 사용
Redis 운영 꿀팁과 장애 포인트
사용하면 안되는 커맨드
Redis는 Single Thread로 동작
- 한 사용자가 오래 걸리는 작업을 요청하면 나머지 요청들은 수행할 수 없고 대기하게 된다.
- keys * -> scan으로 대체
- scan을 사용하면 재귀적으로 key들을 호출할 수 있다.
- Hash나 Sorted Set 등 자료구조
- 키 나누기(최대 100만개)
- 하나의 키에 최대 100만개 이상은 저장되지 않도록 키를 적절히 나누는 것이 좋다.
- hgetall -> hscan
- del -> unlink
- key에 많은 데이터가 들어있을 때 del로 데이터를 지우게 되면, 해당 키를 지우는 동안 아무런 동작을 할 수 없다.
- unlink 명령어를 사용한다면 key를 백그라운드로 지워주기 때문에 권장된다.
- 키 나누기(최대 100만개)
변경하면 장애를 막을 수 있는 기본 설정값
STOP-WRITES-ON-BGSAVE-ERROR = NO
- yes(default)
- RDB 파일 저장 실패 시 redis로의 모든 write 불가능
MAXMEMORY-POLICY = ALLKEYS-LRU
- redis를 캐시로 사용할 때 Expire Time 설정 권장
- 메모리가 가득 찼을 때 MAXMEMORY-POLICY 정책에 의해 키 관리
- noeviction(default): 삭제 안함
- volatile-lru
- allkeys-lru
Cache Stampede
TTL 값을 너무 작게 설정한 경우
TTL(Time-To-Live) 값을 작게 설정하는 경우, Cache Stampede 현상은 특히 주의해야 할 중요한 문제가 된다.
TTL 값은 특정 데이터가 캐시에 남아 있는 시간을 결정한다. 이 값이 작을수록 저장된 데이터는 더 빠르게 만료되며, 이는 곧 캐시되어 있던 데이터에 대한 요청이 데이터베이스나 백엔드 시스템으로 직접 전달됨을 의미한다.
작은 TTL 값으로 인한 Cache Stampede는 다음과 같은 상황에서 발생할 수 있다.
- 빈번한 캐시 만료: TTL 값이 작으면, 동일한 데이터에 대한 요청이 거의 동시에 발생할 때 해당 데이터를 재생성하고 캐시해야 한다. 만약 많은 사용자가 동시에 같은 데이터를 요청한다면, 모든 요청이 백엔드 시스템으로 전달되어 같은 데이터를 찾게 되는 duplicate read, 읽어온 값을 각각 Redis에 write하는 duplicate write가 발생한다.
- 동시성 문제 증가: 작은 TTL 값은 데이터를 더 자주 갱신해야 함을 의미한다. 이는 동시에 같은 캐시 키에 대한 여러 요청이 백엔드 시스템에 도달할 확률을 증가시킨다.
이러한 문제는 TTL 시간을 넉넉히 증가시킴으로써 해결할 수 있다.
MaxMemory 값 설정
Persistence / 복제 사용 시 MaxMemory 설정 주의
- RDB 저장 & AOF rewrite 시 fork()
- Copy-on-Write로 인해 메모리를 두배로 사용하는 경우 발생 가능
- Persistence / 복제 사용 시 MaxMemory는 실제 메모리의 절반으로 설정
- ex) 4GB -> 2048MB
Memory 관리
물리적으로 사용되고 있는 메모리를 모니터링 (used_memory 대신 used_memory_rss를 확인하는 게 중요하다)
- used_memory: 논리적으로 Redis가 사용하는 메모리
- used_memory_rss: OS가 Redis에 할당하기 위해 사용한 물리적 메모리 양
- 실제 저장된 데이터는 적은데 Rss값은 큰 상황이 발생할 수 있다.
- 이 차이가 클 때 fragmentation이 크다고 말한다.
- 삭제되는 키가 많으면 fragmentation 증가
- 특정 시점에 피크를 찍고 다시 삭제되는 경우
- TTL로 인한 eviction이 많이 발생하는 경우
- 이때 activefrag라는 기능을 잠시 켜두는 것이 도움된다(단편화가 많이 발생한 경우).
참고
https://www.youtube.com/watch?v=92NizoBL4uA
'기술(Tech) > Database' 카테고리의 다른 글
ORDER BY와 인덱스의 관계 + sort_buffer_size (0) | 2024.12.27 |
---|---|
Covering Index (0) | 2024.12.23 |
EXPLAIN을 활용한 SQL 쿼리 성능 분석 방법 (0) | 2024.12.22 |
Multi Column Index (0) | 2024.12.10 |
인덱스(Index)란? (1) | 2024.12.10 |