Redis의 영속성
Redis는 In-memory DB이기 때문에 디스크에 저장을 하지 않으면 서버가 내려갔을 때 데이터 유실이 발생할 수 있다. 이로 인해 장애의 주 원인이 되기도 한다.
레디스에서는 이런 데이터 유실을 막기 위해 RDB(snapshotting) 방식과 AOF(Append Only File) 방식의 데이터 저장 방법을 제공한다. 두 가지 방식에 대해 이해하고, 차이점과 활용법을 알아보자.
RDB(Redis Database, snapshotting) 방식
Redis는 직접 세팅하지 않더라도, 주기적으로 인메모리 데이터를 .rdb라는 확장자를 가진 파일에 저장한다. RDB 방식은 기본적으로 설정되어 있는 백업 방식으로, 순간적으로 메모리에 있는 내용을 스냅샷(snapshot)을 떠서 하드디스크(파일)에 바이너리 파일로 저장해두는 방식이다.
Redis 프로세스가 장애로 인해 종료되더라도, 해당 파일을 읽어 이전 상태를 동일하게 복구할 수 있다. 프로세스의 장애 뿐만 아니라, 특정 시점으로 데이터를 복구하거나 데이터의 버저닝 또한 이 방식을 통해 구현할 수 있다.
메모리의 데이터를 snapshot 그대로 저장하기 때문에, 서버를 재구동할 때 그대로 읽어 빠르게 복구할 수 있다는 장점이 있다.
하지만 snapshot을 추출하는데 오래 걸리고, snapshot을 찍지 않고 서버가 꺼지면 snapshot 이후의 데이터를 모두 잃어버리게 된다.(50GB에 7~8분 소요)
RDB 저장 방식에는 SAVE와 BGSAVE(Background Save) 두 가지가 있다.
•
SAVE
순간적으로 Redis의 동작을 정지시키고, 그 상태의 snapshot을 저장(blocking 방식)
•
BGSAVE
별도의 자식 프로세스를 띄워 Redis는 동작을 멈추지 않은 채, 명령어 수행 당시의 snapshot을 저장(non-blocking 방식)
일반적으로 BGSAVE 방식을 사용하는 것이 좋지만, BGSAVE 방식을 사용하면 메모리 사용률에 유의해야한다.
AOF(Append Only File) 방식
AOF 방식은 redis의 모든 write, update 연산 자체를 log파일에 모두 기록하는 형태이다. 조회를 제외한 입력/수정/삭제 명령이 실행될 때마다 default로 appendonly.aof 파일에 기록된다.
서버가 재시작될 때마다, log에 기록된 write, update 연산을 순서대로 재실행하여 데이터를 복구할 수 있다. 명령어가 수행될 때마다 non-blocking 방식으로 log를 저장하기 때문에, 항상 현재 시점까지의 로그를 기록하여 서버가 내려가도 데이터 유실이 없다.
AOF 방식은 명령어마다 log파일에 append하기 때문에 log write하는 속도가 빠르고, 서버가 다운되더라도 데이터 유실이 없다는 장점이 있다. 또한 AOF파일은 텍스트 파일로 편집이 가능하여, 필요한 로그만 수행하거나 불필요한 로그를 손쉽게 삭제할 수 있다.
하지만 모든 write, update 연산을 log파일에 남기기 때문에 log 데이터 양이 굉장히 커질 수 있고, 서버 복구 시 저장된 log 파일에 저장된 모든 연산을 재실행하기 때문에 redis 재시작 속도가 느리다는 단점이 있다.
AOF 방식에는 이러한 단점을 보완하기 위해, AOF 파일의 상태가 특정 조건(파일의 크기가 얼마 이상)일 때 파일을 현재 상태에 맞춰서 덮어쓰거나 새로 생성하는 rewrite 기능이 있다.
RDB vs AOF
RDB 방식의 문제는 서버 장애 발생 시, 백업 이후 서버 장애가 발생한 시점까지의 데이터가 유실된다는 것이다.
AOF 방식의 문제는 서버 장애 발생 시, 데이터 복구를 위해 기록된 모든 로그를 순서대로 재실행 해야하고 그로 인해 서버 재실행 속도가 느리다는 것이다.
각각의 방식은 trade-off가 있으니, 애플리케이션의 환경에 따라 적합한 방식을 사용하는 것이 좋다.
일반적인 캐시 서버라면 RDB 방식의 백업은 굳이 필요가 없다. 백업은 필요하지만 어느 정도의 데이터 손실이 발생해도 괜찮다면 RDB 방식을, 모든 데이터가 보장되어야 하는 경우라면 AOF 방식을 사용해야한다.
하지만 RDB 방식과 AOF 방식을 혼용해서 사용하는 것이 가장 좋다. 주기적으로 RDB(snapshotting)으로 백업하고, 다음 snapshot까지의 저장을 AOF 방식으로 하여 데이터의 손실 없이 각 방식의 장점을 취하자.
Redis 주의 사항
•
위에서도 언급한 것처럼, 인메모리 데이터베이스 특성상 서버에 장애가 발생한 경우에 데이터 유실이 발생할 수 있기 때문에 그에 대한 대비가 필요하다.
•
Redis는 싱글 스레드로 동작하는데, 이 때문에 한 번에 하나의 명령어만 처리할 수 있고 처리가 오래 걸리는 로직이나 명령은 피해야한다.
•
서버와 같은 컴퓨터나 인스턴스에 둔다면, 메모리를 서로 경쟁하기 때문에 메모리에 대한 관리가 필요하다.
Cabi의 Redis 백업 이슈
Cabi에서도 Redis를 사용하는데, 일전에 sichoi님이 Redis 서버가 내려갔다 올라오는 경우 모든 데이터를 잃어버리는 부분을 언급하신 적이 있다.
현재 Cabi에 적용된 Redis의 사용처는 크게 두 가지가 있다.
1.
공유 사물함 Session 참여
2.
사물함의 이전 사용자 이름 저장(캐싱)
당시 이 문제를 접했을 때는 6차 분들의 선발과 리팩토링 작업을 진행 중이여서, 공유 사물함의 Session이 날아가는 경우와 사물함의 이전 사용자 이름을 잃어버리는 상황이 크리티컬하지 않기 때문에 추후 해결하자는 이유로 미루어두었다. 위에서 공부한 Redis 백업 기능을 추가하여 해당 문제를 해결해보고자 한다.
백업 설정하기
우선 현재 Cabi에서 Redis에 적용된 설정을 살펴보자.
docker-compose.yml 파일을 보면 별도로 config를 설정하는 부분이 지정되어있지 않다.
때문에 redis 컨테이너에 들어가서 여러 설정들을 살펴보면, 이처럼 모든 설정값들이 기본값(default)으로 되어있다. 기본적으로 RDB 방식으로 저장하며 /data 디렉토리에 dump.rdb의 이름으로 저장하고, AOF는 설정되어있지 않다.
save 3600 1 300 100 60 10000
Java
복사
RDB의 default 설정은 1시간마다 1건 이상 발생 시 + 5분마다 100건 이상 발생 시 + 1분마다 10000건 이상 발생 시 snapshot을 새로 저장하도록 되어있다.
우선 redis.conf 파일을 생성하여 설정 사항을 추가해보자.
# 사용 포트 관리
port 6379
# Redis 에서 사용할 수 있는 최대 메모리 용량. 지정하지 않으면 시스템 전체 용량
maxmemory 1g
# maxmemory 에 설정된 용량을 초과했을때 삭제할 데이터 선정 방식
# - noeviction : 쓰기 동작에 대해 error 반환 (Default)
# - volatile-lru : expire 가 설정된 key 들중에서 LRU algorithm 에 의해서 선택된 key 제거
# - allkeys-lru : 모든 key 들 중 LRU algorithm에 의해서 선택된 key 제거
# - volatile-random : expire 가 설정된 key 들 중 임의의 key 제거
# - allkeys-random : 모든 key 들 중 임의의 key 제거
# - volatile-ttl : expire time(TTL)이 가장 적게 남은 key 제거 (minor TTL)
maxmemory-policy noeviction
# DB 데이터를 주기적으로 파일로 백업하기 위한 설정
# Redis 가 재시작되면 이 백업을 통해 DB 를 복구
save 43200 10
save 10800 500
save 3600 1000
appendonly yes
dir /data
YAML
복사
RDB의 save 주기를 12시간마다 10건 이상 발생 시 + 3시간마다 1000건 이상 발생 시 + 1시간마다 5000건 이상 발생 시 저장하도록 설정하였다.
또한 RDB + AOF 방식을 적용하기 위해 appendonly yes도 추가해주었다. 이와 같이 설정하면, 주기적으로 RDB 스냅샷을 저장하면서 aof 파일을 rewrite하여 데이터 손실 없이 저장하면서도 각각의 단점을 보완할 수 있다.
RDB와 AOF 파일이 컨테이너 내부에 저장해둔다면 redis 서버가 내려가면 백업 데이터도 같이 삭제되므로, rdb와 aof 파일을 저장해 두기 위한 volume을 추가해야한다. docker-compose.yml 파일에 volume 설정을 추가하고, 작성한 redis.conf 파일을 volume에 넣으면서 command를 통해 해당 config 파일을 설정 파일로 사용하도록 추가했다.
컨테이너를 띄운 후 설정이 잘 적용이 되었는지 확인해보자.
작성한 conf 파일에 맞춰 잘 설정되어 있다.
local 서버에서 새로 데이터를 추가해보면서 aof 파일이 맞게 잘 작성되는지 확인해보자.
비어있는 redis에서 시작하여,
현재 대여 중인 사물함을 반납하고
공유 사물함에 대여를 눌러 세션을 새로 만들었다.
key에는 당연히 잘 들어있고,
aof 파일에 사물함 반납으로 인한 set previousUser + 공유 사물함 대여로 인한 set shadow, user, cabinet + expireat 설정 모두 잘 기록되어있다.
새로운 동작이 수행될 때마다 aof 파일이 수정되는 것도 확인했다.
이벤트 처리가 안되는 문제 발생
위 과정에서 설정도 잘 되고 저장도 잘 되길래 해치웠나? 생각하면서, 마지막으로 서버를 유지한 채로 docker만 내렸다가 올렸을 때도 데이터가 잘 복구 되는지 확인해보았다.
이처럼 공유 사물함을 대여하여 세션을 만들고 컨테이너를 내렸다가 다시 올렸다.
그러니 이처럼 대여 중인 사람으로 보이지만, 실제 대여 중이 아니라 반납이 되지 않는 문제가 발생했다.
redis에 저장된 key 값 변화를 살펴보니,
컨테이너를 내렸다가 올려도 위처럼 데이터는 잘 보존이 되었다. 하지만 공유 사물함 세션이 시간이 지나 만료되어도 82:shadow만 지워지고 애플리케이션에서 설정해둔 만료 시 이벤트 처리가 전혀 되지 않았다. 때문에 해당 사물함에 451번 유저(jpark2)가 남아있는 것처럼 보이는 현상이 발생했다.
원인과 해결
먼저 aof 파일의 PEXPIREAT과 만료 시간 이후 shadow key가 삭제되는 것을 보면, expire 설정 자체는 잘 저장되고 컨테이너를 내렸다가 올려도 복구가 되는 것 같다.
다음으로 TTL(Time To Live)을 확인해 보았는데, key 451:user나 82의 경우에는 애플리케이션 비즈니스 로직으로 처리되는 부분이라 따로 설정이 되어있지 않는 것을 확인했다.
원인을 정확히 모르겠어서 고민을 하던 중, 애플리케이션 서버를 내렸다가 올렸더니 다시 제대로 이벤트 처리가 이루어졌었다. 이 부분에서 힌트를 얻어 고민을 해보니, 애플리케이션의 경우 레디스 키 만료 이벤트를 처리해주는 KeyExpirationEventMessageListener 클래스를 주입받아 RedisConfig를 빈으로 등록해두고 애플리케이션을 실행시킨다. 이 과정에서 KeyExpirationEventMessageListener 클래스가 별도의 이벤트 작업을 처리해줄 것 같다는 생각이 들어, 동작이 되지 않는 상태와 정상 동작하는 상태의 이벤트 등록 여부를 확인해보았다.
이처럼 정상적으로 동작하지 않을 때는 아무 이벤트가 등록이 되어있지 않은데 반해, 애플리케이션 서버를 다시 켜는 경우에 AE(모든 이벤트)에 대해 이벤트가 등록된다.
레디스 컨테이너를 내렸다가 올리면 등록된 이벤트가 날아가는 것이 원인 임을 알았으니, 아예 redis.conf에서 만료 이벤트에 대해서 미리 등록을 해버리면 다시 켜지더라도 config 설정이 유지 될 것이다.
# Expiration에 대한 Event 설정
notify-keyspace-events Ex
YAML
복사
redis.conf에 이와 같이 만료 이벤트에 대해 등록을 해두었더니,
이처럼 만료 이벤트를 애플리케이션 서버에 잘 전달하여, 세션에 남은 유저를 삭제하는 로직을 수행하는 것을 확인했다.