Redis 영속성은 어떻게 설계하는가
BGSAVE의 fork() Copy-On-Write 원리부터 AOF fsync 정책, 혼합 포맷, 장애 복구, 서비스별 최적 설정까지 — Redis 영속성의 모든 트레이드오프를 추적한다.
- 01 Redis의 모든 선택은 하나의 질문에서 나온다
- 01 Redis는 왜 같은 데이터를 다르게 저장하는가
- 01 Redis 영속성은 어떻게 설계하는가
- 01 Redis 운영의 모든 병목은 단일 스레드에서 시작된다
- 01 Redis 복제는 왜 데이터를 잃는가
- 01 Spring + Redis 통합에서 직렬화가 모든 것을 결정한다
Redis는 메모리 데이터베이스다. 프로세스가 죽으면 모든 것이 사라진다. 이 문제를 해결하는 방법이 두 가지 — RDB 스냅샷과 AOF 로그 — 인데, 이 둘을 어떻게 조합하느냐가 서비스의 복구 가능성과 처리량을 동시에 결정한다. 어떤 원칙으로 이 선택을 해야 하는가?
BGSAVE와 fork() — 스냅샷이 가능한 이유
BGSAVE는 fork()로 자식 프로세스를 만들고, 자식이 메모리를 RDB 파일로 직렬화하는 동안 부모(메인 Redis)는 계속 요청을 처리한다. 핵심은 Copy-On-Write(COW) 다.
fork() 직후 부모와 자식은 동일한 물리 페이지를 공유한다. OS는 이 페이지들을 읽기 전용으로 마킹한다. 부모가 SET key "modified"를 실행하면 해당 페이지에 쓰기를 시도하고, OS가 Page Fault를 발생시켜 페이지를 복사한다. 부모는 복사본에 새 값을 쓰고, 자식은 원본을 그대로 유지한다. 자식이 RDB에 기록하는 것은 항상 fork() 시점의 일관된 스냅샷이다.
fork() 직후:
부모(가상 A) ──┐
├── 물리 페이지 A: "hello" (read-only 공유)
자식(가상 A') ──┘
부모가 SET key "modified" 실행:
부모(가상 A) ──── 물리 페이지 A': "modified" (새 복사본)
자식(가상 A') ─── 물리 페이지 A: "hello" (원본 유지)
fork() 비용은 물리 메모리를 복사하지 않지만 페이지 테이블을 복사해야 하므로 메모리 크기에 비례한다. 4 GB Redis라면 fork()에 수백 ms가 걸릴 수 있다. 이 시간 동안 이벤트 루프가 멈춘다.
redis-cli INFO stats | grep latest_fork_usec
# latest_fork_usec:350000 → 350 ms
Linux의 Transparent Huge Pages(THP)가 활성화되어 있으면 COW 시 4 KB 대신 2 MB 단위로 복사되어 메모리 사용이 폭증한다. Redis 공식 권장 사항은 THP 비활성화다.
echo never > /sys/kernel/mm/transparent_hugepage/enabled
maxmemory를 물리 RAM에 가깝게 설정하면 BGSAVE 중 COW 복사본이 쌓여 OOM Killer가 Redis를 종료한다. persistence를 사용한다면 maxmemory는 물리 RAM의 50% 이하로 설정하라.
AOF — fsync 정책이 RPO를 결정한다
AOF는 모든 쓰기 명령어를 RESP 텍스트로 순서대로 파일에 추가한다. 명령어는 먼저 OS 커널의 page cache에 write()되고, fsync()를 호출할 때 비로소 디스크에 강제 동기화된다. 서버 전원이 차단되면 page cache는 소실된다.
appendfsync 정책 세 가지가 이 관계를 다르게 설정한다.
| 정책 | 동작 | 최대 손실 | ops/sec (SSD) |
|---|---|---|---|
always | 명령어마다 fsync | ~0 | ~8,000 |
everysec | 1초마다 fsync (백그라운드) | 최대 1초 | ~95,000 |
no | OS 자체 판단 | 최대 30초 | ~110,000 |
everysec은 fsync를 백그라운드 스레드에서 실행하므로 이벤트 루프에 영향을 주지 않는다. 단, 디스크가 느려 fsync가 2초 이상 지연되면 이벤트 루프도 잠시 블로킹된다.
AOF 파일은 계속 커지므로 BGREWRITEAOF로 주기적으로 압축해야 한다. Rewrite는 fork()로 자식을 만들어 현재 메모리를 “최소 명령어 집합”으로 재직렬화한다. INCR counter를 1,000번 실행한 로그는 SET counter 1000 한 줄로 축약된다.
혼합 포맷 — AOF 안에 RDB를 넣다
순수 AOF의 약점은 재시작 속도다. 4 GB AOF를 명령어 단위로 재실행하면 15~30분이 걸린다. Redis 4.0의 aof-use-rdb-preamble yes(기본값)는 이 문제를 해결한다.
BGREWRITEAOF 실행 시 AOF 파일의 앞부분을 RDB 바이너리로 기록하고, 이후 명령어들만 RESP 텍스트로 추가한다.
appendonly.aof:
┌──────────────────────────────────┐
│ [RDB 섹션] "REDIS0011" + 전체 데이터 │ ← 바이너리, 고속 로드
├──────────────────────────────────┤
│ [AOF 섹션] Rewrite 이후 명령어들 │ ← RESP 텍스트, 재실행
└──────────────────────────────────┘
재시작 시 Redis는 파일 첫 5바이트가 REDIS이면 혼합 포맷으로 판단해 RDB 섹션을 바이너리로 고속 로드하고, AOF 섹션만 재실행한다. 재시작 시간이 순수 AOF 대비 10~20배 단축된다.
장애 복구 — 파일 선택과 손상 처리
Redis 재시작 시 파일 선택 우선순위는 단순하다.
appendonly yes+ AOF 파일 존재 → AOF 우선 (RDB 무시)
그 외 → RDB 로드
둘 다 없으면 → 빈 메모리로 시작
AOF가 활성화된 상태에서 RDB만 최신이라고 생각하고 방치하면, 재시작 시 더 오래된 AOF가 로드되어 데이터가 퇴행할 수 있다.
AOF 파일 손상(주로 크래시 중 마지막 명령어 불완전 기록)은 redis-check-aof로 진단하고 복구한다.
# 1. 손상 진단
redis-check-aof appendonly.aof
# ok_up_to=12345600, diff=78 → 78 bytes 손상
# 2. 백업 먼저
cp appendonly.aof appendonly.aof.bak
# 3. 자동 수정 (손상 지점 이후 제거)
redis-check-aof --fix appendonly.aof
# 4. 재확인
redis-check-aof appendonly.aof # diff=0 확인
aof-load-truncated yes(기본값)는 마지막 불완전 명령어를 자동으로 무시하고 로드를 계속한다. 이 설정 덕분에 대부분의 경우 수동 복구 없이 재시작된다.
서비스별 설정 — 단일 템플릿은 없다
영속성 설정의 핵심은 역할마다 다르게 설정하는 것이다. 캐시에 AOF를 켜면 불필요한 디스크 I/O가 발생하고, 결제 데이터에 appendonly no를 쓰면 데이터를 잃는다.
캐시 전용 (손실 무관):
save ""
appendonly no
maxmemory-policy allkeys-lru
maxmemory [RAM × 75%]
세션 저장소 (최대 1초 손실 허용):
appendonly yes
appendfsync everysec
aof-use-rdb-preamble yes
maxmemory-policy volatile-lru
maxmemory [RAM × 60%]
주 데이터 저장소 (손실 불허):
save 3600 1 300 100 60 10000
appendonly yes
appendfsync everysec
aof-use-rdb-preamble yes
maxmemory-policy noeviction
maxmemory [RAM × 50%]
Replica를 운영한다면 Master에는 영속성을 끄고 Replica에만 AOF를 켜는 전략이 효과적이다. Master의 처리량을 최대화하면서 내구성은 Replica가 담당한다. 단, Master를 재시작할 때는 반드시 Replica를 먼저 독립 Master로 승격해야 한다. 빈 Master가 먼저 뜨면 Replica가 빈 데이터로 동기화된다.
appendfsync always는 손실 0이지만 초당 ~8,000 ops. everysec은 최대 1초 손실에 ~95,000 ops. RDB만 쓰면 마지막 스냅샷 이후 전체 손실이 가능하지만 재시작이 가장 빠르다. 혼합 포맷은 AOF의 내구성과 RDB의 재시작 속도를 함께 가져간다.
정리
BGSAVE는fork()+ COW로 이벤트 루프를 멈추지 않고 일관된 스냅샷을 찍는다. fork() 비용은 메모리 크기에 비례하고 THP가 이를 악화시킨다.- AOF의
appendfsync정책이 RPO(최대 데이터 손실)를 결정한다.everysec이 성능과 내구성의 균형점이다. - 혼합 포맷(
aof-use-rdb-preamble yes)은 재시작 속도를 AOF 수준에서 RDB 수준으로 끌어올린다. 기본값을 바꿀 이유가 없다. - 장애 시
appendonly yes이면 AOF가 무조건 우선이다.redis-check-aof --fix로 손상된 파일을 복구하라. - 모든 Redis에 같은 설정을 적용하면 캐시는 성능을 낭비하고 중요 데이터는 손실된다. 역할마다 RPO를 결정하고 그에 맞는 최소한의 영속성을 선택하라.