Theme:

인메모리 데이터베이스가 갑자기 꺼지면 데이터가 전부 사라지는 걸까요? Redis는 이 문제를 어떻게 해결할까요?

개념 정의

RDB(Redis Database) 스냅샷은 특정 시점의 Redis 데이터를 바이너리 파일로 저장하는 영속성 메커니즘입니다. fork() 시스템 콜과 Copy-on-Write(CoW) 를 활용하여 서비스 중단 없이 스냅샷을 생성합니다.

왜 필요한가

Redis는 인메모리 데이터베이스이므로 프로세스가 종료되면 데이터가 사라집니다.

  • 서버 재시작: 배포, 업그레이드 후 데이터 복구
  • 장애 복구: 예기치 않은 크래시에서 데이터 보호
  • 데이터 마이그레이션: RDB 파일을 다른 서버로 복사
  • 백업: 주기적 데이터 백업

RDB 저장 방식

SAVE — 동기식 (프로덕션에서 사용 금지)

BASH
# 메인 스레드가 직접 저장 — 완료될 때까지 모든 요청 블로킹
SAVE

# 데이터가 10GB라면 수십 초간 서비스 중단

BGSAVE — 비동기식 (권장)

BASH
# 자식 프로세스를 생성하여 백그라운드에서 저장
BGSAVE

# 결과 확인
LASTSAVE    # 마지막 성공적 저장의 Unix timestamp
INFO persistence
# rdb_last_save_time: 1711238400
# rdb_last_bgsave_status: ok
# rdb_last_bgsave_time_sec: 3

자동 저장 설정

BASH
# redis.conf — 조건 기반 자동 BGSAVE
save 900 1      # 900초(15분) 동안 1회 이상 변경 시
save 300 10     # 300초(5분) 동안 10회 이상 변경 시
save 60 10000   # 60초(1분) 동안 10000회 이상 변경 시

# 자동 저장 비활성화
save ""

# RDB 파일 경로
dir /var/lib/redis/
dbfilename dump.rdb

# RDB 압축 (기본: yes)
rdbcompression yes

# RDB 체크섬 (기본: yes)
rdbchecksum yes

fork()와 Copy-on-Write

BGSAVE의 동작 흐름

PLAINTEXT
1. BGSAVE 명령 수신
2. fork() 시스템 콜 호출
   ┌─────────────────────────────────────┐
   │        fork() 실행                   │
   │                                     │
   │  부모 프로세스          자식 프로세스  │
   │  (Redis 서버)          (RDB 저장)    │
   │  ┌───────────┐       ┌───────────┐  │
   │  │ 계속 명령어 │       │ 메모리     │  │
   │  │ 처리       │       │ 스냅샷을   │  │
   │  │           │       │ 디스크에   │  │
   │  │           │       │ 저장      │  │
   │  └───────────┘       └───────────┘  │
   │       │                    │        │
   │       │  CoW 공유 메모리    │        │
   │       └────────┬───────────┘        │
   │                │                    │
   │     ┌──────────┴──────────┐         │
   │     │  물리 메모리 페이지   │         │
   │     │  (읽기 전용 공유)    │         │
   │     └─────────────────────┘         │
   └─────────────────────────────────────┘
3. 자식 프로세스가 RDB 파일 저장 완료
4. 부모 프로세스에 완료 시그널 전송
5. 자식 프로세스 종료

Copy-on-Write 상세

PLAINTEXT
fork() 직후:
  부모 페이지 테이블 → ┐
                       ├→ 물리 페이지 A (읽기 전용)
  자식 페이지 테이블 → ┘

부모가 페이지 A를 수정할 때:
  1. OS가 페이지 폴트 발생
  2. 페이지 A를 복사하여 새로운 페이지 A' 생성
  3. 부모는 A'에 쓰기, 자식은 여전히 원본 A를 참조

  부모 → 페이지 A' (수정된 데이터)
  자식 → 페이지 A  (fork 시점의 원본 데이터)

메모리 사용량

PLAINTEXT
시나리오: Redis 메모리 10GB, 쓰기 비율 30%

fork 직후:
  부모: 10GB (논리), 자식: 10GB (논리)
  실제 물리 메모리: ~10GB (CoW로 공유)

BGSAVE 진행 중 (쓰기 30%):
  수정된 페이지만 복사: 10GB × 30% = 3GB 추가
  실제 물리 메모리: ~13GB

→ 서버 메모리는 최소 데이터 크기의 1.5배 확보 필요

THP (Transparent Huge Pages) 주의

BASH
# THP가 활성화되면 CoW 복사 단위가 4KB → 2MB로 커짐
# 소량의 수정에도 대량의 메모리가 복사될 수 있음

# THP 비활성화 (Redis 공식 권장)
echo never > /sys/kernel/mm/transparent_hugepage/enabled

# redis.log에서 경고 확인
# WARNING you have Transparent Huge Pages (THP) support enabled

fork() 지연 (Fork Latency)

fork() 자체는 Copy-on-Write 덕분에 빠르지만, 페이지 테이블 복사 비용이 있습니다.

PLAINTEXT
데이터 크기별 fork 지연 (대략):
  1GB  → ~10ms
  10GB → ~100ms
  25GB → ~200~500ms

fork 중에는 메인 스레드가 블로킹됨!
→ 대용량 Redis에서는 fork 지연이 서비스에 영향을 줄 수 있음

fork 지연 모니터링

BASH
# INFO persistence에서 확인
INFO persistence

# latest_fork_usec: 마지막 fork의 마이크로초 지연
# 예: latest_fork_usec: 125000 → 125ms

fork 지연 줄이는 방법

  1. 데이터 크기 제한: 가능하면 인스턴스당 25GB 이하 유지
  2. replica에서 BGSAVE: 마스터 부하 감소
  3. vm.overcommit_memory = 1: fork 실패 방지
BASH
# fork 실패 방지 (Linux)
echo 1 > /proc/sys/vm/overcommit_memory
# 0: 기본 (커널이 판단), 1: 항상 허용, 2: 물리 메모리+스왑 이내만 허용

RDB 파일 구조

PLAINTEXT
┌──────────┬─────────┬──────────┬─────────────┬──────────┐
│ REDIS    │ RDB     │ AUX      │ DB 데이터    │ EOF +    │
│ (5bytes) │ version │ fields   │ (키-값 쌍)  │ checksum │
│ "REDIS"  │ "0011"  │ 메타데이터│             │ 8bytes   │
└──────────┴─────────┴──────────┴─────────────┴──────────┘

AUX 필드 (메타데이터)

PLAINTEXT
redis-ver: "7.2.4"
redis-bits: 64
ctime: 1711238400     (생성 시각)
used-mem: 10737418240  (메모리 사용량)
aof-base: 0

DB 데이터 영역

PLAINTEXT
각 키-값 쌍:
┌──────────┬───────────┬──────┬───────┐
│ 만료시각  │ 타입/인코딩│ 키    │ 값    │
│ (선택적) │           │      │       │
└──────────┴───────────┴──────┴───────┘

RDB 파일 검증

BASH
# redis-check-rdb로 파일 무결성 검사
redis-check-rdb /var/lib/redis/dump.rdb

# RDB 파일 크기 확인
ls -lh /var/lib/redis/dump.rdb

RDB 로드 과정

PLAINTEXT
서버 시작


AOF가 활성화되어 있는가?
    │ Yes → AOF 파일 로드
    │ No

RDB 파일이 존재하는가?
    │ Yes → RDB 파일 로드
    │ No → 빈 데이터베이스로 시작
BASH
# 로드 속도 (대략)
# RDB는 바이너리 포맷이라 매우 빠름
# 10GB RDB: ~10~20초 (SSD 기준)

# 로드 중에는 클라이언트 요청을 처리하지 않음
# 로드 진행 상황은 로그에서 확인
# * Loading RDB produced by version 7.2.4
# * DB loaded from disk: 15.234 seconds

실전 설정 패턴

패턴 1: 캐시 전용 (영속성 불필요)

BASH
save ""                    # RDB 비활성화
appendonly no              # AOF 비활성화

패턴 2: 적당한 영속성

BASH
save 300 10                # 5분마다 10회 이상 변경 시
save 60 10000              # 1분마다 10000회 이상 변경 시
rdbcompression yes
rdbchecksum yes

패턴 3: 높은 영속성 (RDB + AOF)

BASH
save 300 10
appendonly yes
appendfsync everysec       # 매초 fsync
aof-use-rdb-preamble yes   # 혼합 모드 (7.0+)

RDB의 장단점

장점

  • 컴팩트: 바이너리 포맷으로 파일 크기가 작음
  • 빠른 복구: AOF 대비 로딩 속도가 빠름
  • 백업 용이: 단일 파일로 백업/복사 간단
  • 성능: BGSAVE가 자식 프로세스에서 수행되어 서비스 영향 최소

단점

  • 데이터 유실: 마지막 스냅샷 이후 데이터 유실 가능
  • fork 비용: 대용량 데이터에서 fork 지연 발생
  • 메모리 오버헤드: CoW로 인한 추가 메모리 사용 (최대 2배)

모니터링

BASH
# RDB 관련 INFO 항목
INFO persistence

# 주요 확인 항목:
# rdb_last_save_time         — 마지막 저장 시각
# rdb_last_bgsave_status     — 마지막 BGSAVE 성공 여부
# rdb_last_bgsave_time_sec   — 마지막 BGSAVE 소요 시간
# rdb_current_bgsave_time_sec — 현재 진행 중인 BGSAVE 시간
# rdb_last_cow_size          — 마지막 BGSAVE의 CoW 메모리 사용량
# latest_fork_usec           — 마지막 fork 지연 (마이크로초)

정리

RDB 스냅샷은 Redis 영속성의 기본 메커니즘입니다.

  • fork() + Copy-on-Write로 서비스 중단 없이 스냅샷을 생성합니다
  • fork 직후에는 메모리를 공유하고, 수정된 페이지만 복사하여 메모리 효율적입니다
  • 대용량 데이터에서는 fork 지연CoW 메모리 오버헤드를 주의해야 합니다
  • THP 비활성화, vm.overcommit_memory = 1 설정이 권장됩니다
  • RDB는 컴팩트하고 복구가 빠르지만, 마지막 스냅샷 이후 데이터 유실 위험이 있습니다
  • 데이터 유실을 최소화하려면 AOF와 함께 사용하는 것이 좋습니다
댓글 로딩 중...