[Redis] Redis 야무지게 사용하기
[NHN FORWARD 2021] Redis 야무지게 사용하기
1. Redis 캐시로 사용하기
Caching이란
- Temporary Location For Speed (속도 향상을 위해 사용)
- 데이터의 원래 소스보다 더 빠르고 효율적으로 액세스할 수 있는 임시 데이터 저장소
- 캐시에 접근하는게 원본에 접근하는 것보다 빨라야 한다!
- cache를 쓰면 좋은 상황
- 동일한 데이터에 대해 반복적으로 액세스하는 상황이 많을 때 cache 사용
- 잘 변하지 않는 데이터일수록
Redis란
- Most popular software caching solution
- 단순한 key-value 구조 → 어떤 데이터라도 사용 간편
- In-memory 데이터 저장소(RAM) → 모든 데이터를 메모리에 올려둠 → 빠름
- 빠른 성능 ↔ RDB
- 평균 작업속도 < 1 ms
- 초당 수백만 건의 작업 가능
⇒ 지연시간 감소, 처리량 증가
캐싱 전략 (Caching Strategies)
: Redis를 cache로 사용할 때 어떻게 배치하는 지 (데이터 유형, 해당 데이터에 대한 액세스 패턴 고려)
- 읽기 전략 (데이터를 읽는 전략이 많은 경우)
-
Look-Aside(Lazy Loading)
- 가장 일반적으로 사용하는 방법
- Application은 cache에서 데이터를 먼저 찾음 → 찾았다! (Cache Hit)
- Application은 cache에서 데이터를 먼저 찾음 → 없다 → DB에서 데이터를 찾고 Cache 저장 (Cache Miss)
- 데이터가 없을 때만 Cache에 write 작업 ⇒ Lazy Loading
장점
- 레디스가 다운되더라도 바로 장애로 이어지지 않고 DB에서 데이터를 가지고 올 수 있음 (책임이 많지 않다)
단점
- cache로 붙어있던 커넥션이 많이 있는 경우, 커넥션이 모두 데이터베이스로 붙기 때문에 DB에 갑자기 많은 부하가 몰릴 수 있다.
- cache를 새로 투입하거나 DB에만 새로운 데이터를 저장한 경우, 초반에 cache miss가 많이 발생 → 성능 저하
- ⇒ Cache Warming : 미리 DB에서 cache로 데이터를 밀어 넣어주는 작업 (ex. 티켓링크)
-
- 쓰기 전략
-
Write-Around
- DB에만 데이터를 저장
- cache miss가 발생한 경우에만, cache에 데이터를 끌어옴
단점
- cache 내의 데이터와 DB내의 데이터가 다를 수 있다.
-
Write-Through
- DB와 cache에 함께 저장
장점
- cache가 항상 최신 정보를 가지고 있다.
단점
- 저장할 때마다 두 단계 스텝 (redis, db 두 번 저장) → 상대적으로 느리다.
- 저장하는 데이터가 재사용되지 않을 수 있는데 무조건 캐시에 넣음 → 리소스 낭비
- ⇒ Expire Time 설정
-
2. Redis 데이터 타입 아무지게 활용하기
레디스는 자체적으로 많은 자료구조 제공한다. → 어떨 때 사용해야 할까
[Strings, Bitmaps, Lists, Hashes, Sets, Sorted Sets, HyperLogLogs, Streams]
- Strings
- 가장 기본적인 데이터 타입
set
커맨드를 이용해 저장되는 데이터 → string
- Bitmaps
- string의 변형
- bit 단위 연산 가능
- Lists
- 데이터 순서대로 저장 → Queue로 사용하기 적절
- Hashes
- 하나의 key 안에 여러 개의 (field와 value) 쌍으로 데이터를 저장.
- Sets
- 중복되지 않은 문자열의 집합
- Sorted Sets
- 중복되지 않은 값을 저장
- 모든 값이 score라는 숫자 값으로 정렬 (score가 같다면 사전 순)
- HyperLogLogs
- 많은 데이터를 다룰 때 사용
- 중복되지 않는 값의 개수를 카운트할 때 사용
- Stream
- log를 저장하기 좋은 자료구조
Best Practice (상황별 자료구조)
- Counting 상황
- 키 하나를 만들어서 카운팅할 상황마다 하나씩 증가
-
String의
increment
함수 사용 - Bit
- 저장공간 절약, But 모든 데이터를 정수로 표현해야 함
- ex) 서비스에 오늘 접속한 유저 수 = 날짜 key 생성 → 유저 ID에 해당하는 bit를 1 올려줌 bit 수 == 유저 수 ⇒ 1000만명 == 1000만 bit ⇒ 1.2MB
- HyperLogLogs
- 모든 string 데이터 값을 유니크하게 구분
- set과 비슷, But 대량의 데이터 카운팅에 더 적절 (오차 0.81%)
- 저장되는 데이터 개수에 상관없이 모든 값이 12KB로 고정되어 저장
- 한번 저장된 값은 다시 불러올 수 없음 → 데이터 보호 목적으로도 사용 가능
- ex) 웹 사이트에 방문한 IP가 유니크하게 몇 개가 있는지, 하루 종일 크롤링한 URL의 개수가 몇 개인지, 검색 엔진에서 검색된 유니크한 단어가 몇 개가 되는지
- Messging 상황
- Lists
- 메시지 큐로 사용하기 적절
- 자체적으로 Blocking 기능을 제공 → 불필요한 polling 방지
- Message Queue
비동기로 사용, 쌓아놨다가 사용한다.
- Blocking
메시지가 있을 때까지 기다린다. → polling 이 필요 없다.
(polling: 상태를 주기적으로 검사하여 일정 조건을 만족할 때 송수신 등의 자료 처리를 하는 방식)LPUSHX
/RPUSHX
: 키가 있을 때만 리스트에 데이터를 저장 가능- key가 이미 있다 == 예전에 사용했던 큐이다. ⇒ 사용했던 queue에만 메시지를 넣어줄 수 있다 ⇒ 비효율적인 데이터 이동 방지
- ex) 트위터에서는 각 유저의 타임라인에 보일 트윗을 캐싱하기 위해 레디스의 리스트 사용 (RPUSHX) 트위터를 자주 이용하던 유저의 타임라인에만 새로운 데이터를 미리 캐시 자주 사용하지 않는 유저는 caching key 자체가 존재하지 않기 때문에 데이터 넣지 않음
- Streams
- 로그를 저장하기 가장 적절한 자료구조
- append-only (실제 서버에 로그가 쌓이는 것처럼, 중간에 데이터 불변)
-
시간 범위로 검색(ID값 이용) / 신규 추가 데이터 수신(tail -f) / 소비자별 다른 데이터 수신(소비자 그룹) (like kafka)
소비자 그룹은 유튜브 구독자 같이 동시에 이 사람들에게 알림을 던져준다는 느낌
-
카프카 개념 차용
- ‘*’ (ID값): 레디스가 알아서 저장하고 ID 값을 반환 (데이터가 저장된 시간)
- key value 형식 - sensor-id ← 1234, temperature ← 20.4
- Lists
3. Redis에서 데이터를 영구 저장하려면? (RDB vs AOF)
Redis = In-memory 데이터 스토어 (모든 데이터 메모리 저장)
- 서버나 레디스 인스턴스가 재시작 시 모든 데이터 유실
- 복제 기능을 사용해도 코드 버그 or 사람의 실수 발생 시 데이터 복원 불가
- Redis를 캐시 이외의 용도로 사용한다면 적절한 데이터 백업 필요
Redis Persistence Option
- AOF (Append Only File)
- 데이터를 변경하는 커맨드를 모두 저장
- 데이터가 추가되기만 해서 대부분 RDB 파일보다 커짐 ⇒ 주기적으로 압축
-
레디스 프로토콜 형태 저장 (can’t read)
- RDB (snapshot)
- 저장 당시의 메모리에 있는 데이터 그대로 사진 찍듯 찍어서 파일로 저장
-
바이너리 파일 형태 저장 (can’t read)
자동 / 수동 파일 저장 방법
- RDB
- 자동: redis.conf 파일에서 SAVE 옵션 (시간 기준)
- 수동:
BGSAVE
커맨드를 이용해 cli 창에서 수동으로 RDB 파일 저장SAVE
커맨드는 절대 사용 X
⇒ 레디스는 싱글 스레드, 저장하는 동안 blocking 되기 때문에 다른 작업 수행 불가
https://velog.io/@banggeunho/레디스Redis-알고-쓰자.-정의-저장방식-아키텍처-자료구조-유효-기간
- AOF
- 자동: redis.conf 파일에서 auto-aof-rewrite-percentage 옵션 (크기 기준)
- 수동:
BGREWRITEAOF
커맨드를 이용해 CLI창에서 수동으로 AOF 파일 재작성
RDB vs AOF 선택 기준
- Redis를 cache로만 사용할 거면 필요 X
-
백업은 필요하지만 어느 정도의 데이터 손실이 발생해도 괜찮은 경우
→ RDB 단독 사용
→ 대신, redis.conf 파일에서 SAVE 옵션을 적절히 사용
(ex. SAVE 900 1 - 900초 동안 1개 이상의 키가 변경되었을 때 RDB 파일을 재작성) - 장애 상황 직전까지의 모든 데이터가 보장되어야 할 경우
- AOF 사용
APPENDFSYNC
옵션이 everysec인 경우 최대 1초 사이의 데이터 유실 가능 (default)
- 제일 강력한 내구성이 필요한 경우
-
RDB & AOF 동시 사용
RDB로 구간을 찾은 다음 AOF로 세밀하게 데이터 복구를 진행한다.
AOF는 시간이 오래걸리고 RDB의 경우 시간이 복구 시간이 빨리 걸린다.
하지만 AOF는 세밀하게 데이터 복구를 진행한다. 안에 데이터 명령어가 많이 들어가 있어서
-
4. Redis 아키텍처 선택 노하우 (Replication vs Sentinel vs Cluster)
Redis 아키텍처의 종류
- Replication
- master, replica 노드 두개만 존재하는 간단한 구성
단순히 복제 연결
replicaof
커맨드를 이용해 간단하게 복제 연결- 비동기식 복제 (마스터에서 복제본에 데이터가 잘 전달됐는지 기다리지 않음)
- HA 기능이 없으므로 장애 상황 시 수동 복구
- 리플리카 노드에 직접 접속해서 복제 중단
replicaof no one
- 애플리케이션에서 연결 정보 변경 → 배포
- 리플리카 노드에 직접 접속해서 복제 중단
HA 기능 :
시스템이 오랜 기간 동안 정상 운영이 가능하도록 하는 성질,
예시로 서버의 이중화 (하나의 서버에서 문제가 발생하더라도 다른 서버를 통해 서비스를 계속해서 사용할 수 있게 함)
즉, 자동으로 페일오버 (이상이 생겼을 때 예비 시스템으로 자동전환되는 기능, 장애 극복기능)
⇒ 같은 말인 것 같다
- Sentinel
- master, replica, sentinel 노드 총 5개 이상 홀수 존재
- sentinal: 일반 노드들을 모니터링
자동 페일오버 가능한 HA 구성(High Availability)
- 마스터가 비정상 상태일 때 자동으로 페일오버
- 기존의 리플리카 노드 → 마스터
- 연결 정보 변경 필요 없음
- 애플리케이션에서는 센티널 노드만 알고 있으면 되고센티널이 변경된 마스터 정보로 바로 연결해줌
- sentinel 노드는 항상 3대 이상의 홀수로 존재해야 함
- 과반수 이상의 sentinel이 동의해야 페일오버 진행
<NHN 방식>
두 대의 서버에는 일반 레디스와 센티널을 함께 띄우고
최저 사양의 다른 서버에는 센티널 노드만 올려 사용
- Cluster
- 최소 3대의 마스터 필요
- 리플리카 노드 하나씩 추가 (일반적)
-
Sharding 기능
: 데이터를 조각내 분산 저장하는 데이터 처리 기법
- 데이터(키)가 여러 마스터 노드에 자동으로 분할되어 저장
- 서비스의 확장을 위한 scale out
스케일 아웃과 HA 구성(High Availiability)
- 모든 노드가 서로를 감시하여, 마스터노드가 비정상 상태일 때 자동 페일오버
- 최소 3대의 마스터 필요
아키텍처 선택 기준
- Stand-Alone : 마스터 하나만 띄움
5. Redis 운영 꿀팁 + 장애 포인트
Redis - 싱글 스레드로 동작
→ 사용자가 오래 걸리는 커맨드를 실행한다면 나머지 모든 요청들은 수행하지 못하고 대기 상태 → 장애 발생
keys *
- 모든 키를 보여주는 커맨드scan 0
으로 대체- 재귀적으로 key를 호출
- hash, sorted set 경우 키 내부에 아이템이 많아질수록 성능 저하
- 하나의 키에 최대 100만 개 미만으로 저장
- key에 많은 데이터가 들어있을 때
del
로 데이터 삭제 → 다른 커맨드 대기unlink
대체 - key를 백그라운드로 삭제
변경하면 장애를 막을 수 있는 기본 설정값
STOP-WRITES-ON-BGSAVE-ERROR = NO
- yes (default)
- RDB 파일 저장 실패 시 redis로의 모든 write 불가능
- 레디스 서버에 대한 모니터링을 적절히 하고 있다는 전제 하에
MAXMEMORY-POLICY = ALLKEYS-LRU
- redis를 캐시로 사용할 때 Expire Time 설정 권장
- 메모리가 가득 찼을 때 MAXMEMORY-POLICY 정책에 의해 키 관리
- noeviction (default) : 삭제 안함, 더 이상 새로운 키를 저장하지 않는다.
- valiatile-lru : 가장 최근에 사용하지 않았던 key부터 삭제 (expire 설정이 있는 것 key값만 삭제)
- expire 설정을 안한 것들만 남아있는 경우 더 이상 삭제를 하지 않고 새로운 키를 저장하지 않음
-
allkeys-lru (추천) : 모든 키에 대해 LRU 방식으로 key를 삭제
Cache Stampede
대규모의 트래픽 환경에서 TTL 값을 너무 작게 설정한 경우 발생
Look-aside 과정에서 key가 만료되는 순간, 많은 서버에서 이 key를 보고 있었다면 그 모든 어플리케이션 서버들이 DB에 가서 같은 데이터를 찾게 되는 duplicate read가 발생
또 읽어온 값을 레디스에 각각 write하는 duplicate write도 발생
→ 비효율적, 처리량 저하, 불필요한 작업 증가 → 장애 발생
ex) 티켓 링크에서 티켓 오픈 시 하나의 공연 데이터를 읽기 위해 몇십 개의 애플리케이션 서버에서 커넥션이 연결
→ 해결 방법 : 부하가 발생했을 때의 상황을 자세하게 분석하기 위해 개발 쪽의 스카우터 로그를 확인하여 알아챔 → TTL 시간늘 여유있게 늘림
MaxMemory 값 설정
레디스의 데이터를 파일로 저장할 때 fork를 통해 자식 프로세스를 생성
자식 프로세스로 백그라운드에서는 데이터를 파일로 저장, 원래의 프로세스는 계속해서 일반적인 요청을 받아 데이터를 처리
→ Copy-on-Write 덕분
Persistence / 복제 사용시 MaxMemory 설정 주의
- RDB 저장 & AOF rewrite 시
fork()
- Copy-on-Write로 인해 메모리를 복사해서 서버의 메모리 사용률이 두 배로 증가하는 경우 발생 가능
- Persistence / 복제 사용 시 MaxMemory는 실제 메모리의 절반으로 설정 ex) 4GB → 2048MB
- 데이터를 영구 저장하지 않더라도 복제 기능을 사용하고 있다면 주의
- 복제 연결을 처음 시도하거나, 혹은 연결이 끊겨 재시도를 할 때에 새로 RDB 파일을 저장하는 과정을 거치기 때문
Memory 관리
물리적으로 사용되고 있는 메모리를 모니터링
used_memory
: 논리적으로 Redis가 사용하는 메모리used_memory_rss
: OS가 Redis에 할당하기 위해 사용한 물리적 메모리 양used_memory
보다used_moery_rss
보는게 더 중요하다.- 실제 저장된 데이터 보다 RSS 값이 큰 상황
- 차이가 클 때 framentation이 크다
- 삭제되는 키가 많으면 fragmentation 증가
- 특정 시점에 피크를 찍고 다시 삭제되는 경우
- TTL로 인한 eviction이 많이 발생하는 경우
CONFIG SET activedefrag yes
: 단편화가 많이 발생했을 때 켜두는 것을 권장