1. 문제 배경: Redis는 왜 갑자기 죽었을까? 2. 배경 지식: Redis의 BGSAVE와 Copy-on-Write 3. 실험 목적: Copy-on-Write가 실제로 OOM을 유발하는가? 4. 실험 환경: Docker 기반 Redis 컨테이너 구성 5. 실험 과정: Copy-on-Write 상황 유발 및 메모리 관측 6. 실험 결과: Copy-on-Write로 인한 OOM은 실제로 발생했다 7. 실험을 통해 알게 된 것 8. 결론 9. Github 링크
1. 문제 배경: Redis는 왜 갑자기 죽었을까?
Redis가 예고 없이 종료될 수 있습니다.
Redis의 maxmemory는 6GB로 설정되어 있었고,
컨테이너의 메모리 제한은 7GB였으며,
메모리 사용량이 한계에 가까운 시점에서 BGSAVE가 실행되었고,
그 결과 Redis가 Out Of Memory (OOM) 로 강제 종료되었습니다.
이유는, BGSAVE 중 발생한 Copy-on-Write로 인해 순간적으로 약 1.3GB의 추가 메모리가 필요했고 (6GB + 1.3GB = 7.3GB), 시스템(=컨테이너)의 memory limit인 7GB를 초과한 것이 원인이었습니다.
이 처럼 Redis를 운영할 때 maxmemory만 설정해서는 안전하지 않다는 것을 의미합니다. Redis 내부 설정뿐 아니라, 운영체제/컨테이너 수준에서 발생하는 메모리 증폭(COW)까지 고려한 설계가 필요합니다.
2. 배경 지식: Redis의 BGSAVE와 Copy-on-Write
Redis는 인메모리 캐시이지만, 데이터를 디스크에 저장하는 Persistence 기능을 통해 장애 이후에도 데이터를 복구할 수 있도록 지원합니다. 대표적인 방식은 다음 두 가지입니다:
RDB (Redis Database Snapshot): 일정 시점의 메모리 상태를 .rdb 파일로 저장 (BGSAVE로 트리거됨)
AOF (Append Only File): 모든 쓰기 명령을 로그 형식으로 저장 (BGREWRITEAOF로 재작성)
이번 실험에서 주목한 부분은 BGSAVE입니다. 이 명령은 fork를 통해 자식 프로세스를 생성하고, 이 자식이 메모리 상태를 파일로 저장합니다. 여기서 사용되는 기술이 바로 Copy-on-Write (COW)입니다.
Copy-on-Write
Copy-on-Write란? 부모와 자식 프로세스는 처음엔 메모리를 공유하지만, 어느 한쪽이 메모리를 변경하려 하면 해당 페이지를 복사한 뒤 변경합니다. 부모와 자식 프로세스가 처음에는 메모리를 공유하므로 효율적입니다. 하지만 부모와 자식프로세스가 공유한 모든 메모리에서 데이터를 변경하면 이 순간, 실제 메모리 사용량이 급격히 증가할 수 있습니다.
문제는 여기서 시작됩니다
Redis는 자체적으로 maxmemory 설정을 통해 사용할 수 있는 메모리 상한선을 지킬 수 있습니다. 하지만 fork 시점에 발생하는 Copy-on-Write는 일시적으로 큰 메모리 사용량을 유발(모든 공유메모리에서 데이터를 변경하는 경우)할 수 있습니다. 이로 인해 실제 메모리 사용량이 Docker 컨테이너 혹은 운영체제의 memory limit을 초과하면, Redis는 Out Of Memory (OOM)로 강제 종료될 수 있습니다. 즉, maxmemory는 Redis 내부의 동작을 제어할 뿐이며, 시스템 전체의 메모리 사용까지 보장하지는 못합니다.
3. 실험 목적: Copy-on-Write가 실제로 OOM을 유발하는가?
이론적으로는 문제가 있다는 걸 알고 있지만, 실제로 얼마나 위험한지 확인하고, 메모리 증폭이 어느 정도인지 수치적으로 검증해보기 위해 실험을 진행했습니다.
실험 시나리오
Redis의 maxmemory는 4~6GB로 제한
Docker 컨테이너의 memory limit은 각각 5GB, 7GB로 설정 (약 1GB 여유)
--appendonly no 설정으로 AOF는 비활성화
데이터 삽입 → 메모리 포화 직전 → BGSAVE 발생 유도
이 시점에서 Copy-on-Write가 발생하며 실제 메모리 사용량 급증 여부 관찰
관찰 항목
used_memory, used_memory_rss의 변화
항목
의미
측정 대상
실험에서의 의미
used_memory
Redis가 논리적으로 사용 중인 메모리
Redis 내부 할당 기준
maxmemory에 의해 제한되는 값
used_memory_rss
운영체제 입장에서 Redis 프로세스가 실제로 차지한 메모리(RSS)
물리 메모리(RAM) 기준
실제로 OOM의 원인이 되는 값
htop 상의 실시간 메모리 사용량
Redis가 OOM으로 종료되는 시점과 로그
fork 이후 실제 메모리 소비량이 얼마나 증가하는지
이 실험을 통해 Redis maxmemory만으로는 해결되지 않는 운영 리스크를 체감적으로 이해하고, 안정적인 설정 기준을 잡는 것이 목적입니다.
4. 실험 환경: Docker 기반 Redis 컨테이너 구성
이번 실험은 Redis의 Copy-on-Write(COW) 특성을 검증하기 위해, Windows 환경의 WSL에서 Docker를 활용해 서로 다른 메모리 제한을 갖는 두 개의 Redis 환경을 구성하여 진행했습니다.
services:
redis4g:
image: redis:latest
container_name: redis4g
command: > # 무조건 RDB - BGSAVE
redis-server
--maxmemory 4gb
--appendonly no
ports:
- "6379:6379"
deploy:
resources:
limits:
memory: 5gb # Docker 컨테이너 메모리 한계 (엄격히 제한)
cpus: '1.0'
tty: true # htop 실행 가능하게
redis6g:
image: redis:latest
container_name: redis6g
command: > # 무조건 RDB - BGSAVE
redis-server
--maxmemory 6gb
--appendonly no
ports:
- "6380:6379"
deploy:
resources:
limits:
memory: 7gb # Docker 컨테이너 메모리 한계 (엄격히 제한)
cpus: '1.0'
tty: true # htop 실행 가능하게
컨테이너 설명
컨테이너 이름
Redis maxmemory
컨테이너 메모리 한도
Port
redis4g
4GB
5GB
6379
redis6g
6GB
7GB
6380
각 설정의 의미
1. --maxmemory
Redis의 메모리 사용 한도를 설정합니다. 예를 들어 redis4g는 4GB까지만 데이터를 저장할 수 있습니다. 그 이상으로 데이터가 들어오면 eviction 또는 오류가 발생하게 됩니다.
이 설정은 Redis 입장에서의 "최대 메모리 사용량" 제한이며, 실제 OS 수준 메모리 사용은 이보다 더 클 수 있습니다. 특히 COW 상황에서는 더 많은 메모리를 사용할 수 있습니다.
2. --appendonly no
AOF(Append Only File)를 끄고, RDB 기반 스냅샷만 사용하는 환경입니다. 이를 통해 BGSAVE 발생 시점에 Copy-on-Write가 어떻게 동작하는지 집중적으로 관찰할 수 있습니다.
AOF를 끄면 BGREWRITEAOF는 발생하지 않으며, BGSAVE만 사용하게 됩니다.
3. deploy.resources.limits.memory
Docker 컨테이너 자체의 메모리 사용량을 제한합니다. 이는 Redis가 설정한 maxmemory보다 약간 더 여유롭게 설정해, Redis 내부의 동작이 먼저 제한되도록 유도하고, Copy-on-Write 시점의 추가 메모리 사용이 OOM(Out of Memory)을 유발하는지를 실험하는 목적입니다.
예: Redis는 4GB까지만 데이터를 저장하게 제한하고, 컨테이너는 5GB까지 허용하므로 여유 1GB가 있음. 이 상태에서 BGSAVE를 수행하면 fork된 프로세스가 COW로 인해 메모리를 추가 사용하면서 5GB를 초과할 수 있음 → 이때 Redis가 죽는지 확인가능합니다.
4. tty: true
컨테이너 안에서 htop 같은 터미널 UI 도구를 실행할 수 있게 하기 위한 설정입니다. 실시간으로 메모리, RSS, CPU 사용량을 관찰하기 위한 목적입니다.
yes A 명령을 통해 1MB 크기의 텍스트 파일을 생성하고, 이를 5000개 Redis 키로 삽입하여 약 5GB의 데이터를 적재했습니다. 이 작업은 Redis의 maxmemory 한계에 근접하도록 설계되었습니다.
for i in $(seq 1 5000); do
cat /tmp/1mb.txt | redis-cli -x SET key:$i > /dev/null
done
3) 시스템 모니터링 도구 실행
Redis 상태 외에도 시스템 수준에서 메모리/CPU 변화와 병목을 추적하기 위해 다음 명령들을 병렬 실행했습니다:
vmstat 1 300: 1초 간격으로 5분간 CPU/메모리 사용량 수집
top: 전체 시스템 부하 캡처
redis-cli latency latest: 지연 이벤트 추적
4) BGSAVE 수동 실행
메모리가 가득 찬 상태에서 BGSAVE 명령을 수동으로 실행하여, Redis가 fork()를 수행하도록 유도하고, Copy-on-Write가 본격적으로 발생하도록 만들었습니다.
docker exec $container redis-cli BGSAVE
5) 동일 키 덮어쓰기 → Copy-on-Write 유도
기존에 삽입한 5000개의 키를 반복적으로 덮어쓰며, 메모리 페이지가 복사되는 Copy-on-Write 현상을 유도했습니다. 이때 used_memory_rss를 지속적으로 추적하여, 메모리 폭증 시점을 탐지했습니다.
for i in $(seq 1 5000); do
cat /tmp/1mb.txt | redis-cli -x SET key:$i
done
used_memory_rss가 임계치를 초과하면 로그에 경고를 출력하고, OOM 여부를 기록했습니다.
6) BGSAVE 종료 대기 및 latency 이벤트 수집
rdb_bgsave_in_progress 상태를 통해 BGSAVE 완료 여부를 확인하고, latency 이벤트 이력을 추가로 수집하여 fork, write-behind 등 성능 병목 원인을 확인했습니다.
6. 실험 결과: Copy-on-Write로 인한 OOM은 실제로 발생했다
실험 결과, BGSAVE와 그에 따른 Copy-on-Write(COW)는 실제 메모리 사용량(used_memory_rss)에 증가를 유발했으며, 이 증가폭은 Redis가 보유한 데이터량에 따라 달라지는 경향을 보였습니다. 특히 redis6g 인스턴스에서는 메모리 사용량이 memory limit을 초과하면서 Redis 프로세스가 OOM(Out Of Memory)으로 종료되었고, 반면 redis4g는 종료되지 않고 정상 유지되었습니다.
주요 결과 요약
컨테이너 maxmemory memory limit OOM 경고 발생 시점 종료 여부
컨테이너
maxmemory
memory limit
종료 여부
redis4g
4GB
5GB
정상 유지
redis6g
6GB
7GB
OOM으로 종료
두 인스턴스 모두 BGSAVE 이후 used_memory_rss가 used_memory보다 약 15~30% 더 높은 수준으로 증가했습니다.
redis6g는 더 많은 데이터를 보유하고 있었기 때문에, Copy-on-Write 시 복사되는 페이지 수도 많아졌고, 결국 메모리 제한을 초과하여 Redis가 OOM으로 종료되었습니다.
반면 redis4g는 보유 데이터가 적어 Copy-on-Write 영향도 작아, 메모리 여유를 유지하며 정상적으로 동작을 유지했습니다.
vmstat 결과에서는 wa(I/O wait), si, so(swap in/out) 등의 값이 함께 증가하여, 시스템 레벨에서도 디스크 및 메모리 경합이 발생했음을 알 수 있었습니다.
7. 실험을 통해 알게 된 것
1. maxmemory 설정만으로는 안전하지 않다 BGSAVE 시점에 발생하는 fork와 Copy-on-Write로 인해, Redis의 실제 물리 메모리 사용량(used_memory_rss)은 maxmemory를 초과할 수 있습니다. 이로 인해 컨테이너나 시스템의 memory limit을 넘어서며 OOM으로 종료될 수 있습니다.
2. 보유 데이터량이 많을수록 COW 리스크는 커진다 Redis가 저장 중인 데이터가 많을수록, COW로 인해 복사되는 메모리 페이지 수도 많아지며, 그만큼 물리 메모리 소모량도 증가합니다. 데이터량 자체가 리스크의 크기를 결정하는 핵심 요인이 됩니다.
3. 충분한 물리 메모리 여유는 필수다 운영 환경에서는 maxmemory 대비 최소 30~40% 이상의 여유 메모리를 확보해야 안정적인 서비스 운영이 가능합니다. Docker 환경에서는 memory limit을 너무 타이트하게 설정하지 말고, swap 사용 여부도 정책적으로 고려해야 합니다.
4. BGSAVE 타이밍은 상황에 맞게 조정되어야 한다 Redis는 기본적으로 조건에 따라 자동으로 BGSAVE를 수행하지만, 메모리 여유가 없는 시점에서 실행될 경우 문제가 됩니다. 필요한 경우 자동 저장을 비활성화하고, 외부에서 제어하는 방식도 고려해야 합니다.
5. 지표 기반의 상시 모니터링이 중요하다 latency latest, info memory, vmstat, used_memory_rss 등의 지표를 주기적으로 모니터링해야 합니다. 특히 used_memory_rss가 maxmemory보다 일정 비율 이상 초과하거나, fork 지연이 자주 발생하는 경우 경고 알림을 통해 장애를 사전 방지할 수 있어야 합니다.
6. Redis 아키텍처 자체에 대한 재검토도 필요할 수 있다 안정성과 쓰기 성능이 동시에 요구되는 환경에서는 Redis 단일 인스턴스만으로는 운영 리스크가 존재합니다. Redis Cluster, Sentinel 구성 또는 RDBMS와의 계층적 캐시 구조 도입을 고려할 필요가 있습니다.
8. 결론
Redis는 뛰어난 성능의 인메모리 데이터베이스지만, BGSAVE 수행 시 발생하는 fork와 Copy-on-Write 동작은 메모리 사용량을 순간적으로 폭증시킬 수 있는리스크를 안고 있습니다.
이번 실험에서는 동일한 데이터를 보유한 Redis 인스턴스에서 BGSAVE와 덮어쓰기 작업을 병행하며 Copy-on-Write 상황을 유도했고, 그 결과 실제 물리 메모리 사용량(used_memory_rss)이 maxmemory보다 최대 30% 이상 높아지는 현상이 관찰되었습니다. 인스턴스에 따라 OOM 종료가 발생하기도 했습니다.
따라서, Redis를 안정적으로 운영하기 위해서는 단순한 설정 수준을 넘어서, 시스템 전체를 아우르는 메모리 전략과 모니터링 체계가 반드시 필요합니다.