Redis 학습
Redis 소개
Redis는 Remote Dictionary Server의 약자로, 고성능의 key-value 저장소이다. 오픈 소스로 개발되었으며, 네트워크를 통해 접근할 수 있는 인메모리 데이터 구조 저장소로, 다양한 데이터 구조를 지원한다. 예를 들어 문자열(String), 해시(Hash), 리스트(List), 집합(Set), 정렬된 집합(Sorted Set) 등의 데이터 타입을 지원하여, 이를 활용해 매우 빠른 읽기와 쓰기 속도를 제공한다.
Redis의 특징
- 빠른 성능: 데이터를 메모리에 저장하므로, 디스크 기반의 데이터베이스에 비해 훨씬 빠른 읽기/쓰기 속도를 제공한다.
- 지속성 옵션: Redis는 데이터를 디스크에 저장할 수 있는 옵션을 제공한다.
- 메모리의 내용이 시스템 장애로 인해 손실되더라도 데이터를 복구할 수 있다.
- 다양한 데이터 구조 지원: Redis는 단순한 key-value 저장 이상의 기능을 제공하며, 다양한 데이터 구조를 활용하여 복잡한 애플리케이션의 요구사항을 충족할 수 있다.
- 펍/서브 메시징: Redis는 펍/서브(pub/sub) 모델을 지원하여 실시간 메시징 시스템을 구축할 수 있다.
- 트랜잭션 지원: Redis는 기본적인 트랜잭션 기능을 제공하며, 여러 명령어를 묶어 순차적으로 실행할 수 있다.
Redis는 캐시 시스템, 세션 관리, 게임 리더보드, 실시간 분석 등 다양한 분야에서 널리 사용되고 있다.
Redis 캐시로 사용하기
캐시란?
캐시(Cache)는 데이터나 값을 미리 복사해 두는 임시 데이터 저장소를 의미한다. 주로 데이터의 접근 시간을 단축하고, 시스템의 전체적인 성능을 향상시키기 위해 사용된다.
캐시는 접근 시간이 빠른 저장소(예: RAM)에 데이터를 저장함으로써, 더 느린 저장소(예: 하드 디스크 또는 네트워크)에서 데이터를 가져오는 것보다 빠르게 정보에 접근할 수 있다. 예를 들어, 웹 사이트의 이미지, 스타일시트, 자바스크립트 파일 등은 사용자의 브라우저 캐시에 저장될 수 있어, 동일한 페이지나 사이트에 다시 접근할 때 더 빠르게 로딩될 수 있다.
캐시 관련 용어
- 캐시 히트(Cache Hit): 요청된 데이터가 캐시에 있을 때, 즉 데이터를 캐시에서 빠르게 가져올 수 있는 경우를 의미한다.
- 캐시 미스(Cache Miss): 요청된 데이터가 캐시에 없어서 느린 저장소에서 직접 데이터를 가져와야 할 때를 의미한다.
- 캐시 교체 정책(Cache Eviction Policy): 캐시가 꽉 찼을 때 어떤 데이터를 캐시에서 제거할지 결정하는 규칙이다. 가장 오래된 데이터를 제거하는 LRU(Least Recently Used), 가장 적게 사용된 데이터를 제거하는 LFU(Least Frequently Used) 등 다양한 정책이 있다.
즉, 같은 데이터에 대해 반복적인 액세스가 일어나는 경우, 원본 보다 빠른 접근 속도, 자주 변하지 않는 데이터인 필요한 경우 캐시를 효율적으로 사용할 수 있다.
Redis를 캐시로 이용하면 좋은 이유
- In-memory 데이터 저장소(RAM): Redis는 메모리 기반 데이터 저장소로서, 디스크 기반의 저장소보다 훨씬 빠른 읽기와 쓰기 작업을 제공한다.
- 빠른 성능
- 평균 작업속도 < 1ms
- 초당 수백만 건의 작업 가능
- 단순한 key-value 구조: Redis는 고성능의 key-value 저장소이다.
- 만료 정책: Redis는 자동 만료 기능을 제공한다. 특정 시간이 지난 캐시 데이터는 자동으로 삭제되어, 캐시의 신선도를 유지할 수 있고, 메모리 누수를 방지할 수 있다.
- 영속성 옵션: 필요한 경우, Redis는 메모리상의 데이터를 디스크에 저장하는 영속성 기능을 제공한다. 시스템 장애가 발생했을 때, 데이터 손실 없이 캐시 데이터를 복구할 수 있다.
- 스케일 아웃: Redis는 클러스터 모드를 지원하여, 쉽게 스케일 아웃할 수 있고 높은 가용성을 확보할 수 있다. 이를 통해 더 많은 요청과 데이터를 처리할 수 있으며, 부하 분산을 통해 캐시 시스템의 성능과 안정성을 높일 수 있다.
캐싱 전략(Caching Strategies)
읽기 전략
Look-Aside(Lazy Loading)
Look-Aside 패턴, 또는 일반적으로 Cache-Aside 패턴으로 불리는 캐싱 전략은 읽기 연산에서 Redis와 같은 캐시를 사용할 때 흔히 사용된다.
동작 방식
- 요청된 데이터의 캐시 검색: 애플리케이션은 우선 Redis 캐시에서 요청된 데이터를 검색한다.
- 캐시 히트(Cache Hit): 만약 요청된 데이터가 캐시에 있으면, 즉시 해당 데이터를 반환하고, 데이터베이스 접근 없이 빠른 응답을 제공한다.
- 캐시 미스(Cache Miss): 요청된 데이터가 캐시에 없는 경우, 애플리케이션은 데이터베이스에서 데이터를 검색한다.
- 데이터베이스에서 데이터 검색: 데이터베이스에서 필요한 데이터를 읽은 후, 그 데이터를 캐시에 저장한다. 이로 인해 다음 요청부터는 데이터에 대한 빠른 액세스가 가능해진다.
- Time-To-Live (TTL) 설정: 읽기 과정 중에 데이터를 캐시할 때, 해당 데이터의 유효 기간(TTL)을 설정할 수 있다. 이는 캐시에 저장된 데이터가 오래되어 오래된 정보를 제공하는 것을 방지하고, 자동으로 데이터를 삭제하여 메모리를 효율적으로 관리할 수 있게 한다.
Cache-Aside 패턴의 주요 장점 중 하나는 캐시와 데이터베이스 간의 일관성 유지를 애플리케이션 로직으로 처리할 수 있다는 점이다. 애플리케이션은 데이터 업데이트 시 캐시 데이터를 명시적으로 만료시키거나 업데이트할 책임이 있다. 또한, 캐시 시스템의 부하를 줄이고, 데이터베이스 요청을 최적화하는데 유용하다. 하지만, 캐시 미스 시 어플리케이션 성능에 부담을 줄 수 있으므로, 적절한 TTL 설정과 캐시 정책 관리가 필요하다.
이러한 구조는 Redis가 다운되더라도 바로 장애로 이어지지 않고 DB에서 바로 데이터를 조회할 수 있다.
대신 캐시로 붙어있던 커넥션(connection)이 많았다면 해당 커넥션이 모두 DB로 이동하므로 갑자기 DB에 많은 부하가 몰릴 수 있다.
그래서 캐시를 새로 투입하거나 혹은 DB에만 새로운 데이터를 저장한 경우, 처음에 캐시 미스가 엄청 발생해 성능 저하가 발생한다.
이럴 때는 미리 DB의 데이터를 캐시로 밀어 넣어주는 작업을 진행할 수 있는데 이러한 작업을 캐시 워밍(cache warming)이라 한다.
cache warming🌤️
캐시 워밍(cache warming)은 캐시 시스템을 초기화하거나 최적화하기 위해 미리 데이터를 캐시에 로딩하는 과정을 말한다.
시스템이 사용자 요청을 받기 전에 이미 자주 접근하는 데이터를 캐시에 저장함으로써, 사용자 요청 시 데이터를 더 빠르게 제공할 수 있게 해 준다. (실제 티켓링크에서는 상품 오픈 전 상품의 정보를 미리 DB에서 cache로 올려주는 작업을 매번 하고 있다)
서비스나 어플리케이션이 시작될 때 또는 특정 이벤트 후에 캐시를 사전에 준비하는 것이 주요 목적이다.
캐시 워밍은 특히 대규모 시스템에서 성능 향상을 도모할 수 있다.
1. 데이터 선택: 어떤 데이터를 캐시에 미리 로딩할지 결정하는 것이 중요하다. 대개 시스템에서 자주 요청되는 데이터나 조회 수가 많은 데이터가 대상이 된다.
2. 캐시 업데이트: 캐시 워밍이 수행될 시간을 결정해야 한다. 이는 서비스가 시작될 때, 저사용 시간대에, 또는 주기적으로 수행될 수 있다.
3. 자동화: 캐시 워밍 과정을 자동화하여 시스템 재시작이나 특정 트리거 발생 시 자동으로 데이터를 캐시에 로딩할 수 있다.
하지만 불필요한 데이터를 미리 캐시하게 되면 캐시 리소스를 낭비하고 시스템 성능에 부정적인 영향을 줄 수 있기 때문에, 어떤 데이터를 캐시할지 신중하게 결정하는 것이 중요하다.
쓰기 전략
Write-Around
Write-Around 전략에서는 데이터 쓰기 연산이 발생할 때 데이터를 바로 데이터베이스에만 기록하고 캐시에는 변경되지 않은 데이터를 남겨둔다. 이 방식은 캐시에 불필요한 쓰기를 줄여 캐시 자원을 절약할 수 있다는 장점이 있지만, 동일한 데이터를 곧바로 다시 읽어야 할 경우 캐시 미스가 발생하여 데이터베이스로부터 다시 읽어야 하므로 읽기 지연이 발생할 수 있다.
Write-Through
Write-Through 전략에서는 데이터를 쓸 때마다 캐시와 데이터베이스에 동시에 기록한다. 이 방식의 장점은 데이터베이스와 캐시 간의 일관성을 유지하기 쉬우며, 데이터를 읽을 때 캐시에 항상 최신 데이터가 있기 때문에 빠른 읽기가 가능하다. 하지만 모든 쓰기 연산이 캐시를 거치기 때문에 쓰기 연산의 지연 시간이 증가할 수 있다. 또한 재사용되지 않는 데이터도 캐시에 매번 저장되므로 리소스 낭비가 발생한다.
이러한 문제를 해결하기 위해, 캐시에 데이터를 저장할 때, 몇 분 또는 몇 시간 동안만 데이터를 보관하겠다는 의미인 expire time을 설정해 주는 것이 좋다.
expire time 값 관리가 장애 포인트가 될 수 있다!!👾
Redis 데이터 타입 활용하기
- 문자열(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라는 기능을 잠시 켜두는 것이 도움된다(단편화가 많이 발생한 경우).