메모리에 있는 데이터는 프로세스가 죽으면 사라진다. 재시작 후에도 남기려면 디스크에 써야 하고, 방식이 둘이다.
RDB — 특정 시점 스냅샷
그 순간의 전체 데이터를 파일 하나로 덤프한다. 핵심은 자식 프로세스를 fork 해서 뜬다는 점이다.
fork 는 부모의 메모리를 복사하지 않는다. 페이지 테이블만 복사해 같은 물리 페이지를 가리키게 한 뒤 양쪽 모두 읽기 전용으로 표시하고, 어느 쪽이든 쓰려 하면 그때 그 페이지만 복사한다(copy-on-write). 그래서 자식이 스냅샷을 조용히 써 내려가는 동안 부모(메인 스레드)는 계속 요청을 받는다.
- 파일이 작고 로딩이 빠르다 — 복구 시간이 짧다
- 마지막 스냅샷 이후의 쓰기는 잃는다
- 쓰기가 몰리는 순간
fork하면 위험하다 — 부모가 건드리는 페이지마다 복사가 일어나 메모리가 최대 두 배까지 튈 수 있다. 여유 메모리 없이 운영하다 스냅샷 도중 OOM 이 나는 사고가 여기서 온다
AOF — 모든 쓰기 명령을 append
받은 쓰기 명령을 로그에 계속 덧붙인다. 복구는 그 로그를 처음부터 재생하는 것이다. fsync 주기를 고를 수 있다.
| 설정 | 성격 |
|---|---|
always | 매 쓰기마다 디스크에 확정. 가장 안전하고 가장 느리다 |
everysec | 1초마다. 최대 1초어치를 잃는다 — 실무 기본값 |
no | OS 에 맡긴다. 빠르지만 유실 구간이 길다 |
write 와 fsync 는 다르다
여기가 요점이다. 파일에 write 하면 데이터는 OS 페이지 캐시까지만 간다. 애플리케이션 입장에서는 "썼다" 지만 아직 메모리에 있을 뿐이다. 전원이 나가면 사라진다.
fsync 는 "실제 저장 장치에 내려앉을 때까지 기다려라" 는 호출이다. 디스크에 썼다 와 디스크에 확정됐다 는 다르고, 그 사이를 메우는 것이 fsync 다. DB 의 WAL 도, 파일 시스템 저널도 같은 지점에서 같은 고민을 한다.
로그는 계속 자란다
INCR count 를 백만 번 하면 로그에 백만 줄이 쌓이지만 결과는 SET count 1000000 하나다. 그래서 주기적으로 재작성(rewrite) 한다 — 현재 상태를 만들어 내는 최소 명령 집합으로 압축하는 것이다. 이때도 자식 프로세스를 fork 한다.
최근 방식은 둘을 섞는다. 재작성 시 앞부분을 RDB 형식으로 쓰고 그 뒤로 새 명령을 AOF 로 덧붙인다. 로딩은 RDB 처럼 빠르고 유실은 AOF 만큼 적다.
복제는 영속성이 아니다
둘을 섞어 생각하기 쉬운데 다른 문제를 푼다.
- 복제 — 지금 이 순간 다른 노드에도 같은 데이터가 있게 한다. 노드 하나가 죽어도 서비스가 이어진다
- 영속성 — 과거의 어느 시점을 디스크에 남긴다. 전체가 죽어도 되살릴 수 있다
복제는 잘못된 삭제까지 충실하게 따라 한다. 누군가 FLUSHALL 을 치면 복제본에서도 즉시 사라진다. 그 상황에서 우리를 구하는 것은 어제 떠 둔 RDB 파일뿐이다. 백업과 복제는 서로를 대체하지 못한다.
한편 복제본을 처음 붙일 때 마스터는 RDB 스냅샷을 떠서 보낸다 — 영속성 기능이 복제의 준비 과정에도 쓰이는 셈이다.
실무에서는
둘을 같이 켠다. AOF 로 유실을 줄이고 RDB 스냅샷을 백업·이관용으로 쓴다.
순수 캐시로만 쓴다면 둘 다 꺼도 된다. 잃어도 DB 에서 다시 채우면 그만이고, 디스크 쓰기와 fork 가 없어 지연이 안정된다. 다만 재시작 직후 캐시가 텅 비므로 부하가 통째로 DB 로 간다는 점을 계산에 넣어야 한다.
면접 함정
- ❌ "AOF 를 켜면 데이터를 안 잃는다" → 기본값
everysec은 1초를 잃을 수 있다. - ❌ "RDB 저장 중에는 서비스가 멈춘다" →
fork한 자식이 쓰므로 멈추지 않는다. 다만fork자체와 copy-on-write 복사는 부하를 준다. - ❌ "복제를 걸었으니 영속성은 필요 없다" → 복제는 잘못된 삭제까지 그대로 따라 한다. 백업과 복제는 다른 문제를 푼다.