Redis: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk
안녕하세요, BlueByte입니다. 애플리케이션은 떠 있고 Redis 는 PING 에 답하며 GET 도 데이터를 돌려주는데, 쓰기만 전부 이렇게 돌아옵니다. (error) MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error. 무엇도 죽지 않았습니다. 마지막 백그라운드 저장이 실패했기 때문에 Redis 가 일부러 쓰기를 거부하는 것이고, 복구할 때가 아니라 지금 알아채라는 뜻입니다. 오늘은 두 가지 MISCONF 응답, 그 뒤에 있는 네 가지 실패, 어느 쪽인지 가려내는 법, 원인별 해결, 그리고 다음번엔 사용자보다 먼저 알 수 있게 만드는 법까지 하나씩 짚어보겠습니다.
MISCONF 응답은 둘인데 스냅샷 얘기는 하나뿐입니다
이 접두사로 시작하는 오류가 Redis 에는 둘 있고, 서로 다른 곳을 가리킵니다. 위의 긴 쪽이 RDB 응답으로, 스냅샷이 켜져 있고 마지막 백그라운드 저장이 실패했을 때 나옵니다. 다른 하나는 짧고 운영체제의 메시지를 그대로 달고 옵니다.
(error) MISCONF Errors writing to the AOF file: No space left on device이쪽은 스냅샷이 아니라 append-only file 이고, RDB 상태가 아니라 aof_last_write_status 가 움직입니다. 시작하기 전에 응답 전체를 읽으세요. MISCONF 다음 단어가 INFO persistence 의 어느 절반을 봐야 하는지 정해 줍니다.
실패한 저장 한 번이 쓰기를 멈추는 이유
버그도 아니고 잘못 걸린 안전장치도 아닙니다. 이것이 기본값이고, redis.conf 가 스스로 설명합니다.
# By default Redis will stop accepting writes if RDB snapshots are enabled
# (at least one save point) and the latest background save failed.
# This will make the user aware (in a hard way) that data is not persisting
# on disk properly, otherwise chances are that no one will notice and some
# disaster will happen.
#
# If the background saving process will start working again Redis will
# automatically allow writes again.
stop-writes-on-bgsave-error yes여기서 기억해 둘 것이 둘입니다. 이 동작은 save 지점이 하나 이상 설정돼 있을 때만 적용되므로, save "" 로 띄운 인스턴스는 이 상태에 아예 들어가지 않습니다. 그리고 스스로 풀립니다. 백그라운드 저장이 한 번 성공하는 순간 재시작도 설정 변경도 없이 쓰기가 다시 허용됩니다. 아래 해결이 서비스 재시작이 아니라 BGSAVE 한 번으로 끝나는 이유입니다.
그 뒤의 네 가지 실패: 공간, 권한, read-only 마운트, fork
Redis 는 dir 안의 임시 파일에 스냅샷을 쓰고, 파일이 완성된 뒤에야 dbfilename(기본값 dump.rdb) 위로 rename 합니다. 단계마다 고유한 로그 줄을 남기기 때문에 로그를 먼저 읽는 편이 빠릅니다.
- 디스크가 찼습니다.
Write error while saving DB to the disk(...): No space left on device. 애플리케이션 로그와 볼륨을 나눠 쓰고 있거나, 중간에 죽은 자식 프로세스가 남긴temp-*.rdb파일이 쌓여 있는 경우가 많습니다. - 디렉터리를 redis 사용자가 쓸 수 없습니다.
Failed opening the temp RDB file temp-2481.rdb (in server root dir /var/lib/redis) for saving: Permission denied. 컨테이너 bind mount, root 로 푼 백업 복원, 정책 변경 뒤에 흔합니다. - rename 단계에서 read-only 파일시스템을 만났습니다.
Error moving temp DB file temp-2481.rdb on the final destination dump.rdb (in server root dir /var/lib/redis): Read-only file system. 자식은 스냅샷을 끝까지 썼는데 제자리에 놓지 못한 것입니다. - fork() 가 실패했습니다.
Can't save in background: fork: Cannot allocate memory. Redis 는 스냅샷을 쓸 자식을 fork 하고 copy-on-write 에 기대므로 이론상 자식은 거의 메모리를 쓰지 않습니다. 커널이 왜 다르게 보는지는 FAQ 가 설명합니다.overcommit_memory가 0 이면 리눅스는 어떤 페이지가 바뀔지 미리 알 수 없어서, 부모의 모든 페이지를 복제할 만큼 여유 RAM 이 없으면 fork 를 거부합니다. 문서의 예를 그대로 옮기면, 데이터셋 3 GB 에 여유 메모리 2 GB 면 실패합니다.
무엇도 바꾸기 전에 INFO persistence 와 로그부터 읽기
쓰기가 막혔는지를 결정하는 상태, 그다음 그 상태가 의존하는 설정 순으로 봅니다.
redis-cli info persistence | grep -E 'rdb_last_bgsave_status|rdb_changes_since_last_save|rdb_last_save_time|aof_enabled|aof_last_write_status'
redis-cli config get dir dbfilename save stop-writes-on-bgsave-errorrdb_changes_since_last_save:184213
rdb_last_save_time:1758501902
rdb_last_bgsave_status:err
aof_enabled:0
aof_last_write_status:ok1) "dir"
2) "/var/lib/redis"
3) "dbfilename"
4) "dump.rdb"
5) "save"
6) "3600 1 300 100 60 10000"
7) "stop-writes-on-bgsave-error"
8) "yes"rdb_last_bgsave_status 는 마지막 RDB 저장 작업의 상태로 문서화돼 있고, err 가 쓰기를 끄는 플래그입니다. rdb_last_save_time 은 마지막으로 성공한 저장의 epoch 타임스탬프이니 date -d @1758501902 로 이 인스턴스가 디스크에 아무것도 없이 얼마나 오래 돌았는지 보고, rdb_changes_since_last_save 로 잃을 양을 봅니다. aof_enabled 가 1 이고 aof_last_write_status 가 err 라면 AOF 쪽 변형입니다.
이제 원인을 찾습니다. 원인은 로그와 파일시스템에만 있습니다.
sudo journalctl -u redis-server --since '2 hours ago' | grep -iE 'saving|fork|rdb'
df -h /var/lib/redis
df -i /var/lib/redis
stat -c '%n %U:%G %a' /var/lib/redis
sudo -u redis test -w /var/lib/redis && echo "writable by redis" || echo "NOT writable by redis"
cat /proc/sys/vm/overcommit_memory결과는 짧은 분기표처럼 읽으면 됩니다. No space left on device 줄과 df -h 100% 면 디스크, 공간은 남았는데 df -i 가 100% 면 inode 입니다. Permission denied 에 stat 의 소유자가 redis 가 아니면 디렉터리 권한, fork: Cannot allocate memory 에 /proc/sys/vm/overcommit_memory 가 0 이면 커널 설정입니다. 여기까지는 아무것도 쓰지 않으니 라이브 인스턴스에서 그대로 돌려도 됩니다.
원인을 고치고 BGSAVE 한 번으로 증명하기
원인마다 고치는 방법이 다르고, 어느 것도 재시작을 요구하지 않습니다.
# 디스크 가득: 볼륨을 쓰고 있는 것을 찾고, 죽은 자식이 남긴 임시 파일을 나열한다.
sudo du -xh --max-depth=1 /var/lib/redis | sort -h | tail -5
sudo find /var/lib/redis -name 'temp-*.rdb' -mmin +60 -ls
# 저장이 돌고 있지 않을 때만 지운다 — 진행 중인 자식의 임시 파일을 지우면
# 그 저장이 깨진다. rdb_bgsave_in_progress 가 0 이어야 한다.
redis-cli info persistence | grep rdb_bgsave_in_progress
sudo find /var/lib/redis -name 'temp-*.rdb' -mmin +60 -delete
# 디렉터리 권한: redis 사용자에게 돌려준다.
sudo chown redis:redis /var/lib/redis
sudo chmod 750 /var/lib/redis
# fork: Cannot allocate memory — 문서가 요구하는 설정을 지금과 재부팅 후 모두에.
sudo sysctl vm.overcommit_memory=1
echo 'vm.overcommit_memory = 1' | sudo tee /etc/sysctl.d/60-redis-overcommit.conf그다음 Redis 에 다시 시도하게 하고 플래그가 풀리는지 봅니다.
redis-cli bgsave
sleep 5
redis-cli info persistence | grep -E 'rdb_last_bgsave_status|rdb_last_save_time'
redis-cli set bluebyte:canary okBackground saving started
rdb_last_bgsave_status:ok
rdb_last_save_time:1758616355
OK마지막 줄의 OK 가 핵심입니다. 무언가를 재시작해서가 아니라 상태가 바뀌었기 때문에 쓰기가 저절로 돌아온 것입니다.
실제 사례: 제 볼륨을 스스로 채운 세션 저장소
20 GB 볼륨 위의 세션 저장소 Redis 가 02:10 부터 쓰기를 거부하기 시작합니다. INFO persistence 에는 rdb_last_bgsave_status:err 와 네 시간 전의 rdb_last_save_time 이 있고, df -h 는 볼륨 100% 사용을 보고하며, 로그에는 Write error while saving DB to the disk(...): No space left on device 가 저장 지점마다 반복됩니다. du 로 보면 5.8 GB 짜리 dump.rdb, 앞서 죽은 자식들이 남긴 temp-*.rdb 세 개, 그리고 같은 볼륨에 있는 애플리케이션 로그 디렉터리가 나옵니다. 담당자는 로그를 다른 파일시스템으로 옮기고 남은 임시 파일을 지운 뒤, Redis 를 건드리기 전에 df -h 가 44% 를 가리키는지부터 확인합니다. 그다음 redis-cli bgsave 가 Background saving started 로 답하고, 몇 초 뒤 rdb_last_bgsave_status 가 ok 가 되며, 다음 SET 이 성공합니다. 재시작도 없고, 이미 저장되지 않았던 분량 말고는 잃은 키도 없습니다. 진짜 마무리는 그다음입니다. dir 에 전용 파일시스템을 줘서 애플리케이션 로그가 다시는 Redis 를 끌어내리지 못하게 합니다.
스위치를 끄는 것은 워크어라운드이고, 무엇을 가리는지
지금 당장 쓰기가 필요하고 그 대가를 받아들인다면, 문서화된 스위치가 있습니다.
redis-cli config set stop-writes-on-bgsave-error noredis.conf 주석은 이것이 합리적인 조건을 분명히 적어 둡니다. 서버와 영속성에 대한 제대로 된 모니터링을 갖춰 두었다면, 디스크나 권한에 문제가 있어도 Redis 가 평소처럼 동작하도록 이 기능을 끄고 싶을 수 있다는 것입니다. 무엇을 사는지 보세요. 쓰기는 즉시 돌아오고, 스냅샷은 여전히 실패하며, 달라진 것은 아무도 그 사실을 듣지 못한다는 점뿐입니다. 저장이 성공하기 전에 프로세스가 재시작되면 rdb_last_save_time 이후의 모든 것이 사라집니다. CONFIG SET 은 CONFIG REWRITE 를 따라 하지 않으면 재시작을 넘기지 못하므로, 이렇게 "고친" 인스턴스는 다음 배포 뒤 다시 막히는 일이 잦습니다. 잃어도 되는 캐시에 한해 의도적으로 고르고, 어느 쪽을 택하든 상태 알람은 남겨 두세요.
재발을 막기
PING 이 아니라 rdb_last_bgsave_status 에 알람을 거세요. 이 상태의 인스턴스는 PING 에 멀쩡히 답합니다. 데이터셋만이 아니라 저장 동작까지 감안해 머신을 잡으세요. 운영 가이드는 쓰기가 많은 애플리케이션에서 RDB 저장이나 AOF 재작성 중에 Redis 가 평소의 최대 두 배 메모리를 쓸 수 있다고 적고, 물리 RAM 보다 낮은 값으로 maxmemory 를 명시하라고 권합니다. 여유가 10 GB 라고 생각되면 8~9 로 잡으라는 식입니다. 같은 가이드가 요구하는 대로 /etc/sysctl.conf 에 vm.overcommit_memory = 1 을 넣고, echo never > /sys/kernel/mm/transparent_hugepage/enabled 로 transparent huge pages 를 끄세요. dir 에는 전용 파일시스템을 주고, 여유 공간을 0 이 아니라 dump.rdb 크기에 견줘 감시하세요. 저장은 rename 전에 사본 하나가 더 들어갈 자리를 필요로 합니다. 정말 순수 캐시라면 저장 지점을 켠 채 안전장치만 끄지 말고, 설정에 save "" 로 그렇다고 적어 두세요.
OOM command not allowed·READONLY 와의 차이
(error) OOM command not allowed when used memory > 'maxmemory'. 는 디스크가 아니라 메모리 한도입니다. 데이터셋이 maxmemory 에 닿았고 제거 정책이 내보낼 수 있는 것이 없다는 뜻입니다. 이쪽도 읽기는 되기 때문에 둘을 헷갈리기 쉬운데, INFO persistence 는 rdb_last_bgsave_status:ok 를 보여주고 해결은 maxmemory·제거 정책·데이터 줄이기 쪽입니다. (error) READONLY You can't write against a read only replica. 는 프라이머리가 아니라 레플리카에 연결돼 있다는 뜻으로, 라우팅이나 페일오버 문제이고 디스크에는 아무 문제가 없습니다. 어느 오류 문자열을 실제로 받았는지부터 확인하고 손을 대세요.
다음에 읽기는 되는데 쓰기만 멈추면 이 순서를 거꾸로 따라가 보세요. 어느 MISCONF 응답인지, rdb_last_bgsave_status 가 무엇을 말하는지, 마지막 저장 시도에 로그가 무엇을 남겼는지 본 뒤에야 디스크를 고칠지 대가를 받아들일지 고르면 됩니다.
관련 질문
디스크를 고친 뒤 Redis 를 재시작해야 하나요?
아닙니다. stop-writes-on-bgsave-error 는 스스로 풀립니다. 백그라운드 저장이 한 번 성공하면 Redis 가 알아서 쓰기를 다시 허용합니다. redis-cli bgsave 를 돌리고 rdb_last_bgsave_status 가 err 에서 ok 로 바뀌는지 본 다음 쓰기를 시도하세요. 여기서는 오히려 재시작이 더 위험합니다. 아직 저장되지 않은 것은 메모리에만 있기 때문입니다.
읽기는 되는데 왜 쓰기만 막나요?
응답이 그대로 말해 줍니다. 데이터 집합을 바꿀 수 있는 명령만 비활성화됩니다. 메모리의 데이터셋은 멀쩡하고 읽을 수 있으며, 실패한 것은 그 사본을 디스크에 올리는 일입니다. 쓰기를 막아 두면 문제가 고쳐지기 전까지 메모리 버전과 디스크 버전이 더 벌어지지 않습니다.
MISCONF 가 뜨는데 save 줄이 비어 있습니다.
그렇다면 AOF 쪽 변형입니다. 문구가 MISCONF Errors writing to the AOF file: 로 시작하고 뒤에 운영체제 메시지가 붙습니다. rdb_ 필드 대신 INFO persistence 의 aof_enabled 와 aof_last_write_status 를 보세요. save "" 이고 AOF 도 꺼져 있다면 이 오류는 아예 나타날 수 없습니다.
stop-writes-on-bgsave-error no 를 그대로 둬도 괜찮나요?
모니터링이 있을 때만입니다. redis.conf 자체가 그 조건을 답니다. 쓰기는 되살아나지만 실패하는 저장은 그대로이므로, 저장이 성공하기 전에 재시작된 인스턴스는 rdb_last_save_time 이후 전부를 잃습니다. 설정한다면 CONFIG REWRITE 로 고정하고 rdb_last_bgsave_status 에 알람을 거세요. 그러지 않으면 시끄러운 실패를 조용한 실패와 맞바꾼 셈이 됩니다.
free 로는 메모리가 넉넉한데 fork 가 Cannot allocate memory 로 실패합니다.
문서에 나오는 overcommit 사례입니다. vm.overcommit_memory 가 0 이면 리눅스는 copy-on-write 덕분에 실제로는 대부분 복사되지 않는데도 부모의 모든 페이지를 복제할 만큼 여유가 없으면 fork 를 거부합니다. vm.overcommit_memory=1 로 두고 sysctl 에 고정하세요. 쓰기가 많은 인스턴스는 저장 중에 평소의 두 배까지 메모리를 쓸 수 있다는 점도 함께 감안해야 합니다.
참고 자료
Haneul Seo
Infrastructure engineer · 10+ years running Linux fleets
같은 카테고리 다른 글
Git: fatal: detected dubious ownership in repository
Git 2.35.2의 CVE-2022-24765 수정 이후, Git은 작업 트리나 .git 디렉터리의 소유자가 명령을 실행한 사용자와 다르면 저장소를 읽지 않습니다. 컨테이너, CI 작업, sudo 세션, 공유 드라이브에서 주로 나타납니다. 저장소가 내 것이어야 한다면 소유권을 바로잡고, 아니라면 global 설정의 safe.directory에 정확한 경로를 추가하세요 — Git은 이 설정을 저장소 자신의 설정에서는 무시합니다.
Kubernetes: Internal error occurred: failed calling webhook
admission webhook이 쓰기 요청 앞에 서 있는데 API server가 응답을 받지 못했고, 기본값인 failurePolicy: Fail이 그 침묵을 거부로 바꾼 것입니다. 메시지 끝부분이 곧 진단입니다 — context deadline exceeded는 호출이 도달하지 못한 것, no endpoints available은 떠 있는 게 없는 것, x509 줄은 API server가 webhook 인증서를 신뢰하지 않는 것입니다. 원인마다 해결이 다르고, 어느 것도 매니페스트 문제가 아닙니다.
MySQL: ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
다른 트랜잭션이 쥐고 있는 행 잠금을 innodb_lock_wait_timeout 만큼 기다리다 포기한 것입니다. sys.innodb_lock_waits 가 막고 있는 세션을 지목하고 KILL 문까지 만들어 주며, blocking_query 가 NULL 이면 블로커가 열린 트랜잭션 위에서 놀고 있다는 뜻입니다. 재시도 로직이 가장 자주 틀리는 지점은 따로 있습니다. 기본값에서는 타임아웃된 문장 하나만 롤백되므로, 트랜잭션은 그대로 열린 채 앞서 잡은 잠금을 전부 쥐고 있습니다.
Node.js: FATAL ERROR: Reached heap limit — JavaScript heap out of memory (종료 코드 134)
V8 힙에는 시스템 메모리와 Node 릴리스에서 유도되는 자기만의 천장이 있고, 가진 RAM 보다 훨씬 낮은 경우가 흔합니다. 빌드나 서버가 거기 닿으면 V8 은 FATAL ERROR: Reached heap limit 과 종료 코드 134 로 스스로 중단합니다. v8.getHeapStatistics().heap_size_limit 으로 실제 한도를 읽은 뒤, 큰 작업은 --max-old-space-size(MiB)나 NODE_OPTIONS 로 올리고, 컨테이너 안에서는 cgroup 한도보다 낮게 잡고, 오래 도는 프로세스의 누수는 --heapsnapshot-near-heap-limit 으로 잡으세요. FATAL ERROR 줄 없이 137 로 끝나면 컨테이너 kill 이지 이 오류가 아닙니다.
Docker: 컨테이너 시작 직후 "exec format error" — 플랫폼이 다른 이미지, 에뮬레이터 없음, 셔뱅 없는 스크립트
컨테이너가 첫 명령에서 exec format error 로 끝납니다. 커널의 ENOEXEC, 즉 파일은 있지만 여기서는 실행할 수 없다는 뜻입니다. 실제로는 한 CPU 아키텍처에서 빌드한 이미지(Apple 실리콘 Mac은 linux/arm64 를 만듭니다)를 binfmt_misc 에 QEMU 핸들러가 없는 다른 아키텍처(x86_64 서버)에서 돌리거나, 엔트리포인트 스크립트의 첫 줄이 셔뱅이 아닌 경우입니다. uname -m, docker image inspect, ls /proc/sys/fs/binfmt_misc 로 원인을 가르고, docker buildx build --platform 을 명시(또는 두 플랫폼 매니페스트 목록 발행)하거나, 에뮬레이션이 목적이면 QEMU 등록과 --platform, 스크립트라면 #!/bin/sh 한 줄로 해결합니다.
curl: (60) SSL certificate problem: unable to get local issuer certificate
curl이 서버가 보낸 인증서 체인을 따라가다가, 자기가 읽는 CA 저장소에 발급자가 없는 인증서에 닿아 종료 코드 60으로 연결을 거부한 것입니다. 발급자가 없는 이유는 넷 중 하나입니다. 서버가 중간 인증서 없이 리프만 보내거나(브라우저는 스스로 받아 와서 가려 줍니다), TLS 검사 프록시가 컨테이너·러너가 신뢰하지 않는 회사 CA로 사이트를 다시 서명했거나, curl이 생각과 다른 CA 번들(CURL_CA_BUNDLE, SSL_CERT_FILE, 벤더 curl)을 읽고 있거나, ca-certificates 패키지가 너무 오래된 경우입니다. curl -v로 어느 저장소를 썼는지, openssl s_client로 서버가 무엇을 보냈는지 보고 고리가 빠진 쪽을 고친 뒤 -w '%{ssl_verify_result}'로 확인합니다.